From 2a52609f23022ac9dfa99220ee70427319699f55 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 11 Nov 2019 19:58:13 +0100 Subject: [PATCH] Add staking favorites (top of list) (#1866) --- packages/app-staking/src/Overview/Address.tsx | 35 ++++++++-- .../app-staking/src/Overview/CurrentList.tsx | 64 ++++++++++++++----- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/packages/app-staking/src/Overview/Address.tsx b/packages/app-staking/src/Overview/Address.tsx index bff78b9460..cc7d134bc7 100644 --- a/packages/app-staking/src/Overview/Address.tsx +++ b/packages/app-staking/src/Overview/Address.tsx @@ -24,8 +24,10 @@ interface Props extends I18nProps { defaultName: string; filter: ValidatorFilter; isElected: boolean; + isFavorite: boolean; lastAuthors?: string[]; myAccounts: string[]; + onFavorite: (accountId: string) => void; points?: Points; recentlyOnline?: DerivedHeartbeats; stakingInfo?: DerivedStaking; @@ -44,7 +46,7 @@ interface StakingState { const WITH_VALIDATOR_PREFS = { validatorPayment: true }; -function Address ({ address, authorsMap, className, defaultName, filter, isElected, lastAuthors, myAccounts, points, recentlyOnline, stakingInfo, t, withNominations = true }: Props): React.ReactElement | null { +function Address ({ address, authorsMap, className, defaultName, filter, isElected, isFavorite, lastAuthors, myAccounts, onFavorite, points, recentlyOnline, stakingInfo, t, withNominations = true }: Props): React.ReactElement | null { const { isSubstrateV2 } = useContext(ApiContext); const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>(); const [hasActivity, setHasActivity] = useState(true); @@ -120,13 +122,23 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect const lastBlockNumber = authorsMap[stashId]; const isAuthor = lastAuthors && lastAuthors.includes(stashId); + const _onFavorite = (): void => onFavorite(stashId); return ( - {lastBlockNumber && ( -
#{lastBlockNumber}
+ {isSubstrateV2 && ( +
+ {lastBlockNumber && ( +
#{lastBlockNumber}
+ )} + +
)} {controllerId && (
@@ -200,6 +212,22 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect export default withMulti( styled(Address)` + .extras { + display: inline-block; + margin-bottom: 0.75rem; + + .favorite { + cursor: pointer; + display: inline-block; + margin-left: 0.5rem; + margin-right: -0.25rem; + + &.isSelected { + color: goldenrod; + } + } + } + .blockNumberV1, .blockNumberV2 { border-radius: 0.25rem; @@ -220,7 +248,6 @@ export default withMulti( .blockNumberV2 { display: inline-block; - margin-bottom: 0.75rem; padding: 0.25rem 0; &.isCurrent { diff --git a/packages/app-staking/src/Overview/CurrentList.tsx b/packages/app-staking/src/Overview/CurrentList.tsx index 1836ae9fbd..10bfab9609 100644 --- a/packages/app-staking/src/Overview/CurrentList.tsx +++ b/packages/app-staking/src/Overview/CurrentList.tsx @@ -2,14 +2,15 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AccountId } from '@polkadot/types/interfaces'; import { DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types'; import { I18nProps } from '@polkadot/react-components/types'; +import { AccountId } from '@polkadot/types/interfaces'; import { ValidatorFilter } from '../types'; import React, { useContext, useEffect, useState } from 'react'; import { ApiContext } from '@polkadot/react-api'; import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/react-components'; +import store from 'store'; import keyring from '@polkadot/ui-keyring'; import translate from '../translate'; @@ -23,17 +24,21 @@ interface Props extends I18nProps { stakingOverview?: DerivedStakingOverview; } -function renderColumn (myAccounts: string[], addresses: AccountId[] | string[], defaultName: string, withOnline: boolean, filter: string, { authorsMap, lastAuthors, recentlyOnline, stakingOverview }: Props, pointIndexes?: number[]): React.ReactNode { - return (addresses as AccountId[]).map((address, index): React.ReactNode => ( +const STORE_FAVS = 'staking:favorites'; + +function renderColumn (myAccounts: string[], favorites: string[], addresses: string[], defaultName: string, withOnline: boolean, filter: string, onFavorite: (accountId: string) => void, { authorsMap, lastAuthors, recentlyOnline, stakingOverview }: Props, pointIndexes?: number[]): React.ReactNode { + return addresses.map((address, index): React.ReactNode => (
accountId.eq(address))} + isFavorite={favorites.includes(address)} lastAuthors={lastAuthors} - key={address.toString()} + key={address} myAccounts={myAccounts} + onFavorite={onFavorite} points={ stakingOverview && pointIndexes && pointIndexes[index] !== -1 ? stakingOverview.eraPoints.individual[pointIndexes[index]] @@ -48,28 +53,55 @@ function renderColumn (myAccounts: string[], addresses: AccountId[] | string[], )); } -function filterAccounts (list: string[] = [], without: AccountId[] | string[]): string[] { - return list.filter((accountId): boolean => !without.includes(accountId as any)); +function filterAccounts (list: string[] = [], without: string[], favorites: string[]): string[] { + return list + .filter((accountId): boolean => !without.includes(accountId as any)) + .sort((a, b): number => { + const aIdx = favorites.includes(a); + const bIdx = favorites.includes(b); + + return aIdx === bIdx + ? 0 + : aIdx + ? -1 + : 1; + }); +} + +function accountsToString (accounts: AccountId[]): string[] { + return accounts.map((accountId): string => accountId.toString()); } function CurrentList (props: Props): React.ReactElement { const { isSubstrateV2 } = useContext(ApiContext); + const [favorites, setFavorites] = useState(store.get(STORE_FAVS, [])); const [filter, setFilter] = useState('all'); const [myAccounts] = useState(keyring.getAccounts().map(({ address }): string => address)); - const [{ electedFiltered, nextFiltered, pointIndexes }, setFiltered] = useState<{ electedFiltered: string[]; nextFiltered: string[]; pointIndexes: number[] }>({ electedFiltered: [], nextFiltered: [], pointIndexes: [] }); + const [{ elected, pointIndexes, validators, waiting }, setFiltered] = useState<{ elected: string[]; pointIndexes: number[]; validators: string[]; waiting: string[] }>({ elected: [], pointIndexes: [], validators: [], waiting: [] }); const { next, stakingOverview, t } = props; useEffect((): void => { if (stakingOverview) { - const elected = stakingOverview.currentElected.map((accountId): string => accountId.toString()); + const validators = filterAccounts(accountsToString(stakingOverview.validators), [], favorites); + const elected = isSubstrateV2 ? filterAccounts(accountsToString(stakingOverview.currentElected), validators, favorites) : []; setFiltered({ - electedFiltered: isSubstrateV2 ? filterAccounts(elected, stakingOverview.validators) : [], - nextFiltered: filterAccounts(next, elected), - pointIndexes: stakingOverview.validators.map((validator): number => elected.indexOf(validator.toString())) + elected, + pointIndexes: validators.map((validator): number => stakingOverview.currentElected.indexOf(validator as any)), + validators, + waiting: filterAccounts(next, elected, favorites) }); } - }, [next, stakingOverview]); + }, [favorites, next, stakingOverview]); + + const _onFavorite = (accountId: string): void => { + const newFavs = favorites.includes(accountId) + ? favorites.filter((thisOne): boolean => thisOne !== accountId) + : [...favorites, accountId]; + + store.set(STORE_FAVS, newFavs); + setFavorites(newFavs); + }; return (
@@ -94,16 +126,16 @@ function CurrentList (props: Props): React.ReactElement { emptyText={t('No addresses found')} headerText={t('validators')} > - {stakingOverview && renderColumn(myAccounts, stakingOverview.validators, t('validator'), true, filter, props, pointIndexes)} + {stakingOverview && renderColumn(myAccounts, favorites, validators, t('validator'), true, filter, _onFavorite, props, pointIndexes)} - {(electedFiltered.length !== 0 || nextFiltered.length !== 0) && ( + {(elected.length !== 0 || waiting.length !== 0) && ( <> - {renderColumn(myAccounts, electedFiltered, t('intention'), false, filter, props)} - {renderColumn(myAccounts, nextFiltered, t('intention'), false, filter, props)} + {renderColumn(myAccounts, favorites, elected, t('intention'), false, filter, _onFavorite, props)} + {renderColumn(myAccounts, favorites, waiting, t('intention'), false, filter, _onFavorite, props)} )}