Show society skeptics & voters (#4879)

* Show society skeptics & voters

* useCounter for candidates

* Sort founder to bottom

* payouts due

* sizing

* remove header
This commit is contained in:
Jaco Greeff 2021-03-20 17:01:48 +01:00 committed by GitHub
parent 96ac7519f1
commit 268d4dd557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 230 additions and 51 deletions

View File

@ -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<Props> {
function Candidates ({ allMembers, candidates, className = '', isMember, ownMembers }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const candidates = useCall<DeriveSocietyCandidate[]>(api.derive.society.candidates);
const headerRef = useRef([
[t('candidates'), 'start'],

View File

@ -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<Props> {
function Candidates ({ allMembers, candidates, className, isMember, ownMembers }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isBidOpen, toggleBidOpen] = useToggle();
@ -36,6 +39,7 @@ function Candidates ({ allMembers, className, isMember, ownMembers }: Props): Re
</Button.Group>
<AllCandidates
allMembers={allMembers}
candidates={candidates}
isMember={isMember}
ownMembers={ownMembers}
/>

View File

@ -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<Props> {
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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const suspended = useCall<bool>(api.query.society.suspendedMembers, [accountId]);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [canInk] = useState(() => api.genesisHash.eq('0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe'));
const [canInk] = useState(() => api.genesisHash.eq(KUSAMA_GENESIS));
const [isInkShowing, toggleInk] = useToggle();
const { isSuspended, payouts } = useCallMulti<MultiState>([
[api.query.society.suspendedMembers, accountId],
[api.query.society.payouts, accountId]
], optMulti);
const renderPayouts = useCallback(
() => bestNumber && payouts.map(([bn, value], index) => (
<p key={index}>
<Columar>
<Columar.Column>
<FormatBalance value={value} />
</Columar.Column>
<Columar.Column>
<div>#{formatNumber(bn)}</div>
<BlockToTime
key={index}
value={bn.sub(bestNumber)}
/>
</Columar.Column>
</Columar>
</p>
)),
[bestNumber, payouts]
);
useEffect((): void => {
if (canvasRef.current) {
@ -60,17 +107,34 @@ function Member ({ className = '', isFounder, isHead, value: { accountId, strike
label={t<string>('society founder')}
/>
)}
{suspended?.isTrue && (
{isSkeptic && (
<Tag
color='yellow'
label={t<string>('skeptic')}
/>
)}
{isVoter && (
<Tag
color='blue'
label={t<string>('voted')}
/>
)}
{isSuspended && (
<Tag
color='red'
label={t<string>('suspended')}
/>
)}
</td>
<td className='all'>&nbsp;</td>
<td className='number'>
{strikes.toString()}
<td className='all number'>
{payouts.length !== 0 && (
<Expander
renderChildren={renderPayouts}
summary={t<string>('Payouts ({{count}})', { replace: { count: formatNumber(payouts.length) } })}
/>
)}
</td>
<td className='number'>{formatNumber(strikes)}</td>
<td>
{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;
}
}
`);

View File

@ -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<Props> {
function Members ({ className = '', mapMembers }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const members = useCall<DeriveSocietyMember[]>(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 (
<Table
className={className}
empty={info && filtered && t<string>('No active members')}
empty={mapMembers && t<string>('No active members')}
header={headerRef.current}
>
{info && filtered && filtered.map((member): React.ReactNode => (
{mapMembers?.map((value): React.ReactNode => (
<Member
isFounder={info.founder?.eq(member.accountId)}
isHead={info.head?.eq(member.accountId)}
key={member.accountId.toString()}
value={member}
bestNumber={bestNumber}
key={value.key}
value={value}
/>
))}
</Table>

View File

@ -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<Props> {
function Overview ({ className, info, isMember, mapMembers, ownMembers }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Summary info={info} />
@ -26,7 +28,7 @@ function Overview ({ className, info, isMember, ownMembers }: Props): React.Reac
isMember={isMember}
ownMembers={ownMembers}
/>
<Members info={info} />
<Members mapMembers={mapMembers} />
</div>
);
}

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const candidateCount = useCounter();
const { allMembers, isMember, ownMembers } = useMembers();
const info = useCall<DeriveSociety>(api.derive.society.info);
const members = useCall<DeriveSocietyMember[]>(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<Props>
<Route path={`${basePath}/candidates`}>
<Candidates
allMembers={allMembers}
candidates={candidates}
isMember={isMember}
ownMembers={ownMembers}
/>
@ -70,6 +114,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
<Overview
info={info}
isMember={isMember}
mapMembers={mapMembers}
ownMembers={ownMembers}
/>
</Route>

View File

@ -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[];
}

View File

@ -7,7 +7,7 @@ import { useApi, useCall } from '@polkadot/react-hooks';
export default function useCounter (): number {
const { api } = useApi();
const bids = useCall<Bid[]>(api.query.society?.bids);
const bids = useCall<Bid[]>(api.query.society?.candidates);
return bids?.length || 0;
}

View File

@ -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<Voters> {
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<DeriveSocietyCandidate[]>(api.derive.society.candidates);
const [state, setState] = useState<Voters>(EMPTY);
useEffect((): void => {
voteTrigger && candidates && (
candidates.length
? getVoters(api, candidates).then(setState).catch(console.error)
: setState(EMPTY)
);
}, [api, candidates, voteTrigger]);
return state;
}