Rework votes display (#3489)

This commit is contained in:
Jaco Greeff 2020-08-23 18:57:57 +02:00 committed by GitHub
parent 8faaa6df4a
commit 71ab4c3d04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 176 deletions

View File

@ -13,7 +13,7 @@ import { BlockToTime } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import Close from './Close';
import Voters from './Votes';
import Voters from './Voters';
import Voting from './Voting';
interface Props {
@ -69,17 +69,19 @@ function Motion ({ className = '', isMember, members, motion: { hash, proposal,
</>
)}
</td>
<Voters
isAye
members={members}
threshold={threshold}
votes={ayes}
/>
<Voters
members={members}
threshold={threshold}
votes={nays}
/>
<td className='expand'>
<Voters
isAye
members={members}
threshold={threshold}
votes={ayes}
/>
<Voters
members={members}
threshold={threshold}
votes={nays}
/>
</td>
<td className='button'>
{isVoteable && !isCloseable && (
<Voting

View File

@ -0,0 +1,59 @@
// Copyright 2017-2020 @polkadot/app-democracy authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, MemberCount } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import { AddressMini, Expander } from '@polkadot/react-components';
import { useTranslation } from '../translate';
interface Props {
isAye?: boolean;
members: string[];
votes: AccountId[];
threshold: MemberCount;
}
function Voters ({ isAye, members, threshold, votes }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const counter = useMemo(
(): string => {
const num = threshold.toNumber();
const max = isAye
? num
: members?.length
? (members.length - num) + 1
: 0;
return `${votes.length}${max ? `/${max}` : ''}`;
},
[isAye, members, threshold, votes]
);
if (!counter || !votes.length) {
return null;
}
return (
<Expander
summary={
isAye
? t<string>('Aye {{counter}}', { replace: { counter } })
: t<string>('Nay {{counter}}', { replace: { counter } })
}
>
{votes.map((address): React.ReactNode => (
<AddressMini
key={address.toString()}
value={address}
withBalance={false}
/>
))}
</Expander>
);
}
export default React.memo(Voters);

View File

@ -1,57 +0,0 @@
// Copyright 2017-2020 @polkadot/app-democracy authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, MemberCount } from '@polkadot/types/interfaces';
import React, { useEffect, useState } from 'react';
import { AddressMini, Expander } from '@polkadot/react-components';
import { useTranslation } from '../translate';
interface Props {
isAye?: boolean;
members: string[];
votes: AccountId[];
threshold: MemberCount;
}
function Votes ({ isAye, members, threshold, votes }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [counter, setCounter] = useState('');
useEffect((): void => {
const num = threshold.toNumber();
const max = isAye
? num
: members?.length
? (members.length - num) + 1
: 0;
setCounter(`${votes.length}${max ? `/${max}` : ''}`);
}, [isAye, members, threshold, votes]);
return (
<td className='address'>
{counter && votes.length !== 0 && (
<Expander
summary={
isAye
? t<string>('Aye {{counter}}', { replace: { counter } })
: t<string>('Nay {{counter}}', { replace: { counter } })
}
>
{votes.map((address): React.ReactNode => (
<AddressMini
key={address.toString()}
value={address}
withBalance={false}
/>
))}
</Expander>
)}
</td>
);
}
export default React.memo(Votes);

View File

@ -29,8 +29,7 @@ function Proposals ({ className = '', motions, prime }: Props): React.ReactEleme
[t('motions'), 'start', 2],
[t('threshold')],
[t('voting end')],
[undefined, 'address'],
[undefined, 'address'],
[t('votes'), 'start'],
[],
[undefined, 'badge'],
[undefined, 'mini']

View File

@ -6,7 +6,7 @@ import { DeriveReferendumExt } from '@polkadot/api-derive/types';
import { Balance, BlockNumber } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { Badge, Button, Icon, LinkExternal } from '@polkadot/react-components';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
@ -42,33 +42,40 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
const { allAccounts } = useAccounts();
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber);
const totalIssuance = useCall<Balance>(api.query.balances.totalIssuance);
const [percentages, setPercentages] = useState<Percentages | null>(null);
const [{ hasVoted, hasVotedAye }, setHasVoted] = useState<VoteType>({ hasVoted: false, hasVotedAye: false });
const { changeAye, changeNay } = useChangeCalc(status.threshold, votedAye, votedNay, votedTotal);
const threshold = useMemo(
() => status.threshold.type.toString().replace('majority', ' majority '),
[status]
);
useEffect((): void => {
if (totalIssuance) {
const aye = allAye.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
const nay = allNay.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
const hasVotedAye = allAye.some(({ accountId }) => allAccounts.includes(accountId.toString()));
const hasVoted = hasVotedAye || allNay.some(({ accountId }) => allAccounts.includes(accountId.toString()));
const [percentages, { hasVoted, hasVotedAye }] = useMemo(
(): [Percentages | null, VoteType] => {
if (totalIssuance) {
const aye = allAye.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
const nay = allNay.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
const hasVotedAye = allAye.some(({ accountId }) => allAccounts.includes(accountId.toString()));
setPercentages({
aye: votedTotal.isZero()
? ''
: `${(aye.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
nay: votedTotal.isZero()
? ''
: `${(nay.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
turnout: `${((votedTotal.muln(10000).div(totalIssuance).toNumber()) / 100).toFixed(2)}%`
});
setHasVoted({ hasVoted, hasVotedAye });
}
}, [allAccounts, allAye, allNay, totalIssuance, votedTotal]);
return [
{
aye: votedTotal.isZero()
? ''
: `${(aye.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
nay: votedTotal.isZero()
? ''
: `${(nay.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
turnout: `${((votedTotal.muln(10000).div(totalIssuance).toNumber()) / 100).toFixed(2)}%`
},
{
hasVoted: hasVotedAye || allNay.some(({ accountId }) => allAccounts.includes(accountId.toString())),
hasVotedAye
}
];
} else {
return [null, { hasVoted: false, hasVotedAye: false }];
}
},
[allAccounts, allAye, allNay, totalIssuance, votedTotal]
);
if (!bestNumber || status.end.sub(bestNumber).lten(0)) {
return null;
@ -102,22 +109,6 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
</>
)}
</td>
<ReferendumVotes
change={changeAye}
count={voteCountAye}
index={index}
isWinning={isPassing}
total={votedAye}
votes={allAye}
/>
<ReferendumVotes
change={changeNay}
count={voteCountNay}
index={index}
isWinning={!isPassing}
total={votedNay}
votes={allNay}
/>
<td className='badge'>
{isBoolean(isPassing) && (
<Badge
@ -131,6 +122,26 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
/>
)}
</td>
<td className='expand'>
<ReferendumVotes
change={changeAye}
count={voteCountAye}
index={index}
isAye
isWinning={isPassing}
total={votedAye}
votes={allAye}
/>
<ReferendumVotes
change={changeNay}
count={voteCountNay}
index={index}
isAye={false}
isWinning={!isPassing}
total={votedNay}
votes={allNay}
/>
</td>
<td className='button'>
<Button.Group>
{!image?.proposal && (

View File

@ -5,8 +5,7 @@
import { DeriveReferendumVote } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import React, { useMemo, useState } from 'react';
import { Expander, Icon, Tooltip } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
@ -18,6 +17,7 @@ interface Props {
change: BN;
className?: string;
count: number;
isAye: boolean;
isWinning: boolean;
index: BN;
total: BN;
@ -28,75 +28,67 @@ const LOCKS = [1, 10, 20, 30, 40, 50, 60];
let id = 0;
function ReferendumVotes ({ change, className = '', count, index, isWinning, total, votes }: Props): React.ReactElement<Props> {
function ReferendumVotes ({ change, className, count, index, isAye, isWinning, total, votes }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [trigger] = useState(`votes-${index.toString()}-${++id}`);
const [sorted, setSorted] = useState<DeriveReferendumVote[]>([]);
useEffect((): void => {
setSorted(
votes.sort((a, b) => {
const ta = a.balance.muln(LOCKS[a.vote.conviction.toNumber()]).divn(10);
const tb = b.balance.muln(LOCKS[b.vote.conviction.toNumber()]).divn(10);
const sorted = useMemo(
() => votes.sort((a, b) => {
const ta = a.balance.muln(LOCKS[a.vote.conviction.toNumber()]).divn(10);
const tb = b.balance.muln(LOCKS[b.vote.conviction.toNumber()]).divn(10);
return tb.cmp(ta);
})
);
}, [votes]);
return tb.cmp(ta);
}),
[votes]
);
if (!count) {
return null;
}
return (
<td className={`${className} number`}>
<Expander
summary={
<FormatBalance
labelPost={count ? ` (${formatNumber(count)})` : '' }
value={total}
/>
}
summarySub={
change.gtn(0)
? (
<>
<Icon
className='double-icon'
icon={isWinning ? 'arrow-circle-down' : 'arrow-circle-up'}
tooltip={trigger}
/>
<FormatBalance value={change} />
<Tooltip
text={
isWinning
? t<string>('The amount this total can be reduced by to change the referendum outcome. This assumes changes to the convictions of the existing votes, with no additional turnout.')
: t<string>('The amount this total should be increased by to change the referendum outcome. This assumes additional turnout with new votes at 1x conviction.')
}
trigger={trigger}
/>
</>
)
: ''
}
>
{sorted.map((vote) =>
<ReferendumVote
key={vote.accountId.toString()}
vote={vote}
/>
)}
</Expander>
</td>
<Expander
className={className}
summary={
<>
{isAye
? t<string>('Aye {{count}}', { replace: { count: ` (${formatNumber(count)})` } })
: t<string>('Nay {{count}}', { replace: { count: ` (${formatNumber(count)})` } })
}
<div><FormatBalance value={total} /></div>
</>
}
summarySub={
<div>
{change.gtn(0) && (
<div>
<Icon
className='double-icon'
icon={isWinning ? 'arrow-circle-down' : 'arrow-circle-up'}
tooltip={trigger}
/>
<FormatBalance value={change} />
<Tooltip
text={
isWinning
? t<string>('The amount this total can be reduced by to change the referendum outcome. This assumes changes to the convictions of the existing votes, with no additional turnout.')
: t<string>('The amount this total should be increased by to change the referendum outcome. This assumes additional turnout with new votes at 1x conviction.')
}
trigger={trigger}
/>
</div>
)}
</div>
}
>
{sorted.map((vote) =>
<ReferendumVote
key={vote.accountId.toString()}
vote={vote}
/>
)}
</Expander>
);
}
export default React.memo(styled(ReferendumVotes)`
.ui--Expander .ui--Expander-summary .double-icon {
margin-bottom: -0.125rem;
margin-right: 0.375rem;
margin-top: 0.125rem;
}
.ui--Expander-summary {
.ui--Icon+.ui--Icon {
margin-left: -0.375rem;
}
}
`);
export default React.memo(ReferendumVotes);

View File

@ -23,10 +23,9 @@ function Referendums ({ className = '', referendums }: Props): React.ReactElemen
[t('remaining'), 'media--1200'],
[t('activate'), 'media--1400'],
[t('turnout'), 'media--1400'],
[t('aye')],
[t('nay')],
[undefined, undefined, 2],
[undefined, 'badge'],
[t('votes'), 'start'],
[undefined, undefined, 2],
[undefined, 'mini media--1000']
]);

View File

@ -84,6 +84,14 @@ export default React.memo(styled(Body)`
border-top-width: 0;
}
&.expand {
text-align: left;
.ui--Expander+.ui--Expander {
margin-top: 0.5rem;
}
}
&.hash {
font-family: monospace;
}