Bläddra i källkod

Merge pull request #438 from Lezek123/council-members-identity

Council - identify candidates/members by membership
Lezek123 4 år sedan
förälder
incheckning
055fea4bbf

+ 2 - 2
packages/joy-election/src/Applicant.tsx

@@ -7,7 +7,7 @@ import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls } from '@polkadot/react-api/with';
 import { AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import CandidatePreview from "./CandidatePreview";
 
 import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
@@ -29,7 +29,7 @@ class Applicant extends React.PureComponent<Props> {
       <Table.Row>
         <Table.Cell>{index + 1}</Table.Cell>
         <Table.Cell>
-          <AddressMini value={accountId} isShort={false} isPadded={false} withBalance={true} />
+          <CandidatePreview accountId={accountId}/>
         </Table.Cell>
         <Table.Cell style={{ textAlign: 'right' }}>
           {formatBalance(calcTotalStake(stake))}

+ 35 - 0
packages/joy-election/src/CandidatePreview.tsx

@@ -0,0 +1,35 @@
+import React from "react";
+import AddressMini from "@polkadot/react-components/AddressMiniJoy";
+import MemberByAccount from "@polkadot/joy-utils/MemberByAccountPreview";
+import { AccountId } from "@polkadot/types/interfaces";
+
+import styled from 'styled-components';
+
+const StyledCouncilCandidate = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  padding: 1rem;
+`;
+
+const CandidateMembership = styled.div``;
+
+const CandidateAddress = styled.div`
+  margin-left: auto;
+`;
+
+type CouncilCandidateProps = {
+  accountId: AccountId | string;
+};
+
+const CouncilCandidate: React.FunctionComponent<CouncilCandidateProps> = ({ accountId }) => (
+  <StyledCouncilCandidate>
+    <CandidateMembership>
+      <MemberByAccount accountId={accountId} />
+    </CandidateMembership>
+    <CandidateAddress>
+      <AddressMini value={accountId} isShort={false} isPadded={false} withBalance={true} />
+    </CandidateAddress>
+  </StyledCouncilCandidate>
+);
+
+export default CouncilCandidate;

+ 2 - 2
packages/joy-election/src/Council.tsx

@@ -5,7 +5,7 @@ import { I18nProps } from "@polkadot/react-components/types";
 import { withCalls } from "@polkadot/react-api/with";
 import { Table } from "semantic-ui-react";
 import { formatBalance } from "@polkadot/util";
-import AddressMini from "@polkadot/react-components/AddressMiniJoy";
+import CouncilCandidate from './CandidatePreview';
 
 import { calcBackersStake } from "@polkadot/joy-utils/index";
 import { Seat } from "@joystream/types/";
@@ -39,7 +39,7 @@ class Council extends React.PureComponent<Props, State> {
             <Table.Row key={index}>
               <Table.Cell>{index + 1}</Table.Cell>
               <Table.Cell>
-                <AddressMini value={seat.member} isShort={false} isPadded={false} withBalance={true} />
+                <CouncilCandidate accountId={seat.member} />
               </Table.Cell>
               <Table.Cell>{formatBalance(seat.stake)}</Table.Cell>
               <Table.Cell>{formatBalance(calcBackersStake(seat.backers))}</Table.Cell>

+ 2 - 1
packages/joy-election/src/SealedVote.tsx

@@ -12,6 +12,7 @@ import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
 import { SealedVote } from '@joystream/types/';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import CandidatePreview from "./CandidatePreview";
 import { findVoteByHash } from './myVotesStore';
 
 type Props = ApiProps & I18nProps & {
@@ -29,7 +30,7 @@ class Comp extends React.PureComponent<Props> {
 
     if (sealedVote.vote.isSome) {
       const candidateId = sealedVote.vote.unwrap();
-      return <AddressMini value={candidateId} isShort={false} isPadded={false} withBalance={true} />;
+      return <CandidatePreview accountId={candidateId} />;
     } else {
       const revealUrl = `/council/reveals?hashedVote=${hash.toHex()}`;
       return <Link to={revealUrl} className='ui button primary inverted'>Reveal this vote</Link>;

+ 12 - 12
packages/joy-election/src/VoteForm.tsx

@@ -8,17 +8,18 @@ import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls, withMulti } from '@polkadot/react-api/with';
 import { AccountId, Balance } from '@polkadot/types/interfaces';
-import { Button, Input, Labelled, InputAddress } from '@polkadot/react-components/index';
+import { Button, Input, Labelled } from '@polkadot/react-components/index';
 import { SubmittableResult } from '@polkadot/api';
 import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
-import { accountIdsToOptions, hashVote } from './utils';
+import { hashVote } from './utils';
 import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index';
 import TxButton from '@polkadot/joy-utils/TxButton';
 import InputStake from '@polkadot/joy-utils/InputStake';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import CandidatePreview from "./CandidatePreview";
 import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import MembersDropdown from "@polkadot/joy-utils/MembersDropdown";
 import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
 
@@ -63,7 +64,6 @@ class Component extends React.PureComponent<Props, State> {
     const { myAddress } = this.props;
     const { applicantId, stake, salt, isStakeValid, isFormSubmitted } = this.state;
     const isFormValid = nonEmptyStr(applicantId) && isStakeValid;
-    const applicantOpts = accountIdsToOptions(this.props.applicants || []);
     const hashedVote = hashVote(applicantId, salt);
 
     const buildNewVote = (): Partial<NewVote> => ({
@@ -86,7 +86,9 @@ class Component extends React.PureComponent<Props, State> {
         <Table.Body>
           <Table.Row>
             <Table.Cell>Applicant</Table.Cell>
-            <Table.Cell><AddressMini value={applicantId} isShort={false} isPadded={false} withBalance={true}  /></Table.Cell>
+            <Table.Cell>
+              { applicantId && <CandidatePreview accountId={applicantId}/> }
+            </Table.Cell>
           </Table.Row>
           <Table.Row>
             <Table.Cell>Stake</Table.Cell>
@@ -115,13 +117,11 @@ class Component extends React.PureComponent<Props, State> {
       // New vote form:
       : <div>
         <div className='ui--row'>
-          <InputAddress
-            label='Applicant to vote for:'
-            onChange={this.onChangeApplicant}
-            type='address'
-            options={applicantOpts}
-            value={applicantId}
-            placeholder='Select an applicant you support'
+          <MembersDropdown
+            onChange={ (event, data) => this.onChangeApplicant(data.value as string) }
+            accounts={this.props.applicants || []}
+            value={applicantId || ''}
+            placeholder="Select an applicant you support"
           />
         </div>
         <InputStake

+ 1 - 1
packages/joy-proposals/src/Proposal/Body.tsx

@@ -7,7 +7,7 @@ import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 import TxButton from '@polkadot/joy-utils/TxButton';
 import { ProposalId } from "@joystream/types/proposals";
 import { MemberId } from "@joystream/types/members";
-import ProfilePreview from "./ProfilePreview";
+import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";
 import { useTransport } from "../runtime";
 import { usePromise } from "../utils";
 import { Profile } from "@joystream/types/members";

+ 1 - 1
packages/joy-proposals/src/Proposal/Details.tsx

@@ -4,7 +4,7 @@ import { ParsedProposal } from "../runtime/transport";
 import { ExtendedProposalStatus } from "./ProposalDetails";
 import styled from 'styled-components';
 
-import ProfilePreview from "./ProfilePreview";
+import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";
 
 const BlockInfo = styled.div`
   font-size: 0.9em;

+ 0 - 32
packages/joy-proposals/src/Proposal/ProfilePreview.tsx

@@ -1,32 +0,0 @@
-import React from "react";
-import { Image, Header } from "semantic-ui-react";
-import { IdentityIcon } from "@polkadot/react-components";
-import { Link } from "react-router-dom";
-
-type ProfileItemProps = {
-  avatar_uri: string;
-  root_account: string;
-  handle: string;
-  link?: boolean;
-};
-
-export default function ProfilePreview({ avatar_uri, root_account, handle, link = false }: ProfileItemProps) {
-  const Preview = (
-    <div className="details-profile">
-      {avatar_uri ? (
-        <Image src={avatar_uri} avatar floated="left" />
-      ) : (
-        <IdentityIcon className="image" value={root_account} size={40} />
-      )}
-      <Header as="h4" className="details-handle">
-        {handle}
-      </Header>
-    </div>
-  );
-
-  if (link) {
-    return <Link to={ `/members/${ handle }` }>{ Preview }</Link>;
-  }
-
-  return Preview;
-}

+ 0 - 9
packages/joy-proposals/src/Proposal/Proposal.css

@@ -20,15 +20,6 @@
     margin-bottom: 0.5em !important;
   }
 
-  h4.ui.header.details-handle {
-    margin: auto 0 auto 0.5rem;
-  }
-
-  .details-profile {
-    display: flex;
-    align-items: center;
-  }
-
   .ui.items > .item .extra.proposed-by {
     /* This is to ensure Proposed By: is above the name of the creator. The image is 50x50 and has 14pxs of margin right*/
     padding-left: 64px;

+ 1 - 1
packages/joy-proposals/src/Proposal/Votes.tsx

@@ -4,7 +4,7 @@ import useVoteStyles from "./useVoteStyles";
 import { ProposalVote } from "../runtime";
 import { VoteKind } from "@joystream/types/proposals";
 import { VoteKindStr } from "./VotingSection";
-import ProfilePreview from "./ProfilePreview";
+import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";
 
 
 type VotesProps = {

+ 48 - 0
packages/joy-utils/src/MemberByAccountPreview.tsx

@@ -0,0 +1,48 @@
+import React, { useEffect, useState, useContext } from "react";
+
+import { Loader } from 'semantic-ui-react';
+import { ApiContext } from "@polkadot/react-api";
+import ProfilePreview from "./MemberProfilePreview";
+import { AccountId } from "@polkadot/types/interfaces";
+import { memberFromAccount, MemberFromAccount } from "./accounts";
+
+import styled from 'styled-components';
+
+const MemberByAccount = styled.div``;
+
+type Props = {
+  accountId: AccountId | string;
+};
+
+const MemberByAccountPreview: React.FunctionComponent<Props> = ({ accountId }) => {
+  const { api } = useContext(ApiContext);
+  const [ member, setMember ] = useState(null as MemberFromAccount | null);
+  useEffect(
+    () => {
+      let isSubscribed = true;
+      memberFromAccount(api, accountId).then(member => isSubscribed && setMember(member));
+      return () => { isSubscribed = false; };
+    },
+    [accountId]
+  );
+
+  return (
+    <MemberByAccount>
+      { member ?
+        (
+          member.profile ?
+            <ProfilePreview
+              avatar_uri={member.profile.avatar_uri.toString()}
+              root_account={member.profile.root_account.toString()}
+              handle={member.profile.handle.toString()}
+              id={member.id}
+              link={true}/>
+            : 'Member profile not found!'
+        )
+        : <Loader inline active>Fetching member profile...</Loader>
+      }
+    </MemberByAccount>
+  );
+};
+
+export default MemberByAccountPreview;

+ 61 - 0
packages/joy-utils/src/MemberProfilePreview.tsx

@@ -0,0 +1,61 @@
+import React from "react";
+import { Image } from "semantic-ui-react";
+import { IdentityIcon } from "@polkadot/react-components";
+import { Link } from "react-router-dom";
+import styled from 'styled-components';
+
+type ProfileItemProps = {
+  avatar_uri: string;
+  root_account: string;
+  handle: string;
+  link?: boolean;
+  id?: number;
+};
+
+const StyledProfilePreview = styled.div`
+  display: flex;
+  align-items: center;
+  & .ui.avatar.image {
+    margin: 0 !important;
+    width: 40px !important;
+    height: 40px !important;
+  }
+`;
+
+const Details = styled.div``;
+
+const DetailsHandle = styled.h4`
+  margin: 0;
+  margin-left: 1rem;
+  font-weight: bold;
+  color: #333;
+`;
+
+const DetailsID = styled.div`
+  margin: 0;
+  margin-top: 0.25rem;
+  margin-left: 1rem;
+  color: #777;
+`;
+
+export default function ProfilePreview({ id, avatar_uri, root_account, handle, link = false }: ProfileItemProps) {
+  const Preview = (
+    <StyledProfilePreview>
+      {avatar_uri ? (
+        <Image src={avatar_uri} avatar floated="left" />
+      ) : (
+        <IdentityIcon className="image" value={root_account} size={40} />
+      )}
+      <Details>
+        <DetailsHandle>{handle}</DetailsHandle>
+        { id !== undefined && <DetailsID>ID: {id}</DetailsID> }
+      </Details>
+    </StyledProfilePreview>
+  );
+
+  if (link) {
+    return <Link to={ `/members/${ handle }` }>{ Preview }</Link>;
+  }
+
+  return Preview;
+}

+ 67 - 0
packages/joy-utils/src/MembersDropdown.tsx

@@ -0,0 +1,67 @@
+import React, { useEffect, useState, useContext } from "react";
+import { Dropdown, DropdownItemProps, DropdownProps } from "semantic-ui-react";
+import { Profile } from "@joystream/types/members";
+import { memberFromAccount, MemberFromAccount } from "./accounts";
+import { AccountId } from "@polkadot/types/interfaces";
+import { ApiContext } from "@polkadot/react-api";
+import styled from 'styled-components';
+
+const StyledMembersDropdown = styled(Dropdown)`
+  & .ui.avatar.image {
+    width: 2em !important;
+    height: 2em !important;
+  }
+`;
+
+function membersToOptions(members: MemberFromAccount[]) {
+  const validMembers = members.filter(m => m.profile !== undefined) as (MemberFromAccount & { profile: Profile })[];
+  return validMembers
+    .map(({ id, profile, account }) => ({
+      key: profile.handle,
+      text: `${ profile.handle } (id:${ id })`,
+      value: account,
+      image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
+    }));
+}
+
+type Props = {
+  accounts: (string | AccountId)[],
+  name?: DropdownProps["name"],
+  onChange?: DropdownProps["onChange"],
+  value?: DropdownProps["value"],
+  placeholder?: DropdownProps["placeholder"],
+};
+
+const MembersDropdown: React.FunctionComponent<Props> = ({ accounts, ...passedProps }) => {
+  const { api } = useContext(ApiContext);
+  // State
+  const [ loading, setLoading ] = useState(true);
+  const [ membersOptions, setMembersOptions ] = useState([] as DropdownItemProps[]);
+  // Generate members options array on load
+  useEffect(() => {
+    let isSubscribed = true;
+    Promise
+      .all(accounts.map(acc => memberFromAccount(api, acc)))
+      .then(members => {
+        if (isSubscribed) {
+          setMembersOptions(membersToOptions(members));
+          setLoading(false);
+        }
+      });
+    return () => { isSubscribed = false; };
+  }, [accounts]);
+
+  return (
+    <StyledMembersDropdown
+      clearable
+      search
+      fluid
+      selection
+      { ...passedProps }
+      options={membersOptions}
+      loading={loading}
+    />
+  );
+};
+
+export default MembersDropdown;

+ 21 - 0
packages/joy-utils/src/accounts.ts

@@ -9,6 +9,12 @@ import keyring from '@polkadot/ui-keyring';
 import { isHex, u8aToHex } from '@polkadot/util';
 import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
 
+import { ApiPromise } from "@polkadot/api";
+import { MemberId, Profile } from "@joystream/types/members";
+import { Option } from "@polkadot/types";
+import { AccountId } from "@polkadot/types/interfaces";
+import { Vec } from "@polkadot/types/codec";
+
 export type SeedType = 'bip' | 'raw' | 'dev';
 
 export interface AddressState {
@@ -135,3 +141,18 @@ export function createAccount (suri: string, pairType: KeypairType, name: string
 export function isPasswordValid(password: string): boolean {
   return password.length == 0 || keyring.isPassValid(password)
 }
+
+export type MemberFromAccount = { account: string, id: number; profile?: Profile };
+
+export async function memberFromAccount(api: ApiPromise, accountId: AccountId | string): Promise<MemberFromAccount> {
+  const [memberId] =
+    ((await api.query.members.memberIdsByRootAccountId(accountId)) as Vec<MemberId>)
+    .concat((await api.query.members.memberIdsByControllerAccountId(accountId)) as Vec<MemberId>);
+  const member = (await api.query.members.memberProfile(memberId)) as Option<Profile>;
+
+  return {
+    account: accountId.toString(),
+    id: memberId.toNumber(),
+    profile: member.unwrapOr(undefined)
+  };
+}