diff --git a/packages/page-society/src/Candidates/Candidates.tsx b/packages/page-society/src/Candidates/Candidates.tsx index 1bffad6a75..d7bfde78f4 100644 --- a/packages/page-society/src/Candidates/Candidates.tsx +++ b/packages/page-society/src/Candidates/Candidates.tsx @@ -6,22 +6,20 @@ import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types'; import React, { useRef } from 'react'; import { Table } from '@polkadot/react-components'; -import { useApi, useCall } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; import Candidate from './Candidate'; interface Props { allMembers: string[]; + candidates?: DeriveSocietyCandidate[]; className?: string; isMember: boolean; ownMembers: string[]; } -function Candidates ({ allMembers, className = '', isMember, ownMembers }: Props): React.ReactElement { +function Candidates ({ allMembers, candidates, className = '', isMember, ownMembers }: Props): React.ReactElement { const { t } = useTranslation(); - const { api } = useApi(); - const candidates = useCall(api.derive.society.candidates); const headerRef = useRef([ [t('candidates'), 'start'], diff --git a/packages/page-society/src/Candidates/index.tsx b/packages/page-society/src/Candidates/index.tsx index c62b6bbe75..65e6e8bed5 100644 --- a/packages/page-society/src/Candidates/index.tsx +++ b/packages/page-society/src/Candidates/index.tsx @@ -1,6 +1,8 @@ // Copyright 2017-2021 @polkadot/app-society authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types'; + import React from 'react'; import { Button } from '@polkadot/react-components'; @@ -13,12 +15,13 @@ import AllCandidates from './Candidates'; interface Props { allMembers: string[]; + candidates?: DeriveSocietyCandidate[]; className?: string; isMember: boolean; ownMembers: string[]; } -function Candidates ({ allMembers, className, isMember, ownMembers }: Props): React.ReactElement { +function Candidates ({ allMembers, candidates, className, isMember, ownMembers }: Props): React.ReactElement { const { t } = useTranslation(); const [isBidOpen, toggleBidOpen] = useToggle(); @@ -36,6 +39,7 @@ function Candidates ({ allMembers, className, isMember, ownMembers }: Props): Re diff --git a/packages/page-society/src/Overview/Member.tsx b/packages/page-society/src/Overview/Member.tsx index 300abc4843..825a4076f1 100644 --- a/packages/page-society/src/Overview/Member.tsx +++ b/packages/page-society/src/Overview/Member.tsx @@ -1,22 +1,35 @@ // Copyright 2017-2021 @polkadot/app-society authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DeriveSocietyMember } from '@polkadot/api-derive/types'; +import type BN from 'bn.js'; import type { bool } from '@polkadot/types'; +import type { BalanceOf, BlockNumber } from '@polkadot/types/interfaces'; +import type { MapMember } from '../types'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; -import { AddressSmall, Icon, Modal, Tag } from '@polkadot/react-components'; -import { useApi, useCall, useToggle } from '@polkadot/react-hooks'; +import { KUSAMA_GENESIS } from '@polkadot/apps-config'; +import { AddressSmall, Columar, Expander, Icon, Modal, Tag } from '@polkadot/react-components'; +import { useApi, useCallMulti, useToggle } from '@polkadot/react-hooks'; +import { BlockToTime, FormatBalance } from '@polkadot/react-query'; +import { formatNumber } from '@polkadot/util'; import drawCanary from '../draw/canary'; import { useTranslation } from '../translate'; interface Props { + bestNumber?: BN; className?: string; isFounder?: boolean; isHead?: boolean; - value: DeriveSocietyMember; + isSkeptic?: boolean; + value: MapMember; +} + +interface MultiState { + isSuspended: boolean; + payouts: [BlockNumber, BalanceOf][]; } const CANVAS_STYLE = { @@ -24,13 +37,47 @@ const CANVAS_STYLE = { margin: '0 auto' }; -function Member ({ className = '', isFounder, isHead, value: { accountId, strikes } }: Props): React.ReactElement { +const optMulti = { + defaultValue: { + isSuspended: false, + payouts: [] as [BlockNumber, BalanceOf][] + }, + transform: ([suspended, payouts]: [bool, [BlockNumber, BalanceOf][]]): MultiState => ({ + isSuspended: suspended.isTrue, + payouts + }) +}; + +function Member ({ bestNumber, className = '', value: { isFounder, isHead, isSkeptic, isVoter, member: { accountId, strikes } } }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const suspended = useCall(api.query.society.suspendedMembers, [accountId]); const canvasRef = useRef(null); - const [canInk] = useState(() => api.genesisHash.eq('0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe')); + const [canInk] = useState(() => api.genesisHash.eq(KUSAMA_GENESIS)); const [isInkShowing, toggleInk] = useToggle(); + const { isSuspended, payouts } = useCallMulti([ + [api.query.society.suspendedMembers, accountId], + [api.query.society.payouts, accountId] + ], optMulti); + + const renderPayouts = useCallback( + () => bestNumber && payouts.map(([bn, value], index) => ( +

+ + + + + +

#{formatNumber(bn)}
+ + + +

+ )), + [bestNumber, payouts] + ); useEffect((): void => { if (canvasRef.current) { @@ -60,17 +107,34 @@ function Member ({ className = '', isFounder, isHead, value: { accountId, strike label={t('society founder')} /> )} - {suspended?.isTrue && ( + {isSkeptic && ( ('skeptic')} + /> + )} + {isVoter && ( + ('voted')} + /> + )} + {isSuspended && ( + ('suspended')} /> )} -   - - {strikes.toString()} + + {payouts.length !== 0 && ( + ('Payouts ({{count}})', { replace: { count: formatNumber(payouts.length) } })} + /> + )} + {formatNumber(strikes)} {canInk && ( <> @@ -104,4 +168,16 @@ function Member ({ className = '', isFounder, isHead, value: { accountId, strike ); } -export default React.memo(Member); +export default React.memo(styled(Member)` + .ui--Column { + &:first-child { + max-width: 100% !important; + } + + &:last-child { + max-width: 13ch; + min-width: 13ch; + white-space: nowrap; + } + } +`); diff --git a/packages/page-society/src/Overview/Members.tsx b/packages/page-society/src/Overview/Members.tsx index f6bdb6783f..b1dea8f0a2 100644 --- a/packages/page-society/src/Overview/Members.tsx +++ b/packages/page-society/src/Overview/Members.tsx @@ -1,59 +1,43 @@ // Copyright 2017-2021 @polkadot/app-society authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types'; +import type { MapMember } from '../types'; -import React, { useMemo, useRef } from 'react'; +import React, { useRef } from 'react'; import { Table } from '@polkadot/react-components'; -import { useApi, useCall } from '@polkadot/react-hooks'; +import { useBestNumber } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; import Member from './Member'; interface Props { className?: string; - info?: DeriveSociety; + mapMembers?: MapMember[]; } -function Members ({ className = '', info }: Props): React.ReactElement { +function Members ({ className = '', mapMembers }: Props): React.ReactElement { const { t } = useTranslation(); - const { api } = useApi(); - const members = useCall(api.derive.society.members); + const bestNumber = useBestNumber(); const headerRef = useRef([ - [t('members'), 'start', 3], + [t('members'), 'start', 2], + [], [t('strikes')], [] ]); - const filtered = useMemo( - () => members && info - ? members - .filter((member) => !info.hasDefender || !member.accountId.eq(info.defender)) - .sort((a, b) => - !info.head?.eq(a.accountId) - ? 1 - : !info.head?.eq(b.accountId) - ? -1 - : 0 - ) - : undefined, - [info, members] - ); - return ( ('No active members')} + empty={mapMembers && t('No active members')} header={headerRef.current} > - {info && filtered && filtered.map((member): React.ReactNode => ( + {mapMembers?.map((value): React.ReactNode => ( ))}
diff --git a/packages/page-society/src/Overview/index.tsx b/packages/page-society/src/Overview/index.tsx index 2afb735eca..97f72abd3b 100644 --- a/packages/page-society/src/Overview/index.tsx +++ b/packages/page-society/src/Overview/index.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveSociety } from '@polkadot/api-derive/types'; +import type { MapMember } from '../types'; import React from 'react'; import styled from 'styled-components'; @@ -14,10 +15,11 @@ interface Props { className?: string; info?: DeriveSociety; isMember: boolean; + mapMembers?: MapMember[]; ownMembers: string[]; } -function Overview ({ className, info, isMember, ownMembers }: Props): React.ReactElement { +function Overview ({ className, info, isMember, mapMembers, ownMembers }: Props): React.ReactElement { return (
@@ -26,7 +28,7 @@ function Overview ({ className, info, isMember, ownMembers }: Props): React.Reac isMember={isMember} ownMembers={ownMembers} /> - +
); } diff --git a/packages/page-society/src/index.tsx b/packages/page-society/src/index.tsx index 1e61fb7161..0bfee257da 100644 --- a/packages/page-society/src/index.tsx +++ b/packages/page-society/src/index.tsx @@ -1,7 +1,8 @@ // Copyright 2017-2021 @polkadot/app-society authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DeriveSociety } from '@polkadot/api-derive/types'; +import type { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types'; +import type { MapMember } from './types'; import React, { useMemo } from 'react'; import { Route, Switch } from 'react-router'; @@ -15,6 +16,7 @@ import Suspended from './Suspended'; import { useTranslation } from './translate'; import useCounter from './useCounter'; import useMembers from './useMembers'; +import useVoters from './useVoters'; interface Props { basePath: string; @@ -23,12 +25,53 @@ interface Props { export { useCounter }; +function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], voters: string[], info: DeriveSociety): MapMember[] { + return members + .filter((member) => !info.hasDefender || !member.accountId.eq(info.defender)) + .map((member): MapMember => ({ + isFounder: info.founder?.eq(member.accountId), + isHead: info.head?.eq(member.accountId), + isSkeptic: skeptics.includes(member.accountId.toString()), + isVoter: voters.includes(member.accountId.toString()), + key: member.accountId.toString(), + member + })) + .sort((a, b) => + a.isHead + ? -1 + : b.isHead + ? 1 + : a.isSkeptic + ? -1 + : b.isSkeptic + ? 1 + : a.isVoter + ? -1 + : b.isVoter + ? 1 + : a.isFounder + ? 1 + : b.isFounder + ? -1 + : 0 + ); +} + function SocietyApp ({ basePath, className }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const candidateCount = useCounter(); const { allMembers, isMember, ownMembers } = useMembers(); const info = useCall(api.derive.society.info); + const members = useCall(api.derive.society.members); + const { candidates, skeptics, voters } = useVoters(); + + const mapMembers = useMemo( + () => members && info + ? getMapMembers(members, skeptics, voters, info) + : undefined, + [info, members, skeptics, voters] + ); const items = useMemo(() => [ { @@ -59,6 +102,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement @@ -70,6 +114,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement diff --git a/packages/page-society/src/types.ts b/packages/page-society/src/types.ts index dd036640dc..45a9438cf3 100644 --- a/packages/page-society/src/types.ts +++ b/packages/page-society/src/types.ts @@ -1,8 +1,18 @@ // Copyright 2017-2021 @polkadot/app-society authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { DeriveSocietyCandidate, DeriveSocietyMember } from '@polkadot/api-derive/types'; import type { SocietyVote } from '@polkadot/types/interfaces'; +export interface MapMember { + isFounder?: boolean; + isHead?: boolean; + isSkeptic?: boolean; + isVoter?: boolean; + key: string; + member: DeriveSocietyMember; +} + export interface OwnMembers { allMembers: string[]; isMember: boolean; @@ -16,3 +26,9 @@ export interface VoteSplit { allNay: VoteType[]; allSkeptic: VoteType[]; } + +export interface Voters { + candidates: DeriveSocietyCandidate[]; + skeptics: string[]; + voters: string[]; +} diff --git a/packages/page-society/src/useCounter.ts b/packages/page-society/src/useCounter.ts index 57df850e5c..1511185995 100644 --- a/packages/page-society/src/useCounter.ts +++ b/packages/page-society/src/useCounter.ts @@ -7,7 +7,7 @@ import { useApi, useCall } from '@polkadot/react-hooks'; export default function useCounter (): number { const { api } = useApi(); - const bids = useCall(api.query.society?.bids); + const bids = useCall(api.query.society?.candidates); return bids?.length || 0; } diff --git a/packages/page-society/src/useVoters.ts b/packages/page-society/src/useVoters.ts new file mode 100644 index 0000000000..1890581ffb --- /dev/null +++ b/packages/page-society/src/useVoters.ts @@ -0,0 +1,54 @@ +// Copyright 2017-2021 @polkadot/app-society authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types'; +import type { Voters } from './types'; + +import { useEffect, useState } from 'react'; + +import { useApi, useCall, useEventTrigger } from '@polkadot/react-hooks'; + +const EMPTY: Voters = { candidates: [], skeptics: [], voters: [] }; + +async function getVoters (api: ApiPromise, candidates: DeriveSocietyCandidate[]): Promise { + const entries = await Promise.all( + candidates.map(({ accountId }) => api.query.society.votes.entries(accountId)) + ); + const skeptics: string[] = []; + const voters: string[] = []; + + entries.forEach((list): void => { + list.forEach(([{ args: [, accountId] }, opt]) => { + if (opt.isSome) { + const key = accountId.toString(); + const vote = opt.unwrap(); + + if (vote.isSkeptic) { + !skeptics.includes(key) && skeptics.push(key); + } else { + !voters.includes(key) && voters.push(key); + } + } + }); + }); + + return { candidates, skeptics, voters }; +} + +export default function useVoters (): Voters { + const { api } = useApi(); + const voteTrigger = useEventTrigger([api.events.society.Vote]); + const candidates = useCall(api.derive.society.candidates); + const [state, setState] = useState(EMPTY); + + useEffect((): void => { + voteTrigger && candidates && ( + candidates.length + ? getVoters(api, candidates).then(setState).catch(console.error) + : setState(EMPTY) + ); + }, [api, candidates, voteTrigger]); + + return state; +}