diff --git a/packages/page-accounts/src/Accounts/index.tsx b/packages/page-accounts/src/Accounts/index.tsx index 48d2187e42..fc16410404 100644 --- a/packages/page-accounts/src/Accounts/index.tsx +++ b/packages/page-accounts/src/Accounts/index.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { Button, FilterInput, SortDropdown, SummaryBox, Table } from '@polkadot/react-components'; import { getAccountCryptoType } from '@polkadot/react-components/util'; -import { useAccounts, useApi, useDelegations, useFavorites, useIpfs, useLedger, useLoadingDelay, useProxies, useToggle } from '@polkadot/react-hooks'; +import { useAccounts, useApi, useDelegations, useFavorites, useIpfs, useLedger, useNextTick, useProxies, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { settings } from '@polkadot/ui-settings'; import { BN_ZERO, isFunction } from '@polkadot/util'; @@ -107,7 +107,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen const [{ sortBy, sortFromMax }, setSortBy] = useState(DEFAULT_SORT_CONTROLS); const delegations = useDelegations(); const proxies = useProxies(); - const isLoading = useLoadingDelay(); + const isNextTick = useNextTick(); const onSortChange = useCallback( (sortBy: SortCategory) => setSortBy(({ sortFromMax }) => ({ sortBy, sortFromMax })), @@ -374,10 +374,10 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen /> - {isLoading || !sortedAccounts.length + {!isNextTick || !sortedAccounts.length ? ( ("You don't have any accounts. Some features are currently hidden and will only become available once you have accounts.")} + empty={isNextTick && sortedAccounts && t("You don't have any accounts. Some features are currently hidden and will only become available once you have accounts.")} header={header.accounts} /> ) diff --git a/packages/page-addresses/src/Contacts/index.tsx b/packages/page-addresses/src/Contacts/index.tsx index 44cf2a30c1..2acbc684c4 100644 --- a/packages/page-addresses/src/Contacts/index.tsx +++ b/packages/page-addresses/src/Contacts/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { Button, FilterInput, SummaryBox, Table } from '@polkadot/react-components'; -import { useAddresses, useFavorites, useLoadingDelay, useToggle } from '@polkadot/react-hooks'; +import { useAddresses, useFavorites, useNextTick, useToggle } from '@polkadot/react-hooks'; import CreateModal from '../modals/Create'; import { useTranslation } from '../translate'; @@ -29,7 +29,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen const [favorites, toggleFavorite] = useFavorites(STORE_FAVS); const [sortedAddresses, setSortedAddresses] = useState(); const [filterOn, setFilter] = useState(''); - const isLoading = useLoadingDelay(); + const isNextTick = useNextTick(); const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([ [t('contacts'), 'start', 4] @@ -75,11 +75,11 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
('no addresses saved yet, add any existing address')} + empty={isNextTick && sortedAddresses && t('no addresses saved yet, add any existing address')} header={headerRef.current} isSplit > - {!isLoading && sortedAddresses?.map(({ address, isFavorite }): React.ReactNode => ( + {isNextTick && sortedAddresses?.map(({ address, isFavorite }): React.ReactNode => (
(''); - - // we have a very large list, so we use a loading delay - const isLoading = useLoadingDelay(); + const isNextTick = useNextTick(); const { validators, waiting } = useMemo( () => getFiltered(isOwn, stakingOverview, favorites, targets.waitingIds, ownStashIds), @@ -123,12 +121,12 @@ function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, m ); const list = useMemo( - () => isLoading - ? undefined - : isIntentions + () => isNextTick + ? isIntentions ? nominatedBy && waiting - : validators, - [isIntentions, isLoading, nominatedBy, validators, waiting] + : validators + : undefined, + [isIntentions, isNextTick, nominatedBy, validators, waiting] ); const infoMap = useMemo( diff --git a/packages/page-staking2/src/Validators/ListActive.tsx b/packages/page-staking2/src/Validators/ListActive.tsx new file mode 100644 index 0000000000..68f1f1381e --- /dev/null +++ b/packages/page-staking2/src/Validators/ListActive.tsx @@ -0,0 +1,57 @@ +// Copyright 2017-2023 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SessionInfo, Validator } from '../types'; +import type { UsePoints } from './types'; + +import React, { useRef } from 'react'; + +import Legend from '@polkadot/app-staking/Legend'; +import { Table } from '@polkadot/react-components'; +import { useNextTick } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate'; +import ValidatorRow from './Validator'; + +interface Props { + className?: string; + isRelay: boolean; + points?: UsePoints; + sessionInfo: SessionInfo; + toggleFavorite: (stashId: string) => void; + validatorsActive?: Validator[]; +} + +function ListActive ({ className = '', isRelay, points, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement { + const { t } = useTranslation(); + const isNextTick = useNextTick(); + + const headerActive = useRef<[string?, string?, number?][]>([ + // favorite, badges, details, expand + [t('validators'), 'start', 4] + ]); + + return ( +
('No session validators found')} + emptySpinner={t('Retrieving session validators')} + header={headerActive.current} + isSplit + legend={} + > + {isNextTick && validatorsActive?.map((v) => ( + + ))} +
+ ); +} + +export default React.memo(ListActive); diff --git a/packages/page-staking2/src/Validators/ListWaiting.tsx b/packages/page-staking2/src/Validators/ListWaiting.tsx new file mode 100644 index 0000000000..06faada80b --- /dev/null +++ b/packages/page-staking2/src/Validators/ListWaiting.tsx @@ -0,0 +1,57 @@ +// Copyright 2017-2023 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SessionInfo, Validator } from '../types'; + +import React, { useRef } from 'react'; + +import Legend from '@polkadot/app-staking/Legend'; +import { Table } from '@polkadot/react-components'; +import { useNextTick } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate'; +import useValidatorsWaiting from '../useValidatorsWaiting'; +import ValidatorRow from './Validator'; + +interface Props { + className?: string; + favorites: string[]; + isRelay: boolean; + sessionInfo: SessionInfo; + toggleFavorite: (stashId: string) => void; + validatorsActive?: Validator[]; +} + +function ListActive ({ className = '', favorites, isRelay, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement { + const { t } = useTranslation(); + const isNextTick = useNextTick(); + const validatorsWaiting = useValidatorsWaiting(favorites, sessionInfo, validatorsActive); + + const headerWaiting = useRef<[string?, string?, number?][]>([ + // favorite, badges, details, expand + [t('waiting'), 'start', 4] + ]); + + return ( + ('No waiting validators found')} + emptySpinner={t('Retrieving waiting validators')} + header={headerWaiting.current} + isSplit + legend={} + > + {isNextTick && validatorsWaiting?.map((v) => ( + + ))} +
+ ); +} + +export default React.memo(ListActive); diff --git a/packages/page-staking2/src/Validators/Validator/Status.tsx b/packages/page-staking2/src/Validators/Validator/Status.tsx index a3815e7b86..ac06184acc 100644 --- a/packages/page-staking2/src/Validators/Validator/Status.tsx +++ b/packages/page-staking2/src/Validators/Validator/Status.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Validator } from '../../types'; -import type { Heartbeat } from './types'; +import type { UseHeartbeat } from '../types'; import React, { useMemo } from 'react'; @@ -10,7 +10,7 @@ import { Badge } from '@polkadot/react-components'; import { useAccounts } from '@polkadot/react-hooks'; interface Props { - heartbeat: Heartbeat; + heartbeat: UseHeartbeat; isChilled?: boolean; isPara?: boolean; isRelay?: boolean; diff --git a/packages/page-staking2/src/Validators/Validator/index.tsx b/packages/page-staking2/src/Validators/Validator/index.tsx index 86e4647e4a..934e4023e0 100644 --- a/packages/page-staking2/src/Validators/Validator/index.tsx +++ b/packages/page-staking2/src/Validators/Validator/index.tsx @@ -3,7 +3,7 @@ import type { SessionInfo, Validator } from '../../types'; -import React, { useMemo } from 'react'; +import React, { useMemo, useRef } from 'react'; import { AddressSmall, Table, Tag } from '@polkadot/react-components'; import { useToggle } from '@polkadot/react-hooks'; @@ -41,12 +41,22 @@ function ValidatorExpanded ({ className = '' }: PropsExpanded): React.ReactEleme function Validator ({ className = '', isRelay, points, sessionInfo, toggleFavorite, validator }: Props): React.ReactElement { const [isExpanded, toggleExpanded] = useToggle(); + const pointsRef = useRef<{ counter: number, points: number }>({ counter: 0, points: 0 }); const exposure = useExposure(validator, sessionInfo); const heartbeat = useHeartbeat(validator, sessionInfo); const pointsAnimClass = useMemo( - () => (points && `greyAnim-${Date.now() % 25}`) || '', - [points] + (): string => { + if (!points || pointsRef.current.points === points) { + return ''; + } + + pointsRef.current.points = points; + pointsRef.current.counter = (pointsRef.current.counter + 1) % 25; + + return `greyAnim-${pointsRef.current.counter}`; + }, + [points, pointsRef] ); return ( diff --git a/packages/page-staking2/src/Validators/Validator/types.ts b/packages/page-staking2/src/Validators/Validator/types.ts deleted file mode 100644 index 0fcbdf0187..0000000000 --- a/packages/page-staking2/src/Validators/Validator/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017-2023 @polkadot/app-staking authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface Heartbeat { - authoredBlocks?: number; - isOnline: boolean; -} diff --git a/packages/page-staking2/src/Validators/Validator/useExposure.ts b/packages/page-staking2/src/Validators/Validator/useExposure.ts index 3290aa6dbf..c0f3fdacbd 100644 --- a/packages/page-staking2/src/Validators/Validator/useExposure.ts +++ b/packages/page-staking2/src/Validators/Validator/useExposure.ts @@ -3,34 +3,21 @@ import type { PalletStakingExposure } from '@polkadot/types/lookup'; import type { SessionInfo, Validator } from '../../types'; +import type { UseExposure, UseExposureExposure } from '../types'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; -import { BN } from '@polkadot/util'; +import { BN, objectSpread } from '@polkadot/util'; -interface ExposureEntry { - who: string, - value: BN +interface CacheEntry extends UseExposure { + activeEra: BN; } -interface Exposure { - others: ExposureEntry[]; - own: BN; - total: BN; -} - -interface Result { - clipped?: Exposure; - exposure?: Exposure; - waiting?: { - others: ExposureEntry[], - total: BN - }; -} +type Cache = Record; const OPT_EXPOSURE = { - transform: ({ others, own, total }: PalletStakingExposure): Exposure => ({ + transform: ({ others, own, total }: PalletStakingExposure): UseExposureExposure => ({ others: others .map(({ value, who }) => ({ value: value.unwrap(), @@ -42,28 +29,28 @@ const OPT_EXPOSURE = { }) }; -function getResult (exposure?: Exposure, clipped?: Exposure): Result { - let waiting: Result['waiting']; +function getResult (exposure: UseExposureExposure, clipped: UseExposureExposure): UseExposure { + let waiting: UseExposure['waiting']; - if (exposure && clipped) { - const others = exposure.others.filter(({ who }) => - !clipped.others.find((c) => - who === c.who - ) - ); + const others = exposure.others.filter(({ who }) => + !clipped.others.find((c) => + who === c.who + ) + ); - if (others.length) { - waiting = { - others, - total: others.reduce((total, { value }) => total.iadd(value), new BN(0)) - }; - } + if (others.length) { + waiting = { + others, + total: others.reduce((total, { value }) => total.iadd(value), new BN(0)) + }; } return { clipped, exposure, waiting }; } -function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): Result | undefined { +const cache: Cache = {}; + +function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): UseExposure | undefined { const { api } = useApi(); const params = useMemo( @@ -74,10 +61,26 @@ function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): R const fullExposure = useCall(params && api.query.staking.erasStakers, params, OPT_EXPOSURE); const clipExposure = useCall(params && api.query.staking.erasStakersClipped, params, OPT_EXPOSURE); - return useMemo( - () => getResult(fullExposure, clipExposure), + const result = useMemo( + () => fullExposure && clipExposure && getResult(fullExposure, clipExposure), [clipExposure, fullExposure] ); + + // clear the cached result on era changes + useEffect((): void => { + if (activeEra && (!cache[stashId] || !activeEra.eq(cache[stashId].activeEra))) { + cache[stashId] = { activeEra }; + } + }, [activeEra, stashId]); + + // update the cached result on exposure changes + useEffect((): void => { + if (result && activeEra && cache[stashId] && activeEra.eq(cache[stashId].activeEra)) { + cache[stashId] = objectSpread({ activeEra }, result); + } + }, [activeEra, result, stashId]); + + return result || cache[stashId]; } export default createNamedHook('useExposure', useExposureImpl); diff --git a/packages/page-staking2/src/Validators/Validator/useHeartbeat.ts b/packages/page-staking2/src/Validators/Validator/useHeartbeat.ts index dbe947af0e..8fbdbd83a0 100644 --- a/packages/page-staking2/src/Validators/Validator/useHeartbeat.ts +++ b/packages/page-staking2/src/Validators/Validator/useHeartbeat.ts @@ -3,12 +3,22 @@ import type { Option, u32 } from '@polkadot/types'; import type { Codec } from '@polkadot/types/types'; +import type { BN } from '@polkadot/util'; import type { SessionInfo, Validator } from '../../types'; -import type { Heartbeat } from './types'; +import type { UseHeartbeat } from '../types'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; +import { isBoolean, isNumber, objectSpread } from '@polkadot/util'; + +interface CacheEntry extends UseHeartbeat { + currentSession: BN; +} + +type Cache = Record; + +const EMPTY: UseHeartbeat = {}; const OPT_BLOCKS = { transform: (authoredBlocks: u32): number => @@ -21,27 +31,47 @@ const OPT_BEATS = { receivedHeartbeats.isSome }; -function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): Heartbeat { +const cache: Cache = {}; + +function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): UseHeartbeat { const { api } = useApi(); const params = useMemo( - () => currentSession && ({ - authoredBlocks: [currentSession, stashId], - receivedHeartbeats: [currentSession, stashIndex] - }), + () => stashIndex === -1 + ? undefined + : currentSession && ({ + authoredBlocks: [currentSession, stashId], + receivedHeartbeats: [currentSession, stashIndex] + }), [currentSession, stashId, stashIndex] ); const authoredBlocks = useCall(params && api.query.imOnline.authoredBlocks, params?.authoredBlocks, OPT_BLOCKS); const receivedHeartbeats = useCall(params && api.query.imOnline.receivedHeartbeats, params?.receivedHeartbeats, OPT_BEATS); - return useMemo( - () => ({ + const result = useMemo( + () => isNumber(authoredBlocks) && isBoolean(receivedHeartbeats) && ({ authoredBlocks, isOnline: !!(authoredBlocks || receivedHeartbeats) }), [authoredBlocks, receivedHeartbeats] ); + + // when the session changes, empty the cache + useEffect((): void => { + if (currentSession && (!cache[stashId] || !currentSession.eq(cache[stashId].currentSession))) { + cache[stashId] = { currentSession }; + } + }, [currentSession, stashId]); + + // set the new heartbeat info based on retrieved + useEffect((): void => { + if (result && currentSession && currentSession.eq(cache[stashId].currentSession)) { + cache[stashId] = objectSpread({ currentSession }, result); + } + }, [currentSession, result, stashId]); + + return result || cache[stashId] || EMPTY; } export default createNamedHook('useHeartbeat', useHeartbeatImpl); diff --git a/packages/page-staking2/src/Validators/index.tsx b/packages/page-staking2/src/Validators/index.tsx index c0309068ea..808faa06e0 100644 --- a/packages/page-staking2/src/Validators/index.tsx +++ b/packages/page-staking2/src/Validators/index.tsx @@ -1,83 +1,94 @@ // Copyright 2017-2023 @polkadot/app-staking authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { SessionInfo, Validator } from '../types'; +import type { SessionInfo } from '../types'; -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import styled from 'styled-components'; -import Legend from '@polkadot/app-staking/Legend'; -import { Table } from '@polkadot/react-components'; +import { Button, ToggleGroup } from '@polkadot/react-components'; import { useTranslation } from '../translate'; +import useValidatorsActive from '../useValidatorsActive'; +import ListActive from './ListActive'; +import ListWaiting from './ListWaiting'; import usePoints from './usePoints'; -import ValidatorRow from './Validator'; interface Props { className?: string; + favorites: string[]; isRelay: boolean; sessionInfo: SessionInfo; toggleFavorite: (stashId: string) => void; - validatorsSession?: Validator[]; } -function Validators ({ className = '', isRelay, sessionInfo, toggleFavorite, validatorsSession }: Props): React.ReactElement { +function Validators ({ className = '', favorites, isRelay, sessionInfo, toggleFavorite }: Props): React.ReactElement { const { t } = useTranslation(); + const [intentIndex, setIntentIndex] = useState(0); + const validatorsActive = useValidatorsActive(favorites, sessionInfo); const points = usePoints(sessionInfo); - const headerActive = useRef<[string?, string?, number?][]>([ - // favorite, badges, details, expand - [t('validators'), 'start', 4] + const intentOptions = useRef([ + { text: t('Active'), value: 'active' }, + { text: t('Waiting'), value: 'waiting' } ]); return ( - ('No session validators found')} - emptySpinner={t('Retrieving session validators')} - header={headerActive.current} - isSplit - legend={ - + + + + {intentIndex === 0 + ? ( + + ) + : ( + + ) } - > - {validatorsSession?.map((v) => ( - - ))} - + ); } -const StyledTable = styled(Table)` - td.statusInfo { - padding: 0 0 0 0.5rem; - vertical-align: middle; +const StyledDiv = styled.div` + .ui--Table table { + td.statusInfo { + padding: 0 0 0 0.5rem; + vertical-align: middle; - > div { - display: inline-block; - max-width: 3.6rem; - min-width: 3.6rem; + > div { + display: inline-block; + max-width: 3.6rem; + min-width: 3.6rem; - .ui--Badge { - margin: 0.125rem; + .ui--Badge { + margin: 0.125rem; - &.opaque { - opacity: var(--opacity-gray); + &.opaque { + opacity: var(--opacity-gray); + } } } - } - + td.address { - padding-left: 0.5rem; + + td.address { + padding-left: 0.5rem; + } } } `; diff --git a/packages/page-staking2/src/Validators/types.ts b/packages/page-staking2/src/Validators/types.ts new file mode 100644 index 0000000000..e3eaddce7a --- /dev/null +++ b/packages/page-staking2/src/Validators/types.ts @@ -0,0 +1,31 @@ +// Copyright 2017-2023 @polkadot/app-staking authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; + +export type UsePoints = Record; + +export interface UseHeartbeat { + authoredBlocks?: number; + isOnline?: boolean; +} + +export interface UseExposureExposureEntry { + who: string, + value: BN +} + +export interface UseExposureExposure { + others: UseExposureExposureEntry[]; + own: BN; + total: BN; +} + +export interface UseExposure { + clipped?: UseExposureExposure; + exposure?: UseExposureExposure; + waiting?: { + others: UseExposureExposureEntry[]; + total: BN; + }; +} diff --git a/packages/page-staking2/src/Validators/usePoints.ts b/packages/page-staking2/src/Validators/usePoints.ts index c94b980997..52ae7ceccd 100644 --- a/packages/page-staking2/src/Validators/usePoints.ts +++ b/packages/page-staking2/src/Validators/usePoints.ts @@ -2,27 +2,34 @@ // SPDX-License-Identifier: Apache-2.0 import type { PalletStakingEraRewardPoints } from '@polkadot/types/lookup'; +import type { BN } from '@polkadot/util'; import type { SessionInfo } from '../types'; +import type { UsePoints } from './types'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; import { BN_ZERO } from '@polkadot/util'; -type Result = Record; +interface Cache { + activeEra: BN; + points?: UsePoints; +} const OPT_POINTS = { - transform: ({ individual }: PalletStakingEraRewardPoints): Result => + transform: ({ individual }: PalletStakingEraRewardPoints): UsePoints => [...individual.entries()] .filter(([, points]) => points.gt(BN_ZERO)) - .reduce((result: Result, [stashId, points]): Result => { + .reduce((result: UsePoints, [stashId, points]): UsePoints => { result[stashId.toString()] = points.toNumber(); return result; }, {}) }; -function usePointsImpl ({ activeEra }: SessionInfo): Result | undefined { +let cache: Cache | undefined; + +function usePointsImpl ({ activeEra }: SessionInfo): UsePoints | undefined { const { api } = useApi(); const queryParams = useMemo( @@ -30,7 +37,19 @@ function usePointsImpl ({ activeEra }: SessionInfo): Result | undefined { [activeEra] ); - return useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS); + const points = useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS); + + useEffect((): void => { + if (activeEra && points) { + cache = { activeEra, points }; + } + }, [activeEra, points]); + + return points || ( + cache && + activeEra?.eq(cache.activeEra) && + cache.points + ) || undefined; } export default createNamedHook('usePoints', usePointsImpl); diff --git a/packages/page-staking2/src/index.tsx b/packages/page-staking2/src/index.tsx index 50cc4cc669..d03fac14e0 100644 --- a/packages/page-staking2/src/index.tsx +++ b/packages/page-staking2/src/index.tsx @@ -12,7 +12,6 @@ import { useApi, useFavorites } from '@polkadot/react-hooks'; import { STORE_FAVS_BASE } from './constants'; import { useTranslation } from './translate'; import useSessionInfo from './useSessionInfo'; -import useValidatorsActive from './useValidatorsActive'; import Validators from './Validators'; function StakingApp ({ basePath }: Props): React.ReactElement { @@ -20,7 +19,6 @@ function StakingApp ({ basePath }: Props): React.ReactElement { const { api } = useApi(); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); const sessionInfo = useSessionInfo(); - const validatorsActive = useValidatorsActive(favorites, sessionInfo); const isRelay = useMemo( () => !!(api.query.parasShared || api.query.shared)?.activeValidatorIndices, @@ -44,10 +42,10 @@ function StakingApp ({ basePath }: Props): React.ReactElement { diff --git a/packages/page-staking2/src/useElectedValidators.ts b/packages/page-staking2/src/useElectedValidators.ts index 13ae7b8df5..3f28c33dba 100644 --- a/packages/page-staking2/src/useElectedValidators.ts +++ b/packages/page-staking2/src/useElectedValidators.ts @@ -3,6 +3,7 @@ import type { StorageKey, u32 } from '@polkadot/types'; import type { AccountId32 } from '@polkadot/types/interfaces'; +import type { BN } from '@polkadot/util'; import type { SessionInfo } from './types'; import { useEffect, useMemo } from 'react'; @@ -10,8 +11,8 @@ import { useEffect, useMemo } from 'react'; import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks'; interface Cache { - currentEra: SessionInfo['currentEra']; - elected?: string[]; + currentEra: BN; + elected: string[]; } const OPT_ELECTED = { @@ -23,15 +24,13 @@ const OPT_ELECTED = { // since this would only change once per era for the full runtime, // we just store it globally to not have expensive map-key lookups -const cached: Cache = { - currentEra: null -}; +let cache: Cache | undefined; function useElectedValidatorsImpl ({ currentEra }: SessionInfo): string[] | undefined { const { api } = useApi(); const electedParams = useMemo( - () => currentEra && (!cached.currentEra || !cached.currentEra.eq(currentEra)) + () => cache && currentEra && !cache.currentEra.eq(currentEra) ? [currentEra] : null, [currentEra] @@ -41,20 +40,15 @@ function useElectedValidatorsImpl ({ currentEra }: SessionInfo): string[] | unde useEffect((): void => { if (currentEra && elected) { - cached.currentEra = currentEra; - cached.elected = elected; + cache = { currentEra, elected }; } }, [currentEra, elected]); - return useMemo( - () => ( - currentEra && - cached.currentEra && - cached.currentEra.eq(currentEra) && - cached.elected - ) || elected, - [currentEra, elected] - ); + return elected || ( + cache && + currentEra?.eq(cache.currentEra) && + cache.elected + ) || undefined; } export default createNamedHook('useElectedValidators', useElectedValidatorsImpl); diff --git a/packages/page-staking2/src/useValidatorsActive.ts b/packages/page-staking2/src/useValidatorsActive.ts index b1665e1192..eda67a947b 100644 --- a/packages/page-staking2/src/useValidatorsActive.ts +++ b/packages/page-staking2/src/useValidatorsActive.ts @@ -2,12 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 import type { AccountId32 } from '@polkadot/types/interfaces'; +import type { BN } from '@polkadot/util'; import type { SessionInfo, Validator } from './types'; +import { useEffect } from 'react'; + import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; import useTaggedValidators from './useTaggedValidators'; +interface Cache { + activeEra: BN; + validators: Validator[]; +} + const OPT_VALIDATORS = { transform: (validators: AccountId32[]): Validator[] => validators.map((a, stashIndex) => { @@ -24,11 +32,24 @@ const OPT_VALIDATORS = { }) }; +let cache: Cache | undefined; + function useValidatorsActiveImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined { const { api } = useApi(); - const validators = useCall(api.query.session.validators, undefined, OPT_VALIDATORS); + const sessionValidators = useCall(api.query.session.validators, undefined, OPT_VALIDATORS); + const validators = useTaggedValidators(favorites, sessionInfo, sessionValidators); - return useTaggedValidators(favorites, sessionInfo, validators); + useEffect((): void => { + if (validators && sessionInfo.activeEra) { + cache = { activeEra: sessionInfo.activeEra, validators }; + } + }, [sessionInfo, validators]); + + return validators || ( + cache && + sessionInfo.activeEra?.eq(cache.activeEra) && + cache.validators + ) || undefined; } export default createNamedHook('useValidatorsActive', useValidatorsActiveImpl); diff --git a/packages/page-staking2/src/useValidatorsAll.ts b/packages/page-staking2/src/useValidatorsAll.ts index 0dc7da54f1..f2d3415a2f 100644 --- a/packages/page-staking2/src/useValidatorsAll.ts +++ b/packages/page-staking2/src/useValidatorsAll.ts @@ -6,7 +6,7 @@ import type { StorageKey } from '@polkadot/types'; import type { AccountId32, EventRecord } from '@polkadot/types/interfaces'; import type { SessionInfo, Validator } from './types'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@polkadot/react-hooks'; @@ -49,6 +49,8 @@ function mapValidators (validators?: AccountId32[]): Validator[] | undefined { }); } +let cache: Validator[] | undefined; + function useValidatorsAllImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined { const { api } = useApi(); const startValue = useMapKeys(api.query.staking.validators, EMPTY_PARAMS, OPT_VALIDATORS); @@ -62,7 +64,15 @@ function useValidatorsAllImpl (favorites: string[], sessionInfo: SessionInfo): V [validators] ); - return useTaggedValidators(favorites, sessionInfo, validatorsIndexed); + const result = useTaggedValidators(favorites, sessionInfo, validatorsIndexed); + + useEffect((): void => { + if (result) { + cache = result; + } + }, [result]); + + return result || cache; } export default createNamedHook('useValidatorsAll', useValidatorsAllImpl); diff --git a/packages/page-staking2/src/useValidatorsWaiting.ts b/packages/page-staking2/src/useValidatorsWaiting.ts index 1489961c86..c8a247a43c 100644 --- a/packages/page-staking2/src/useValidatorsWaiting.ts +++ b/packages/page-staking2/src/useValidatorsWaiting.ts @@ -1,31 +1,49 @@ // Copyright 2017-2023 @polkadot/app-staking authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { BN } from '@polkadot/util'; import type { SessionInfo, Validator } from './types'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { createNamedHook } from '@polkadot/react-hooks'; -import useValidatorsActive from './useValidatorsActive'; import useValidatorsAll from './useValidatorsAll'; +interface Cache { + activeEra: BN; + validators: Validator[]; +} + function excludeValidators (from?: Validator[], exclude?: Validator[]): Validator[] | undefined { return from && exclude && from.filter(({ stashId }) => !exclude.some((v) => v.stashId === stashId) ); } -function useValidatorsWaitingImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined { - const activeValidators = useValidatorsActive(favorites, sessionInfo); +let cache: Cache | undefined; + +function useValidatorsWaitingImpl (favorites: string[], sessionInfo: SessionInfo, activeValidators?: Validator[]): Validator[] | undefined { const allValidators = useValidatorsAll(favorites, sessionInfo); // both active and all is already sorted and tagged, so we don't // need to re-sort the waiting list - return useMemo( + const validators = useMemo( () => excludeValidators(allValidators, activeValidators), [activeValidators, allValidators] ); + + useEffect((): void => { + if (validators && sessionInfo.activeEra) { + cache = { activeEra: sessionInfo.activeEra, validators }; + } + }, [sessionInfo, validators]); + + return validators || ( + cache && + sessionInfo.activeEra?.eq(cache.activeEra) && + cache.validators + ) || undefined; } export default createNamedHook('useValidatorsWaiting', useValidatorsWaitingImpl); diff --git a/packages/react-components/src/InputAddressMulti/index.tsx b/packages/react-components/src/InputAddressMulti/index.tsx index 6372f486fc..080d628253 100644 --- a/packages/react-components/src/InputAddressMulti/index.tsx +++ b/packages/react-components/src/InputAddressMulti/index.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { useDebounce, useLoadingDelay } from '@polkadot/react-hooks'; +import { useDebounce, useNextTick } from '@polkadot/react-hooks'; import Input from '../Input'; import Spinner from '../Spinner'; @@ -39,7 +39,7 @@ function InputAddressMulti ({ available, availableLabel, className = '', default const [_filter, setFilter] = useState(''); const [selected, setSelected] = useState([]); const filter = useDebounce(_filter); - const isLoading = useLoadingDelay(); + const isNextTick = useNextTick(); useEffect((): void => { defaultValue && setSelected(defaultValue); @@ -86,9 +86,8 @@ function InputAddressMulti ({ available, availableLabel, className = '', default
- {isLoading - ? - : available.map((address) => ( + {isNextTick + ? available.map((address) => ( )) + : }
diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 941be94622..3bd722a9ec 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -45,11 +45,11 @@ export { useIsMountedRef } from './useIsMountedRef'; export { useJudgements } from './useJudgements'; export { useKeyring } from './useKeyring'; export { useLedger } from './useLedger'; -export { useLoadingDelay } from './useLoadingDelay'; export { useMapEntries } from './useMapEntries'; export { useMapKeys } from './useMapKeys'; export { useMemoValue } from './useMemoValue'; export { useModal } from './useModal'; +export { useNextTick } from './useNextTick'; export { useNonEmptyString } from './useNonEmptyString'; export { useNonZeroBn } from './useNonZeroBn'; export { useOwnEraRewards } from './useOwnEraRewards'; diff --git a/packages/react-hooks/src/useLoadingDelay.ts b/packages/react-hooks/src/useLoadingDelay.ts deleted file mode 100644 index 16f9a3d71b..0000000000 --- a/packages/react-hooks/src/useLoadingDelay.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017-2023 @polkadot/react-hooks authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useEffect, useState } from 'react'; - -import { createNamedHook } from './createNamedHook'; -import { useIsMountedRef } from './useIsMountedRef'; - -function useLoadingDelayImpl (delay = 0): boolean { - const mountedRef = useIsMountedRef(); - const [isLoading, setIsLoading] = useState(true); - - useEffect((): void => { - setTimeout((): void => { - mountedRef.current && setIsLoading(false); - }, delay); - // Ignore, this is for the initial setup - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return isLoading; -} - -export const useLoadingDelay = createNamedHook('useLoadingDelay', useLoadingDelayImpl); diff --git a/packages/react-hooks/src/useNextTick.ts b/packages/react-hooks/src/useNextTick.ts new file mode 100644 index 0000000000..8553b9bf33 --- /dev/null +++ b/packages/react-hooks/src/useNextTick.ts @@ -0,0 +1,20 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react'; + +import { nextTick } from '@polkadot/util'; + +import { createNamedHook } from './createNamedHook'; + +function useNextTickImpl (): boolean { + const [isNextTick, setIsNextTick] = useState(false); + + useEffect((): void => { + nextTick(() => setIsNextTick(true)); + }, []); + + return isNextTick; +} + +export const useNextTick = createNamedHook('useNextTick', useNextTickImpl); diff --git a/packages/react-hooks/src/useWeight.ts b/packages/react-hooks/src/useWeight.ts index 2c0153a83e..2385aa69e3 100644 --- a/packages/react-hooks/src/useWeight.ts +++ b/packages/react-hooks/src/useWeight.ts @@ -45,11 +45,18 @@ const EMPTY_STATE: Partial = { // return both v1 & v2 weight structures (would depend on actual use) export function convertWeight (weight: V1Weight | V2Weight): { v1Weight: BN, v2Weight: V2WeightConstruct } { if ((weight as V2Weight).proofSize) { + // V2 weight const refTime = (weight as V2Weight).refTime.toBn(); return { v1Weight: refTime, v2Weight: weight as V2Weight }; + } else if ((weight as V2Weight).refTime) { + // V1.5 weight (when not converted) + const refTime = (weight as V2Weight).refTime.toBn(); + + return { v1Weight: refTime, v2Weight: { refTime } }; } + // V1 weight const refTime = (weight as V1Weight).toBn(); return { v1Weight: refTime, v2Weight: { refTime } }; diff --git a/packages/test-support/src/pages/Page.tsx b/packages/test-support/src/pages/Page.tsx index cb80b08642..3880a3e52f 100644 --- a/packages/test-support/src/pages/Page.tsx +++ b/packages/test-support/src/pages/Page.tsx @@ -60,8 +60,8 @@ jest.mock('@polkadot/react-hooks/useAccountInfo', () => { }); }); -jest.mock('@polkadot/react-hooks/useLoadingDelay', () => ({ - useLoadingDelay: () => false +jest.mock('@polkadot/react-hooks/useNextTick', () => ({ + useNextTick: () => true })); jest.mock('@polkadot/react-hooks/useBalancesAll', () => ({