diff --git a/packages/apps/src/Apps.tsx b/packages/apps/src/Apps.tsx index 8674457248..24a38fefe3 100644 --- a/packages/apps/src/Apps.tsx +++ b/packages/apps/src/Apps.tsx @@ -20,7 +20,7 @@ import WarmUp from './WarmUp'; export const PORTAL_ID = 'portals'; function Apps ({ className = '' }: Props): React.ReactElement { - const theme = useTheme(); + const { themeClassName } = useTheme(); const { isDevelopment, specName, systemChain, systemName } = useApi(); const uiHighlight = useMemo( @@ -33,7 +33,7 @@ function Apps ({ className = '' }: Props): React.ReactElement { return ( <> -
+
diff --git a/packages/apps/src/Content/Status.tsx b/packages/apps/src/Content/Status.tsx index ed7fbd947e..12ea9305c1 100644 --- a/packages/apps/src/Content/Status.tsx +++ b/packages/apps/src/Content/Status.tsx @@ -5,10 +5,10 @@ import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { EventRecord } from '@polkadot/types/interfaces'; import type { KeyringOptions } from '@polkadot/ui-keyring/options/types'; -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; -import { Status as StatusDisplay, StatusContext } from '@polkadot/react-components'; -import { useAccounts, useApi, useCall } from '@polkadot/react-hooks'; +import { Status as StatusDisplay } from '@polkadot/react-components'; +import { useAccounts, useApi, useCall, useQueue } from '@polkadot/react-hooks'; import { stringToU8a } from '@polkadot/util'; import { xxhashAsHex } from '@polkadot/util-crypto'; @@ -62,7 +62,7 @@ function filterEvents (allAccounts: string[], t: (key: string, opts } function Status ({ optionsAll }: Props): React.ReactElement { - const { queueAction } = useContext(StatusContext); + const { queueAction } = useQueue(); const { api, isApiReady } = useApi(); const { allAccounts } = useAccounts(); const { t } = useTranslation(); diff --git a/packages/apps/src/Content/index.tsx b/packages/apps/src/Content/index.tsx index 4160cbd2f4..b9712d8c35 100644 --- a/packages/apps/src/Content/index.tsx +++ b/packages/apps/src/Content/index.tsx @@ -3,13 +3,13 @@ import type { Route } from '@polkadot/apps-routing/types'; -import React, { Suspense, useContext, useMemo } from 'react'; +import React, { Suspense, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; import createRoutes from '@polkadot/apps-routing'; -import { ErrorBoundary, Spinner, StatusContext, TabsContext } from '@polkadot/react-components'; -import { useApi } from '@polkadot/react-hooks'; +import { ErrorBoundary, Spinner, TabsCtx } from '@polkadot/react-components'; +import { useApi, useQueue } from '@polkadot/react-hooks'; import { findMissingApis } from '../endpoint'; import { useTranslation } from '../translate'; @@ -34,7 +34,7 @@ function Content ({ className }: Props): React.ReactElement { const location = useLocation(); const { t } = useTranslation(); const { api, isApiConnected, isApiReady, isDevelopment } = useApi(); - const { queueAction } = useContext(StatusContext); + const { queueAction } = useQueue(); const { Component, display: { needsApi, needsApiCheck, needsApiInstances }, icon, name, text } = useMemo( (): Route => { @@ -70,7 +70,7 @@ function Content ({ className }: Props): React.ReactElement { <> - + {missingApis.length ? ( { /> ) } - + diff --git a/packages/apps/src/Root.tsx b/packages/apps/src/Root.tsx index 6adf9083fa..eab753d5f5 100644 --- a/packages/apps/src/Root.tsx +++ b/packages/apps/src/Root.tsx @@ -1,22 +1,19 @@ // Copyright 2017-2023 @polkadot/apps authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ThemeDef } from '@polkadot/react-components/types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; import type { KeyringStore } from '@polkadot/ui-keyring/types'; import React, { Suspense, useEffect, useState } from 'react'; import { HashRouter } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; -import { Api } from '@polkadot/react-api'; -import { ApiStats } from '@polkadot/react-components'; -import Queue from '@polkadot/react-components/Status/Queue'; -import { BlockAuthors, Events } from '@polkadot/react-query'; +import { ApiCtxRoot } from '@polkadot/react-api'; +import { ApiStatsCtxRoot, BlockAuthorsCtxRoot, BlockEventsCtxRoot, KeyringCtxRoot, QueueCtxRoot, WindowSizeCtxRoot } from '@polkadot/react-hooks'; import { settings } from '@polkadot/ui-settings'; import Apps from './Apps'; import { darkTheme, lightTheme } from './themes'; -import WindowDimensions from './WindowDimensions'; interface Props { isElectron: boolean; @@ -41,28 +38,32 @@ function Root ({ isElectron, store }: Props): React.ReactElement { settings.on('change', (settings) => setTheme(createTheme(settings))); }, []); + // The ordering here is critical. It defines the hierarchy of dependencies, + // i.e. Block* could from Api. Certainly no cross-deps allowed return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); diff --git a/packages/apps/src/WindowDimensions.tsx b/packages/apps/src/WindowDimensions.tsx deleted file mode 100644 index b70cc3d296..0000000000 --- a/packages/apps/src/WindowDimensions.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2017-2023 @polkadot/apps authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -// Adapted from https://hackernoon.com/simplifying-responsive-layouts-with-react-hooks-19db73893a7a - -import React, { useEffect, useState } from 'react'; - -interface Dimensions { - windowHeight: number; - windowWidth: number; -} - -interface Props { - children: React.ReactNode; -} - -const WindowDimensionsCtx = React.createContext({ windowHeight: window.innerHeight, windowWidth: window.innerWidth }); - -function WindowDimensionsProvider ({ children }: Props): React.ReactElement { - const [dimensions, setDimensions] = useState({ windowHeight: window.innerHeight, windowWidth: window.innerWidth }); - - useEffect(() => { - const handleResize = () => setDimensions({ windowHeight: window.innerHeight, windowWidth: window.innerWidth }); - - window.addEventListener('resize', handleResize); - - return (): void => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - return ( - - {children} - - ); -} - -export default React.memo(WindowDimensionsProvider); - -export { WindowDimensionsCtx }; diff --git a/packages/apps/src/themes.ts b/packages/apps/src/themes.ts index 595185d71c..8a07309129 100644 --- a/packages/apps/src/themes.ts +++ b/packages/apps/src/themes.ts @@ -1,7 +1,7 @@ // Copyright 2017-2023 @polkadot/apps authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ThemeDef } from '@polkadot/react-components/types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; export const darkTheme: ThemeDef = { theme: 'dark' diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index 8440dcc208..8c15667bb3 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -10,13 +10,13 @@ import type { ProxyDefinition, RecoveryConfig } from '@polkadot/types/interfaces import type { KeyringAddress, KeyringJson$Meta } from '@polkadot/ui-keyring/types'; import type { AccountBalance, Delegation } from '../types'; -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { ApiPromise } from '@polkadot/api'; import useAccountLocks from '@polkadot/app-referenda/useAccountLocks'; -import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Columar, CryptoType, Forget, LinkExternal, Menu, Popup, StatusContext, Table, Tags } from '@polkadot/react-components'; -import { useAccountInfo, useApi, useBalancesAll, useBestNumber, useCall, useLedger, useStakingInfo, useToggle } from '@polkadot/react-hooks'; +import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Columar, CryptoType, Forget, LinkExternal, Menu, Popup, Table, Tags } from '@polkadot/react-components'; +import { useAccountInfo, useApi, useBalancesAll, useBestNumber, useCall, useLedger, useQueue, useStakingInfo, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { BN, BN_ZERO, formatBalance, formatNumber, isFunction } from '@polkadot/util'; @@ -151,7 +151,7 @@ const transformRecovery = { function Account ({ account: { address, meta }, className = '', delegation, filter, isFavorite, proxy, setBalance, toggleFavorite }: Props): React.ReactElement | null { const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); - const { queueExtrinsic } = useContext(StatusContext); + const { queueExtrinsic } = useQueue(); const api = useApi(); const { getLedger } = useLedger(); const bestNumber = useBestNumber(); diff --git a/packages/page-accounts/src/Accounts/useKnownAddresses.ts b/packages/page-accounts/src/Accounts/useKnownAddresses.ts index f6d7ae8d5f..c2bcbc37a3 100644 --- a/packages/page-accounts/src/Accounts/useKnownAddresses.ts +++ b/packages/page-accounts/src/Accounts/useKnownAddresses.ts @@ -3,14 +3,23 @@ import { useMemo } from 'react'; -import { createNamedHook, useAccounts, useAddresses } from '@polkadot/react-hooks'; +import { createNamedHook, useKeyring } from '@polkadot/react-hooks'; + +function merge (result: string[], input: string[], exclude?: string): string[] { + return input.reduce((result, a) => { + if (a !== exclude && !result.includes(a)) { + result.push(a); + } + + return result; + }, result); +} function useKnownAddressesImpl (exclude?: string): string[] { - const { allAccounts } = useAccounts(); - const { allAddresses } = useAddresses(); + const { accounts: { allAccounts }, addresses: { allAddresses } } = useKeyring(); return useMemo( - () => [...allAccounts, ...allAddresses].filter((a) => a !== exclude), + () => merge(merge([], allAccounts, exclude), allAddresses, exclude), [allAccounts, allAddresses, exclude] ); } diff --git a/packages/page-accounts/src/Accounts/useMultisigApprovals.ts b/packages/page-accounts/src/Accounts/useMultisigApprovals.ts index bed273027c..95433d3507 100644 --- a/packages/page-accounts/src/Accounts/useMultisigApprovals.ts +++ b/packages/page-accounts/src/Accounts/useMultisigApprovals.ts @@ -4,13 +4,12 @@ import type { Option, StorageKey } from '@polkadot/types'; import type { H256, Multisig } from '@polkadot/types/interfaces'; -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; -import { createNamedHook, useApi, useIncrement, useIsMountedRef } from '@polkadot/react-hooks'; -import { EventsContext } from '@polkadot/react-query'; +import { createNamedHook, useApi, useBlockEvents, useIncrement, useIsMountedRef } from '@polkadot/react-hooks'; function useMultisigApprovalsImpl (address: string): [H256, Multisig][] { - const { events } = useContext(EventsContext); + const { events } = useBlockEvents(); const { api } = useApi(); const [multiInfos, setMultiInfos] = useState<[H256, Multisig][]>([]); const [trigger, incTrigger] = useIncrement(); diff --git a/packages/page-accounts/src/CreateAccount.slow.spec.tsx b/packages/page-accounts/src/CreateAccount.slow.spec.tsx index 6790fac163..7bb16af232 100644 --- a/packages/page-accounts/src/CreateAccount.slow.spec.tsx +++ b/packages/page-accounts/src/CreateAccount.slow.spec.tsx @@ -10,7 +10,7 @@ import { ThemeProvider } from 'styled-components'; import AccountsApp from '@polkadot/app-accounts'; import { lightTheme } from '@polkadot/apps/themes'; -import { Api } from '@polkadot/react-api'; +import { ApiCtxRoot } from '@polkadot/react-api'; import { MemoryStore } from '@polkadot/test-support/keyring'; import { WaitForApi } from '@polkadot/test-support/react'; import { SUBSTRATE_PORT } from '@polkadot/test-support/substrate'; @@ -25,7 +25,7 @@ const renderAccounts = () => { return render( - { />
- + ); diff --git a/packages/page-accounts/src/Sidebar/SidebarEditableSection.tsx b/packages/page-accounts/src/Sidebar/SidebarEditableSection.tsx index a98a80e1a5..0ca5b8fd18 100644 --- a/packages/page-accounts/src/Sidebar/SidebarEditableSection.tsx +++ b/packages/page-accounts/src/Sidebar/SidebarEditableSection.tsx @@ -1,7 +1,7 @@ // Copyright 2017-2023 @polkadot/page-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Tags } from '@polkadot/react-components'; import { useAccountInfo, useOutsideClick } from '@polkadot/react-hooks'; @@ -22,7 +22,12 @@ interface Props { function SidebarEditableSection ({ accountIndex, address, isBeingEdited, onUpdateName, sidebarRef }: Props): React.ReactElement { const { flags, isEditing, isEditingName, isEditingTags, name, onForgetAddress, onSaveName, onSaveTags, setIsEditingName, setIsEditingTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags } = useAccountInfo(address); - useEffect(() => { + const refs = useMemo( + () => [sidebarRef], + [sidebarRef] + ); + + useEffect((): void => { isBeingEdited(isEditing()); }, [isBeingEdited, isEditing]); @@ -42,7 +47,7 @@ function SidebarEditableSection ({ accountIndex, address, isBeingEdited, onUpdat } }, [isEditing, setName, setTags, setIsEditingName, setIsEditingTags, address]); - useOutsideClick([sidebarRef], onCancel); + useOutsideClick(refs, onCancel); return ( <> diff --git a/packages/page-accounts/src/Sidebar/index.tsx b/packages/page-accounts/src/Sidebar/index.tsx index 452b6dc3e8..8eec88d007 100644 --- a/packages/page-accounts/src/Sidebar/index.tsx +++ b/packages/page-accounts/src/Sidebar/index.tsx @@ -11,12 +11,14 @@ interface Props { type State = [string | null, (() => void) | null]; -type ToggleContext = undefined | (([address, onUpdateName]: State) => void); +type SetStateContext = undefined | (([address, onUpdateName]: State) => void); -const AccountSidebarToggle: React.Context = React.createContext(undefined); +const EMPTY_STATE: State = [null, null]; + +export const AccountSidebarCtx = React.createContext(undefined); function AccountSidebar ({ children }: Props): React.ReactElement { - const [[address, onUpdateName], setAddress] = useState([null, null]); + const [[address, onUpdateName], setAddress] = useState(EMPTY_STATE); const onClose = useCallback( () => setAddress([null, null]), @@ -24,7 +26,7 @@ function AccountSidebar ({ children }: Props): React.ReactElement { ); return ( - + {children} {address && ( { onUpdateName={onUpdateName} /> )} - + ); } -export { AccountSidebarToggle }; - export default React.memo(AccountSidebar); diff --git a/packages/page-accounts/src/modals/Derive.tsx b/packages/page-accounts/src/modals/Derive.tsx index 7eca4ef3ba..ce3ac2c20f 100644 --- a/packages/page-accounts/src/modals/Derive.tsx +++ b/packages/page-accounts/src/modals/Derive.tsx @@ -5,10 +5,10 @@ import type { KeyringPair } from '@polkadot/keyring/types'; import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { KeypairType } from '@polkadot/util-crypto/types'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import { AddressRow, Button, Input, InputAddress, MarkError, Modal, Password, StatusContext } from '@polkadot/react-components'; -import { useApi, useDebounce, useToggle } from '@polkadot/react-hooks'; +import { AddressRow, Button, Input, InputAddress, MarkError, Modal, Password } from '@polkadot/react-components'; +import { useApi, useDebounce, useQueue, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { nextTick } from '@polkadot/util'; import { keyExtractPath } from '@polkadot/util-crypto'; @@ -70,7 +70,7 @@ function createAccount (source: KeyringPair, suri: string, name: string, passwor function Derive ({ className = '', from, onClose }: Props): React.ReactElement { const { t } = useTranslation(); const { api, isDevelopment } = useApi(); - const { queueAction } = useContext(StatusContext); + const { queueAction } = useQueue(); const [source] = useState(() => keyring.getPair(from)); const [isBusy, setIsBusy] = useState(false); const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' }); diff --git a/packages/page-alliance/src/Members/Member.tsx b/packages/page-alliance/src/Members/Member.tsx index 26a93ef379..fc05747c15 100644 --- a/packages/page-alliance/src/Members/Member.tsx +++ b/packages/page-alliance/src/Members/Member.tsx @@ -4,10 +4,10 @@ import type { BN } from '@polkadot/util'; import type { Member as MemberType } from '../types'; -import React, { useCallback, useContext, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; -import { AddressSmall, Menu, Popup, StatusContext, Tag } from '@polkadot/react-components'; -import { useAccounts, useApi } from '@polkadot/react-hooks'; +import { AddressSmall, Menu, Popup, Tag } from '@polkadot/react-components'; +import { useAccounts, useApi, useQueue } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; import { useTranslation } from '../translate'; @@ -26,7 +26,7 @@ function Member ({ bestNumber, className, info: { accountId, role }, isPrime, is const { api } = useApi(); const { allAccounts } = useAccounts(); const info = useMemberInfo(accountId); - const { queueExtrinsic } = useContext(StatusContext); + const { queueExtrinsic } = useQueue(); const hasNotice = !!api.tx.alliance.giveRetirementNotice; const hasActions = useMemo( diff --git a/packages/page-bounties/src/Bounties.slow.spec.tsx b/packages/page-bounties/src/Bounties.slow.spec.tsx index 5622d10a8c..63be7d461d 100644 --- a/packages/page-bounties/src/Bounties.slow.spec.tsx +++ b/packages/page-bounties/src/Bounties.slow.spec.tsx @@ -10,7 +10,7 @@ import { ThemeProvider } from 'styled-components'; import BountiesApp from '@polkadot/app-bounties/index'; import { lightTheme } from '@polkadot/apps/themes'; -import { Api } from '@polkadot/react-api'; +import { ApiCtxRoot } from '@polkadot/react-api'; import { createApi } from '@polkadot/test-support/api'; import { MemoryStore } from '@polkadot/test-support/keyring'; import { aliceSigner } from '@polkadot/test-support/keyring/signers'; @@ -27,7 +27,7 @@ const renderBounties = () => { - {
- + diff --git a/packages/page-bounties/src/Bounties.spec.ts b/packages/page-bounties/src/Bounties.spec.ts index 650b56e16e..aa5c03dc2e 100644 --- a/packages/page-bounties/src/Bounties.spec.ts +++ b/packages/page-bounties/src/Bounties.spec.ts @@ -60,7 +60,9 @@ describe('Bounties', () => { beforeAll(async () => { await i18next.changeLanguage('en'); + keyring.loadAll({ isDevelopment: true, store: new MemoryStore() }); + augmentedApi = createAugmentedApi(); ({ aBounty, aBountyIndex, bountyStatusWith, bountyWith } = new BountyFactory(augmentedApi)); ({ aProposal } = proposalFactory(augmentedApi)); @@ -347,7 +349,6 @@ describe('Bounties', () => { bountiesPage.renderOne(bounty); await bountiesPage.openRejectCuratorRole(); - await bountiesPage.clickButton('Reject'); bountiesPage.expectExtrinsicQueued({ accountId: bob }); @@ -359,7 +360,6 @@ describe('Bounties', () => { bountiesPage.renderOne(bounty); await bountiesPage.openExtraActions(); - await bountiesPage.expectText('Give up'); await bountiesPage.expectText('Slash curator (Council)'); }); diff --git a/packages/page-bounties/src/BountyExtraActions/index.tsx b/packages/page-bounties/src/BountyExtraActions/index.tsx index 4203438deb..7eeb6b3855 100644 --- a/packages/page-bounties/src/BountyExtraActions/index.tsx +++ b/packages/page-bounties/src/BountyExtraActions/index.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; -import { Button, Menu, Popup } from '@polkadot/react-components'; +import { Menu, Popup } from '@polkadot/react-components'; import { useCollectiveMembers, useToggle } from '@polkadot/react-hooks'; import { BlockNumber, BountyIndex, BountyStatus } from '@polkadot/types/interfaces'; @@ -37,10 +37,9 @@ function Index ({ bestNumber, className, description, index, proposals, status } const { t } = useTranslation(); const { isMember } = useCollectiveMembers('council'); const { curator, updateDue } = useBountyStatus(status); + const { isCurator, roles } = useUserRole(curator); const blocksUntilUpdate = useMemo(() => updateDue?.sub(bestNumber), [bestNumber, updateDue]); - - const { isCurator, roles } = useUserRole(curator); const availableSlashActions = determineUnassignCuratorAction(roles, status, blocksUntilUpdate); const slashCuratorActionNames = useRef>({ @@ -126,13 +125,13 @@ function Index ({ bestNumber, className, description, index, proposals, status } - {showCloseBounty && - ('Close')} - onClick={toggleCloseBounty} - /> - } + {showCloseBounty && ( + ('Close')} + onClick={toggleCloseBounty} + /> + )} {showRejectCurator && ( } - > - ); } @@ -129,10 +131,6 @@ export default React.memo(styled(Button)` background: transparent; } - .ui--Button-spinner { - visibility: hidden; - } - .ui--Button-overlay { background: rgba(253, 252, 251, 0.75); bottom: 0; @@ -152,12 +150,6 @@ export default React.memo(styled(Button)` width: 1rem; } - &.isBusy { - .ui--Button-spinner { - visibility: visible; - } - } - &.isDisabled { color: #bcbbba; } diff --git a/packages/react-components/src/CopyButton.tsx b/packages/react-components/src/CopyButton.tsx index 9b85604f40..9206cd7248 100644 --- a/packages/react-components/src/CopyButton.tsx +++ b/packages/react-components/src/CopyButton.tsx @@ -3,11 +3,12 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import styled from 'styled-components'; -import StatusContext from './Status/Context'; +import { useQueue } from '@polkadot/react-hooks'; + import Button from './Button'; import { useTranslation } from './translate'; @@ -25,7 +26,7 @@ const NOOP = () => undefined; function CopyButton ({ children, className = '', icon = 'copy', label, type, value }: Props): React.ReactElement { const { t } = useTranslation(); - const { queueAction } = useContext(StatusContext); + const { queueAction } = useQueue(); const _onCopy = useCallback( (): void => { diff --git a/packages/react-components/src/IdentityIcon/index.tsx b/packages/react-components/src/IdentityIcon/index.tsx index 2f58e3533b..374c7b83a9 100644 --- a/packages/react-components/src/IdentityIcon/index.tsx +++ b/packages/react-components/src/IdentityIcon/index.tsx @@ -4,16 +4,15 @@ import type { IdentityProps } from '@polkadot/react-identicon/types'; import type { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { getSystemIcon } from '@polkadot/apps-config'; import { ThemeProps } from '@polkadot/react-components/types'; -import { useApi } from '@polkadot/react-hooks'; +import { useApi, useQueue } from '@polkadot/react-hooks'; import BaseIdentityIcon from '@polkadot/react-identicon'; import { settings } from '@polkadot/ui-settings'; -import StatusContext from '../Status/Context'; import { useTranslation } from '../translate'; import RoboHash from './RoboHash'; @@ -36,7 +35,7 @@ function isCodec (value?: AccountId | AccountIndex | Address | string | Uint8Arr function IdentityIcon ({ className = '', prefix, size = 24, theme, value }: Props): React.ReactElement { const { isEthereum, specName, systemName } = useApi(); const { t } = useTranslation(); - const { queueAction } = useContext(StatusContext); + const { queueAction } = useQueue(); const thisTheme = theme || getIdentityTheme(systemName, specName); const Custom = thisTheme === 'robohash' ? RoboHash diff --git a/packages/react-components/src/InputTags.tsx b/packages/react-components/src/InputTags.tsx index dd9e6aed2b..3ba900ed42 100644 --- a/packages/react-components/src/InputTags.tsx +++ b/packages/react-components/src/InputTags.tsx @@ -1,12 +1,12 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; + import React, { useContext } from 'react'; import store from 'store'; import styled, { ThemeContext } from 'styled-components'; -import { ThemeDef } from '@polkadot/react-components/types'; - import Dropdown from './Dropdown'; interface Option { diff --git a/packages/react-components/src/Modal/Base.tsx b/packages/react-components/src/Modal/Base.tsx index bca13ef507..027371c09e 100644 --- a/packages/react-components/src/Modal/Base.tsx +++ b/packages/react-components/src/Modal/Base.tsx @@ -12,7 +12,7 @@ import { useTheme } from '@polkadot/react-hooks'; import Header from './Header'; function Base (props: ModalProps): React.ReactElement { - const theme = useTheme(); + const { themeClassName } = useTheme(); const { children, className = '', header, onClose, size = 'medium', testId = 'modal' } = props; const listenKeyboard = useCallback((event: KeyboardEvent) => { @@ -32,7 +32,7 @@ function Base (props: ModalProps): React.ReactElement { return createPortal(
diff --git a/packages/react-components/src/Popup/Popup.spec.tsx b/packages/react-components/src/Popup/Popup.spec.tsx index 30461c5d84..772d6606f3 100644 --- a/packages/react-components/src/Popup/Popup.spec.tsx +++ b/packages/react-components/src/Popup/Popup.spec.tsx @@ -76,7 +76,7 @@ async function expectPopupToBeOpen () { } async function togglePopup () { - fireEvent.click(await screen.findByRole('button')); + fireEvent.click(await screen.findByTestId('popup-open')); } async function clickOutside () { diff --git a/packages/react-components/src/Popup/PopupWindow.tsx b/packages/react-components/src/Popup/PopupWindow.tsx index 888caef1a6..5f6acb2a2f 100644 --- a/packages/react-components/src/Popup/PopupWindow.tsx +++ b/packages/react-components/src/Popup/PopupWindow.tsx @@ -3,21 +3,29 @@ import type { PopupWindowProps } from './types'; -import React from 'react'; +import React, { useMemo } from 'react'; import { createPortal } from 'react-dom'; import styled, { css } from 'styled-components'; import { usePopupWindow } from '@polkadot/react-hooks/usePopupWindow'; function PopupWindow ({ children, className = '', position, triggerRef, windowRef }: PopupWindowProps) { - const { renderWindowPosition, verticalPosition } = usePopupWindow(windowRef, triggerRef, position); + const { pointerStyle, renderCoords } = usePopupWindow(windowRef, triggerRef, position); + + const style = useMemo( + () => ({ + transform: `translate3d(${renderCoords.x}px, ${renderCoords.y}px, 0)`, + zIndex: 1000 + }), + [renderCoords] + ); return createPortal(
{children}
, @@ -26,26 +34,24 @@ function PopupWindow ({ children, className = '', position, triggerRef, windowRe } export default React.memo(styled(PopupWindow)` - position: absolute; - top:0; - left:0; - z-index: -1; - + background-color: var(--bg-menu); + border: 1px solid #d4d4d5; + border-radius: 4px; + box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%); + color: var(--color-text); + left: 0; margin: 0.7rem 0; padding: 0; - - color: var(--color-text); - background-color: var(--bg-menu); - border-radius: 4px; - border: 1px solid #d4d4d5; - box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%); + position: absolute; + top: 0; + z-index: -1; &::before { + bottom: -0.45rem; + box-shadow: 1px 1px 0 0 #bababc; position: absolute; right: 50%; top: unset; - bottom: -0.45rem; - box-shadow: 1px 1px 0 0 #bababc; ${({ position }) => position === 'left' && css` left: unset; @@ -58,7 +64,6 @@ export default React.memo(styled(PopupWindow)` `} content: ''; - background-color: var(--bg-menu); width: 1rem; @@ -67,7 +72,7 @@ export default React.memo(styled(PopupWindow)` z-index: 2; } - &.pointerBottom::before { + &.bottomPointer::before { box-shadow: -1px -1px 0 0 #bababc; top: -0.45rem; diff --git a/packages/react-components/src/Popup/index.tsx b/packages/react-components/src/Popup/index.tsx index 2499575bd2..b5d6798c9d 100644 --- a/packages/react-components/src/Popup/index.tsx +++ b/packages/react-components/src/Popup/index.tsx @@ -3,7 +3,7 @@ import type { PopupProps } from './types'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { Button } from '@polkadot/react-components/index'; @@ -11,14 +11,23 @@ import { useOutsideClick, useTheme, useToggle } from '@polkadot/react-hooks'; import PopupWindow from './PopupWindow'; -function Popup ({ children, className = '', closeOnScroll = false, isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) { - const theme = useTheme(); +function Popup ({ children, className = '', closeOnScroll, isDisabled, onCloseAction, position = 'left', value }: PopupProps) { + const { themeClassName } = useTheme(); const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false); const triggerRef = useRef(null); const dropdownRef = useRef(null); - const closeWindow = useCallback(() => setIsOpen(false), [setIsOpen]); - useOutsideClick([triggerRef, dropdownRef], closeWindow); + const closeWindow = useCallback( + () => setIsOpen(false), + [setIsOpen] + ); + + const refs = useMemo( + () => [triggerRef, dropdownRef], + [triggerRef, dropdownRef] + ); + + useOutsideClick(refs, closeWindow); useEffect(() => { if (closeOnScroll) { @@ -35,7 +44,7 @@ function Popup ({ children, className = '', closeOnScroll = false, isDisabled = }, [isOpen, onCloseAction]); return ( -
+
{isOpen && ( )}
diff --git a/packages/react-components/src/Popup/utils.ts b/packages/react-components/src/Popup/utils.ts index 0e048d521c..4a64d09270 100644 --- a/packages/react-components/src/Popup/utils.ts +++ b/packages/react-components/src/Popup/utils.ts @@ -1,11 +1,14 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { WindowSize } from '@polkadot/react-hooks/ctx/types'; import type { ElementPosition } from '@polkadot/react-hooks/useElementPosition'; +import type { HorizontalPosition, VerticalPosition } from './types'; -import { WindowSize } from '@polkadot/react-hooks/useWindowSize'; - -import { HorizontalPosition, VerticalPosition } from './types'; +interface Coords { + x: number; + y: number; +} // 0.8rem const POINTER_OFFSET = 14 * 0.8; @@ -14,14 +17,7 @@ const MENU_BAR_HEIGHT = 56; // adds 1rem margin between browsers window and popup const FIXED_VERTICAL_OFFSET_MARGIN = 14; -export function getPosition ( - triggerPosition: ElementPosition, - positionX: HorizontalPosition, - positionY: VerticalPosition, - windowPosition: ElementPosition, - scrollY: number, - windowSize: WindowSize -): {x: number, y: number} { +export function getPosition (triggerPosition: ElementPosition, positionX: HorizontalPosition, positionY: VerticalPosition, windowPosition: ElementPosition, scrollY: number, windowSize: WindowSize): Coords { const globalX = triggerPosition.x + (triggerPosition.width / 2); const globalY = triggerPosition.y + scrollY + (triggerPosition.height / 2); diff --git a/packages/react-components/src/Status/Context.ts b/packages/react-components/src/Status/Context.ts deleted file mode 100644 index ddc4deef79..0000000000 --- a/packages/react-components/src/Status/Context.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2017-2023 @polkadot/react-components authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import type { QueueProps, QueueStatus, QueueTx } from './types'; - -import React from 'react'; - -const defaultState: Partial = { - stqueue: [] as QueueStatus[], - txqueue: [] as QueueTx[] -}; - -const StatusContext: React.Context = React.createContext(defaultState as QueueProps); -const QueueConsumer: React.Consumer = StatusContext.Consumer; -const QueueProvider: React.Provider = StatusContext.Provider; - -export default StatusContext; - -export { - QueueConsumer, - QueueProvider -}; diff --git a/packages/react-components/src/Status/index.tsx b/packages/react-components/src/Status/index.tsx index 74a6c76501..e38ddadb7b 100644 --- a/packages/react-components/src/Status/index.tsx +++ b/packages/react-components/src/Status/index.tsx @@ -4,16 +4,15 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; import type { QueueStatus, QueueTx, QueueTxStatus } from './types'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; +import { useQueue } from '@polkadot/react-hooks'; + import AddressMini from '../AddressMini'; import Icon from '../Icon'; import Spinner from '../Spinner'; import { STATUS_COMPLETE } from './constants'; -import StatusContext from './Context'; - -export { StatusContext }; interface Props { className?: string; @@ -155,7 +154,7 @@ function filterTx (txqueue?: QueueTx[]): QueueTx[] { } function Status ({ className = '' }: Props): React.ReactElement | null { - const { stqueue, txqueue } = useContext(StatusContext); + const { stqueue, txqueue } = useQueue(); const [allSt, setAllSt] = useState([]); const [allTx, setAllTx] = useState([]); diff --git a/packages/react-components/src/Tabs/index.tsx b/packages/react-components/src/Tabs/index.tsx index 9477c0bca7..c4955be7be 100644 --- a/packages/react-components/src/Tabs/index.tsx +++ b/packages/react-components/src/Tabs/index.tsx @@ -12,7 +12,7 @@ import CurrentSection from './CurrentSection'; import Tab from './Tab'; import Delimiter from './TabsSectionDelimiter'; -export const TabsContext = React.createContext({}); +export const TabsCtx = React.createContext({}); interface Props { className?: string; @@ -40,7 +40,7 @@ function redirect (basePath: string, location: Location, items: TabItem[], hidde function Tabs ({ basePath, className = '', hidden, items }: Props): React.ReactElement { const location = useLocation(); - const { icon, text } = React.useContext(TabsContext); + const { icon, text } = React.useContext(TabsCtx); useEffect( () => redirect(basePath, location, items, hidden), diff --git a/packages/react-components/src/Tag.tsx b/packages/react-components/src/Tag.tsx index e8a18b58aa..12aa8ac77f 100644 --- a/packages/react-components/src/Tag.tsx +++ b/packages/react-components/src/Tag.tsx @@ -1,11 +1,12 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { FlagColor as TagColor } from '@polkadot/react-components/types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; + import React, { useContext, useState } from 'react'; import styled, { ThemeContext } from 'styled-components'; -import { FlagColor as TagColor, ThemeDef } from '@polkadot/react-components/types'; - import Tooltip from './Tooltip'; interface Props { diff --git a/packages/react-components/src/TxButton.tsx b/packages/react-components/src/TxButton.tsx index 77fb6876a3..7c6c1b4013 100644 --- a/packages/react-components/src/TxButton.tsx +++ b/packages/react-components/src/TxButton.tsx @@ -4,20 +4,19 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { TxButtonProps as Props } from './types'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { SubmittableResult } from '@polkadot/api'; -import { useIsMountedRef } from '@polkadot/react-hooks'; +import { useIsMountedRef, useQueue } from '@polkadot/react-hooks'; import { assert, isFunction } from '@polkadot/util'; import Button from './Button'; -import { StatusContext } from './Status'; import { useTranslation } from './translate'; function TxButton ({ accountId, className = '', extrinsic: propsExtrinsic, icon, isBasic, isBusy, isDisabled, isIcon, isToplevel, isUnsigned, label, onClick, onFailed, onSendRef, onStart, onSuccess, onUpdate, params, tooltip, tx, withSpinner, withoutLink }: Props): React.ReactElement { const { t } = useTranslation(); const mountedRef = useIsMountedRef(); - const { queueExtrinsic } = useContext(StatusContext); + const { queueExtrinsic } = useQueue(); const [isSending, setIsSending] = useState(false); const [isStarted, setIsStarted] = useState(false); diff --git a/packages/react-components/src/index.tsx b/packages/react-components/src/index.tsx index f1a09c3cf5..59d24c2dc5 100644 --- a/packages/react-components/src/index.tsx +++ b/packages/react-components/src/index.tsx @@ -10,7 +10,6 @@ export { default as AddressMini } from './AddressMini'; export { default as AddressRow } from './AddressRow'; export { default as AddressSmall } from './AddressSmall'; export { default as AddressToggle } from './AddressToggle'; -export { default as ApiStats, ApiStatsContext } from './ApiStats'; export { default as Available } from './Available'; export { default as AvatarItem } from './AvatarItem'; export { default as Badge } from './Badge'; @@ -88,10 +87,10 @@ export { default as StakingBonded } from './StakingBonded'; export { default as StakingRedeemable } from './StakingRedeemable'; export { default as StakingUnbonding } from './StakingUnbonding'; export { default as Static } from './Static'; -export { default as Status, StatusContext } from './Status'; +export { default as Status } from './Status'; export { default as SummaryBox } from './SummaryBox'; export { default as Table } from './Table'; -export { default as Tabs, TabsContext } from './Tabs'; +export { default as Tabs, TabsCtx } from './Tabs'; export { default as Tag } from './Tag'; export { default as Tags } from './Tags'; export { default as Toggle } from './Toggle'; diff --git a/packages/react-components/src/styles/components.ts b/packages/react-components/src/styles/components.ts index d69b529270..bad223bf45 100644 --- a/packages/react-components/src/styles/components.ts +++ b/packages/react-components/src/styles/components.ts @@ -1,7 +1,7 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ThemeDef } from '../types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export default (_theme: ThemeDef): string => ` diff --git a/packages/react-components/src/styles/semantic.ts b/packages/react-components/src/styles/semantic.ts index ba5a04d0c7..4140feb1c7 100644 --- a/packages/react-components/src/styles/semantic.ts +++ b/packages/react-components/src/styles/semantic.ts @@ -1,7 +1,7 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ThemeDef } from '../types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export default (_theme: ThemeDef): string => ` diff --git a/packages/react-components/src/types.ts b/packages/react-components/src/types.ts index 06b481054f..b46ef6cce5 100644 --- a/packages/react-components/src/types.ts +++ b/packages/react-components/src/types.ts @@ -6,6 +6,7 @@ import type { WithTranslation } from 'react-i18next'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { Abi } from '@polkadot/api-contract'; import type { ActionStatus } from '@polkadot/react-components/Status/types'; +import type { ThemeDef } from '@polkadot/react-hooks/ctx/types'; import type { AccountId, Index } from '@polkadot/types/interfaces'; import type { TxCallback, TxFailedCallback } from './Status/types'; @@ -72,10 +73,6 @@ export interface NullContract { address: null; } -export interface ThemeDef { - theme: 'dark' | 'light'; -} - export interface ThemeProps { theme: ThemeDef; } diff --git a/packages/react-hooks/src/ctx/ApiStats.tsx b/packages/react-hooks/src/ctx/ApiStats.tsx new file mode 100644 index 0000000000..7e7e436dd5 --- /dev/null +++ b/packages/react-hooks/src/ctx/ApiStats.tsx @@ -0,0 +1,110 @@ +// Copyright 2017-2023 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { ProviderStats } from '@polkadot/rpc-provider/types'; +import type { ApiStats } from './types'; + +import React, { useEffect, useRef, useState } from 'react'; + +import { useApi } from '../useApi'; +import { useIsMountedRef } from '../useIsMountedRef'; + +interface Props { + children?: React.ReactNode; +} + +const MAX_NUM = 60; // 5 minutes +const INTERVAL = 5_000; +const EMPTY_STATE: ApiStats[] = []; + +function getStats (...apis: ApiPromise[]): { stats: ProviderStats, when: number } { + const stats: ProviderStats = { + active: { + requests: 0, + subscriptions: 0 + }, + total: { + bytesRecv: 0, + bytesSent: 0, + cached: 0, + errors: 0, + requests: 0, + subscriptions: 0, + timeout: 0 + } + }; + + if (apis.length === 0) { + return { stats, when: Date.now() }; + } else if (apis.length === 1) { + return { stats: apis[0].stats || stats, when: Date.now() }; + } + + for (let i = 0; i < apis.length; i++) { + if (apis[i]) { + const s = apis[i].stats; + + if (s) { + stats.active.requests += s.active.requests; + stats.active.subscriptions += s.active.subscriptions; + stats.total.bytesRecv += s.total.bytesRecv; + stats.total.bytesSent += s.total.bytesSent; + stats.total.cached += s.total.cached; + stats.total.errors += s.total.errors; + stats.total.requests += s.total.requests; + stats.total.subscriptions += s.total.subscriptions; + stats.total.timeout += s.total.timeout; + } + } + } + + return { + stats, + when: Date.now() + }; +} + +export const ApiStatsCtx = React.createContext(EMPTY_STATE); + +export function ApiStatsCtxRoot ({ children }: Props): React.ReactElement { + const { api } = useApi(); + const [stats, setStats] = useState(EMPTY_STATE); + const timerId = useRef | null>(null); + const mountedRef = useIsMountedRef(); + + useEffect((): () => void => { + function fireTimer (): void { + timerId.current = null; + + if (mountedRef.current) { + const curr = getStats(api); + + setStats((prev): ApiStats[] => { + if (prev.length === 0) { + return [curr]; + } + + return prev.length === MAX_NUM + ? prev.concat(curr).slice(-MAX_NUM) + : prev.concat(curr); + }); + + timerId.current = setTimeout(fireTimer, INTERVAL); + } + } + + fireTimer(); + + return (): void => { + timerId.current && clearTimeout(timerId.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/react-query/src/BlockAuthors.tsx b/packages/react-hooks/src/ctx/BlockAuthors.tsx similarity index 63% rename from packages/react-query/src/BlockAuthors.tsx rename to packages/react-hooks/src/ctx/BlockAuthors.tsx index 12c3b1ba7c..8060c97616 100644 --- a/packages/react-query/src/BlockAuthors.tsx +++ b/packages/react-hooks/src/ctx/BlockAuthors.tsx @@ -3,26 +3,13 @@ import type { HeaderExtended } from '@polkadot/api-derive/types'; import type { EraRewardPoints } from '@polkadot/types/interfaces'; +import type { BlockAuthors } from './types'; import React, { useEffect, useState } from 'react'; import { useApi, useCall } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; -// TODO update HeaderExtended in api-derive -export interface HeaderExtendedWithMapping extends HeaderExtended { - authorFromMapping?: string; -} - -export interface Authors { - byAuthor: Record; - eraPoints: Record; - lastBlockAuthors: string[]; - lastBlockNumber?: string; - lastHeader?: HeaderExtendedWithMapping; - lastHeaders: HeaderExtendedWithMapping[]; -} - interface Props { children: React.ReactNode; } @@ -31,27 +18,23 @@ const MAX_HEADERS = 75; const byAuthor: Record = {}; const eraPoints: Record = {}; -const BlockAuthorsContext: React.Context = React.createContext({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] }); -const ValidatorsContext: React.Context = React.createContext([]); -function BlockAuthorsBase ({ children }: Props): React.ReactElement { +const EMPTY_STATE: BlockAuthors = { byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] }; + +export const BlockAuthorsCtx = React.createContext(EMPTY_STATE); + +export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement { const { api, isApiReady } = useApi(); const queryPoints = useCall(isApiReady && api.derive.staking?.currentPoints); - const [state, setState] = useState({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] }); - const [validators, setValidators] = useState([]); + const [state, setState] = useState(EMPTY_STATE); + // No unsub, global context - destroyed on app close useEffect((): void => { - // No unsub, global context - destroyed on app close api.isReady.then((): void => { - let lastHeaders: HeaderExtendedWithMapping[] = []; + let lastHeaders: HeaderExtended[] = []; let lastBlockAuthors: string[] = []; let lastBlockNumber = ''; - // subscribe to all validators - api.query.session && api.query.session.validators((validatorIds): void => { - setValidators(validatorIds.map((validatorId) => validatorId.toString())); - }).catch(console.error); - // subscribe to new headers api.derive.chain.subscribeNewHeads((lastHeader: HeaderExtended): void => { if (lastHeader?.number) { @@ -77,7 +60,7 @@ function BlockAuthorsBase ({ children }: Props): React.ReactElement { lastHeaders = lastHeaders .filter((old, index) => index < MAX_HEADERS && old.number.unwrap().lt(blockNumber)) - .reduce((next, header): HeaderExtendedWithMapping[] => { + .reduce((next, header): HeaderExtended[] => { next.push(header); return next; @@ -111,14 +94,8 @@ function BlockAuthorsBase ({ children }: Props): React.ReactElement { }, [queryPoints]); return ( - - - {children} - - + + {children} + ); } - -const BlockAuthors = React.memo(BlockAuthorsBase); - -export { BlockAuthorsContext, BlockAuthors, ValidatorsContext }; diff --git a/packages/react-query/src/Events.tsx b/packages/react-hooks/src/ctx/BlockEvents.tsx similarity index 83% rename from packages/react-query/src/Events.tsx rename to packages/react-hooks/src/ctx/BlockEvents.tsx index 5d2e59f283..ca73115757 100644 --- a/packages/react-query/src/Events.tsx +++ b/packages/react-hooks/src/ctx/BlockEvents.tsx @@ -4,18 +4,15 @@ import type { ApiPromise } from '@polkadot/api'; import type { Vec } from '@polkadot/types'; import type { EventRecord } from '@polkadot/types/interfaces'; -import type { IndexedEvent, KeyedEvent } from './types'; +import type { BlockEvents, IndexedEvent, KeyedEvent } from './types'; import React, { useEffect, useRef, useState } from 'react'; -import { useApi, useCall } from '@polkadot/react-hooks'; import { stringify, stringToU8a } from '@polkadot/util'; import { xxhashAsHex } from '@polkadot/util-crypto'; -interface Events { - eventCount: number; - events: KeyedEvent[]; -} +import { useApi } from '../useApi'; +import { useCall } from '../useCall'; interface Props { children: React.ReactNode; @@ -26,12 +23,12 @@ interface PrevHashes { event: string | null; } -const DEFAULT_EVENTS: Events = { eventCount: 0, events: [] }; +const DEFAULT_EVENTS: BlockEvents = { eventCount: 0, events: [] }; const MAX_EVENTS = 75; -const EventsContext: React.Context = React.createContext(DEFAULT_EVENTS); +export const BlockEventsCtx = React.createContext(DEFAULT_EVENTS); -async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec, setState: React.Dispatch>): Promise { +async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec, setState: React.Dispatch>): Promise { const newEvents: IndexedEvent[] = records .map((record, index) => ({ indexes: [index], record })) .filter(({ record: { event: { method, section } } }) => @@ -104,9 +101,9 @@ async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec { +export function BlockEventsCtxRoot ({ children }: Props): React.ReactElement { const { api, isApiReady } = useApi(); - const [state, setState] = useState(DEFAULT_EVENTS); + const [state, setState] = useState(DEFAULT_EVENTS); const records = useCall>(isApiReady && api.query.system.events); const prevHashes = useRef({ block: null, event: null }); @@ -115,12 +112,8 @@ function EventsBase ({ children }: Props): React.ReactElement { }, [api, prevHashes, records, setState]); return ( - + {children} - + ); } - -const Events = React.memo(EventsBase); - -export { EventsContext, Events }; diff --git a/packages/react-hooks/src/ctx/Keyring.tsx b/packages/react-hooks/src/ctx/Keyring.tsx new file mode 100644 index 0000000000..a292f9f07e --- /dev/null +++ b/packages/react-hooks/src/ctx/Keyring.tsx @@ -0,0 +1,130 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; +import type { Accounts, Addresses } from './types'; + +import React, { useEffect, useState } from 'react'; +import { combineLatest, map } from 'rxjs'; + +import { keyring } from '@polkadot/ui-keyring'; +import { u8aToHex } from '@polkadot/util'; +import { decodeAddress } from '@polkadot/util-crypto'; + +interface Props { + children?: React.ReactNode; +} + +interface State { + accounts: Accounts; + addresses: Addresses; +} + +const EMPTY_IS = () => false; + +const EMPTY: State = { + accounts: { allAccounts: [], allAccountsHex: [], areAccountsLoaded: false, hasAccounts: false, isAccount: EMPTY_IS }, + addresses: { allAddresses: [], allAddressesHex: [], areAddressesLoaded: false, hasAddresses: false, isAddress: EMPTY_IS } +}; + +export const KeyringCtx = React.createContext(EMPTY); + +/** + * @internal Helper function to dedupe a list of items, only adding it if + * + * 1. It is not already present in our list of results + * 2. It does not exist against a secondary list to check + * + * The first check ensures that we never have dupes in the original. The second + * ensures that e.g. an address is not also available as an account + **/ +function filter (items: string[], others: string[] = []): string[] { + return items.reduce((result, a) => { + if (!result.includes(a) && !others.includes(a)) { + result.push(a); + } + + return result; + }, []); +} + +/** + * @internal Helper function to convert a list of ss58 addresses into hex + **/ +function toHex (items: string[]): string[] { + return items + .map((a): string | null => { + try { + return u8aToHex(decodeAddress(a)); + } catch (error) { + // This is actually just a failsafe - the keyring really should + // not be passing through invalid ss58 values, but never say never + console.error(`Unable to convert address ${a} to hex`, (error as Error).message); + + return null; + } + }) + .filter((a): a is string => !!a); +} + +/** + * @internal Helper to create an is{Account, Address} check + **/ +function createCheck (items: string[]): Accounts['isAccount'] { + return (a?: string | null | { toString: () => string }): boolean => + !!a && items.includes(a.toString()); +} + +function extractAccounts (accounts: SubjectInfo = {}): Accounts { + const allAccounts = filter(Object.keys(accounts)); + + return { + allAccounts, + allAccountsHex: toHex(allAccounts), + areAccountsLoaded: true, + hasAccounts: allAccounts.length !== 0, + isAccount: createCheck(allAccounts) + }; +} + +function extractAddresses (addresses: SubjectInfo = {}, accounts: string[]): Addresses { + const allAddresses = filter(Object.keys(addresses), accounts); + + return { + allAddresses, + allAddressesHex: toHex(allAddresses), + areAddressesLoaded: true, + hasAddresses: allAddresses.length !== 0, + isAddress: createCheck(allAddresses) + }; +} + +export function KeyringCtxRoot ({ children }: Props): React.ReactElement { + const [state, setState] = useState(EMPTY); + + useEffect((): () => void => { + const sub = combineLatest([ + keyring.accounts.subject.pipe( + map((accInfo) => extractAccounts(accInfo)) + ), + keyring.addresses.subject + ]) + .pipe( + map(([accounts, addrInfo]): State => ({ + accounts, + addresses: extractAddresses(addrInfo, accounts.allAccounts) + })) + ) + .subscribe((state) => setState(state)); + + return (): void => { + sub && sub.unsubscribe(); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/react-components/src/Status/Queue.tsx b/packages/react-hooks/src/ctx/Queue.tsx similarity index 84% rename from packages/react-components/src/Status/Queue.tsx rename to packages/react-hooks/src/ctx/Queue.tsx index e9b5e41f2e..b983bb5c76 100644 --- a/packages/react-components/src/Status/Queue.tsx +++ b/packages/react-hooks/src/ctx/Queue.tsx @@ -2,21 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import type { SubmittableExtrinsic } from '@polkadot/api/promise/types'; +import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueProps, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from '@polkadot/react-components/Status/types'; import type { Bytes } from '@polkadot/types'; import type { DispatchError } from '@polkadot/types/interfaces'; import type { ITuple, Registry, SignerPayloadJSON } from '@polkadot/types/types'; -import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from './types'; -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { SubmittableResult } from '@polkadot/api'; +import { getDispatchError, getIncompleteMessage } from '@polkadot/react-components/Status/checks'; +import { STATUS_COMPLETE } from '@polkadot/react-components/Status/constants'; +import { getContractAbi } from '@polkadot/react-components/util'; import jsonrpc from '@polkadot/types/interfaces/jsonrpc'; -import { getContractAbi } from '../util'; -import { getDispatchError, getIncompleteMessage } from './checks'; -import { STATUS_COMPLETE } from './constants'; -import { QueueProvider } from './Context'; - export interface Props { children: React.ReactNode; } @@ -26,12 +24,19 @@ interface StatusCount { status: ActionStatusPartial; } +const EMPTY_STATE: Partial = { + stqueue: [] as QueueStatus[], + txqueue: [] as QueueTx[] +}; + let nextId = 0; const EVENT_MESSAGE = 'extrinsic event'; const REMOVE_TIMEOUT = 7500; const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic; +export const QueueCtx = React.createContext(EMPTY_STATE as QueueProps); + function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] { let others: ActionStatus | null = null; @@ -150,9 +155,12 @@ function extractEvents (result?: SubmittableResult): ActionStatus[] { ); } -function Queue ({ children }: Props): React.ReactElement { - const [stqueue, _setStQueue] = useState([]); - const [txqueue, _setTxQueue] = useState([]); +const EMPTY_QUEUE_ST: QueueStatus[] = []; +const EMPTY_QUEUE_TX: QueueTx[] = []; + +export function QueueCtxRoot ({ children }: Props): React.ReactElement { + const [stqueue, _setStQueue] = useState(EMPTY_QUEUE_ST); + const [txqueue, _setTxQueue] = useState(EMPTY_QUEUE_TX); const stRef = useRef(stqueue); const txRef = useRef(txqueue); @@ -279,21 +287,22 @@ function Queue ({ children }: Props): React.ReactElement { [queueAction, setTxQueue] ); + const value = useMemo( + () => ({ + queueAction, + queueExtrinsic, + queuePayload, + queueRpc, + queueSetTxStatus, + stqueue, + txqueue + }), + [queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue] + ); + return ( - + {children} - + ); } - -export default React.memo(Queue); diff --git a/packages/react-hooks/src/ctx/WindowSize.tsx b/packages/react-hooks/src/ctx/WindowSize.tsx new file mode 100644 index 0000000000..f1f200b333 --- /dev/null +++ b/packages/react-hooks/src/ctx/WindowSize.tsx @@ -0,0 +1,43 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Adapted from https://hackernoon.com/simplifying-responsive-layouts-with-react-hooks-19db73893a7a + +import type { WindowSize } from './types'; + +import React, { useEffect, useState } from 'react'; + +interface Props { + children: React.ReactNode; +} + +function getDimensions (): WindowSize { + return { + height: window.innerHeight, + width: window.innerWidth + }; +} + +export const WindowSizeCtx = React.createContext(getDimensions()); + +export function WindowSizeCtxRoot ({ children }: Props): React.ReactElement { + const [dimensions, setDimensions] = useState(() => getDimensions()); + + useEffect((): () => void => { + function handleResize (): void { + setDimensions(getDimensions()); + } + + window.addEventListener('resize', handleResize); + + return (): void => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/react-hooks/src/ctx/index.ts b/packages/react-hooks/src/ctx/index.ts new file mode 100644 index 0000000000..df279de24e --- /dev/null +++ b/packages/react-hooks/src/ctx/index.ts @@ -0,0 +1,9 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { ApiStatsCtxRoot } from './ApiStats'; +export { BlockAuthorsCtxRoot } from './BlockAuthors'; +export { BlockEventsCtxRoot } from './BlockEvents'; +export { KeyringCtxRoot } from './Keyring'; +export { QueueCtxRoot } from './Queue'; +export { WindowSizeCtxRoot } from './WindowSize'; diff --git a/packages/react-hooks/src/ctx/types.ts b/packages/react-hooks/src/ctx/types.ts new file mode 100644 index 0000000000..d066655d74 --- /dev/null +++ b/packages/react-hooks/src/ctx/types.ts @@ -0,0 +1,65 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HeaderExtended } from '@polkadot/api-derive/types'; +import type { ProviderStats } from '@polkadot/rpc-provider/types'; +import type { BlockNumber, EventRecord } from '@polkadot/types/interfaces'; + +export interface Accounts { + allAccounts: string[]; + allAccountsHex: string[]; + areAccountsLoaded: boolean; + hasAccounts: boolean; + isAccount: (address?: string | null | { toString: () => string }) => boolean; +} + +export interface Addresses { + allAddresses: string[]; + allAddressesHex: string[]; + areAddressesLoaded: boolean; + hasAddresses: boolean; + isAddress: (address?: string | null | { toString: () => string }) => boolean; +} + +export interface ApiStats { + stats: ProviderStats; + when: number; +} + +export interface BlockAuthors { + byAuthor: Record; + eraPoints: Record; + lastBlockAuthors: string[]; + lastBlockNumber?: string; + lastHeader?: HeaderExtended; + lastHeaders: HeaderExtended[]; +} + +export interface BlockEvents { + eventCount: number; + events: KeyedEvent[]; +} + +export interface IndexedEvent { + indexes: number[]; + record: EventRecord; +} + +export interface KeyedEvent extends IndexedEvent { + blockHash?: string; + blockNumber?: BlockNumber; + key: string; +} + +export type SidebarState = [string | null, (() => void) | null]; + +export type Sidebar = undefined | (([address, onUpdateName]: SidebarState) => void); + +export interface ThemeDef { + theme: 'dark' | 'light'; +} + +export interface WindowSize { + height: number; + width: number; +} diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 5bcf437ef4..51fb012cc8 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -1,17 +1,22 @@ // Copyright 2017-2023 @polkadot/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 +export * from './ctx'; + export { createNamedHook } from './createNamedHook'; export { useAccountId } from './useAccountId'; export { useAccountInfo } from './useAccountInfo'; export { useAccounts } from './useAccounts'; export { useAddresses } from './useAddresses'; export { useApi } from './useApi'; +export { useApiStats } from './useApiStats'; export { useApiUrl } from './useApiUrl'; export { useAvailableSlashes } from './useAvailableSlashes'; export { useBalancesAll } from './useBalancesAll'; export { useBestHash } from './useBestHash'; export { useBestNumber } from './useBestNumber'; +export { useBlockAuthors } from './useBlockAuthors'; +export { useBlockEvents } from './useBlockEvents'; export { useBlockInterval } from './useBlockInterval'; export { useBlockTime } from './useBlockTime'; export { useBlocksPerDays } from './useBlocksPerDays'; @@ -38,6 +43,7 @@ export { useIpfsFetch } from './useIpfsFetch'; export { useIpfsLink } from './useIpfsLink'; export { useIsMountedRef } from './useIsMountedRef'; export { useJudgements } from './useJudgements'; +export { useKeyring } from './useKeyring'; export { useLedger } from './useLedger'; export { useLoadingDelay } from './useLoadingDelay'; export { useMapEntries } from './useMapEntries'; @@ -54,6 +60,7 @@ export { usePopupWindow } from './usePopupWindow'; export { useProxies } from './useProxies'; export { useIsParasLinked, useParaEndpoints } from './useParaEndpoints'; export { usePassword } from './usePassword'; +export { useQueue } from './useQueue'; export { useRegistrars } from './useRegistrars'; export { useSavedFlags } from './useSavedFlags'; export { useScroll } from './useScroll'; diff --git a/packages/react-hooks/src/useAccountInfo.ts b/packages/react-hooks/src/useAccountInfo.ts index 895e3f1b0e..72349bb6f5 100644 --- a/packages/react-hooks/src/useAccountInfo.ts +++ b/packages/react-hooks/src/useAccountInfo.ts @@ -5,18 +5,17 @@ import type { Nominations, ValidatorPrefs } from '@polkadot/types/interfaces'; import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types'; import type { AddressFlags, AddressIdentity, UseAccountInfo } from './types'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { keyring } from '@polkadot/ui-keyring'; import { isFunction, isHex } from '@polkadot/util'; import { createNamedHook } from './createNamedHook'; -import { useAccounts } from './useAccounts'; -import { useAddresses } from './useAddresses'; import { useApi } from './useApi'; import { useCall } from './useCall'; import { useDeriveAccountFlags } from './useDeriveAccountFlags'; import { useDeriveAccountInfo } from './useDeriveAccountInfo'; +import { useKeyring } from './useKeyring'; import { useToggle } from './useToggle'; const IS_NONE = { @@ -41,8 +40,7 @@ const IS_NONE = { function useAccountInfoImpl (value: string | null, isContract = false): UseAccountInfo { const { api } = useApi(); - const { isAccount } = useAccounts(); - const { isAddress } = useAddresses(); + const { accounts: { isAccount }, addresses: { isAddress } } = useKeyring(); const accountInfo = useDeriveAccountInfo(value); const accountFlags = useDeriveAccountFlags(value); const nominator = useCall(api.query.staking?.nominators, [value]); @@ -249,7 +247,7 @@ function useAccountInfoImpl (value: string | null, isContract = false): UseAccou const isEditing = useCallback(() => isEditingName || isEditingTags, [isEditingName, isEditingTags]); - return { + return useMemo(() => ({ accountIndex, flags, genesisHash, @@ -271,7 +269,7 @@ function useAccountInfoImpl (value: string | null, isContract = false): UseAccou tags, toggleIsEditingName, toggleIsEditingTags - }; + }), [accountIndex, flags, genesisHash, identity, isEditing, isEditingName, isEditingTags, meta, name, onForgetAddress, onSaveName, onSaveTags, onSetGenesisHash, setIsEditingName, setIsEditingTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags, value]); } export const useAccountInfo = createNamedHook('useAccountInfo', useAccountInfoImpl); diff --git a/packages/react-hooks/src/useAccounts.ts b/packages/react-hooks/src/useAccounts.ts index 8144bc7019..24d31e209f 100644 --- a/packages/react-hooks/src/useAccounts.ts +++ b/packages/react-hooks/src/useAccounts.ts @@ -1,51 +1,17 @@ // Copyright 2017-2023 @polkadot/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; +import type { Accounts } from './ctx/types'; -import { useEffect, useState } from 'react'; - -import { keyring } from '@polkadot/ui-keyring'; -import { nextTick, u8aToHex } from '@polkadot/util'; -import { decodeAddress } from '@polkadot/util-crypto'; +import { useContext } from 'react'; +import { KeyringCtx } from './ctx/Keyring'; import { createNamedHook } from './createNamedHook'; -import { useIsMountedRef } from './useIsMountedRef'; -export interface UseAccounts { - allAccounts: string[]; - allAccountsHex: string[]; - areAccountsLoaded: boolean - hasAccounts: boolean; - isAccount: (address?: string | null) => boolean; -} +function useAccountsImpl (): Accounts { + const { accounts } = useContext(KeyringCtx); -const EMPTY: UseAccounts = { allAccounts: [], allAccountsHex: [], areAccountsLoaded: false, hasAccounts: false, isAccount: () => false }; - -function extractAccounts (accounts: SubjectInfo = {}): UseAccounts { - const allAccounts = Object.keys(accounts); - const allAccountsHex = allAccounts.map((a) => u8aToHex(decodeAddress(a))); - const hasAccounts = allAccounts.length !== 0; - const isAccount = (address?: string | null) => !!address && allAccounts.includes(address); - - return { allAccounts, allAccountsHex, areAccountsLoaded: true, hasAccounts, isAccount }; -} - -function useAccountsImpl (): UseAccounts { - const mountedRef = useIsMountedRef(); - const [state, setState] = useState(EMPTY); - - useEffect((): () => void => { - const subscription = keyring.accounts.subject.subscribe((accounts = {}) => - mountedRef.current && setState(extractAccounts(accounts)) - ); - - return (): void => { - nextTick(() => subscription.unsubscribe()); - }; - }, [mountedRef]); - - return state; + return accounts; } export const useAccounts = createNamedHook('useAccounts', useAccountsImpl); diff --git a/packages/react-hooks/src/useAddresses.ts b/packages/react-hooks/src/useAddresses.ts index b3bf03762d..eea701756b 100644 --- a/packages/react-hooks/src/useAddresses.ts +++ b/packages/react-hooks/src/useAddresses.ts @@ -1,41 +1,17 @@ // Copyright 2017-2023 @polkadot/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { useEffect, useState } from 'react'; +import type { Addresses } from './ctx/types'; -import { keyring } from '@polkadot/ui-keyring'; -import { nextTick } from '@polkadot/util'; +import { useContext } from 'react'; +import { KeyringCtx } from './ctx/Keyring'; import { createNamedHook } from './createNamedHook'; -import { useIsMountedRef } from './useIsMountedRef'; -interface UseAddresses { - allAddresses: string[]; - hasAddresses: boolean; - isAddress: (address: string) => boolean; -} +function useAddressesImpl (): Addresses { + const { addresses } = useContext(KeyringCtx); -function useAddressesImpl (): UseAddresses { - const mountedRef = useIsMountedRef(); - const [state, setState] = useState({ allAddresses: [], hasAddresses: false, isAddress: () => false }); - - useEffect((): () => void => { - const subscription = keyring.addresses.subject.subscribe((addresses): void => { - if (mountedRef.current) { - const allAddresses = addresses ? Object.keys(addresses) : []; - const hasAddresses = allAddresses.length !== 0; - const isAddress = (address: string): boolean => allAddresses.includes(address.toString()); - - setState({ allAddresses, hasAddresses, isAddress }); - } - }); - - return (): void => { - nextTick(() => subscription.unsubscribe()); - }; - }, [mountedRef]); - - return state; + return addresses; } export const useAddresses = createNamedHook('useAddresses', useAddressesImpl); diff --git a/packages/react-hooks/src/useApi.ts b/packages/react-hooks/src/useApi.ts index c5349349fc..ceea2fb815 100644 --- a/packages/react-hooks/src/useApi.ts +++ b/packages/react-hooks/src/useApi.ts @@ -5,12 +5,12 @@ import type { ApiProps } from '@polkadot/react-api/types'; import { useContext } from 'react'; -import { ApiContext } from '@polkadot/react-api'; +import { ApiCtx } from '@polkadot/react-api'; import { createNamedHook } from './createNamedHook'; function useApiImpl (): ApiProps { - return useContext(ApiContext); + return useContext(ApiCtx); } export const useApi = createNamedHook('useApi', useApiImpl); diff --git a/packages/react-hooks/src/useApiStats.ts b/packages/react-hooks/src/useApiStats.ts new file mode 100644 index 0000000000..643222c196 --- /dev/null +++ b/packages/react-hooks/src/useApiStats.ts @@ -0,0 +1,15 @@ +// Copyright 2017-2023 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiStats } from './ctx/types'; + +import { useContext } from 'react'; + +import { ApiStatsCtx } from './ctx/ApiStats'; +import { createNamedHook } from './createNamedHook'; + +function useApiStatsImpl (): ApiStats[] { + return useContext(ApiStatsCtx); +} + +export const useApiStats = createNamedHook('useApiStats', useApiStatsImpl); diff --git a/packages/react-hooks/src/useBlockAuthors.ts b/packages/react-hooks/src/useBlockAuthors.ts new file mode 100644 index 0000000000..0aa5144e45 --- /dev/null +++ b/packages/react-hooks/src/useBlockAuthors.ts @@ -0,0 +1,15 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BlockAuthors } from './ctx/types'; + +import { useContext } from 'react'; + +import { BlockAuthorsCtx } from './ctx/BlockAuthors'; +import { createNamedHook } from './createNamedHook'; + +function useBlockAuthorsImpl (): BlockAuthors { + return useContext(BlockAuthorsCtx); +} + +export const useBlockAuthors = createNamedHook('useBlockAuthors', useBlockAuthorsImpl); diff --git a/packages/react-hooks/src/useBlockEvents.ts b/packages/react-hooks/src/useBlockEvents.ts new file mode 100644 index 0000000000..d4f13ebd65 --- /dev/null +++ b/packages/react-hooks/src/useBlockEvents.ts @@ -0,0 +1,15 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BlockEvents } from './ctx/types'; + +import { useContext } from 'react'; + +import { BlockEventsCtx } from './ctx/BlockEvents'; +import { createNamedHook } from './createNamedHook'; + +function useBlockEventsImpl (): BlockEvents { + return useContext(BlockEventsCtx); +} + +export const useBlockEvents = createNamedHook('useBlockEvents', useBlockEventsImpl); diff --git a/packages/react-hooks/src/useElementPosition.ts b/packages/react-hooks/src/useElementPosition.ts index fb855e0b92..7866392417 100644 --- a/packages/react-hooks/src/useElementPosition.ts +++ b/packages/react-hooks/src/useElementPosition.ts @@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react'; import { createNamedHook } from './createNamedHook'; +import { useIsMountedRef } from './useIsMountedRef'; import { useScroll } from './useScroll'; import { useWindowSize } from './useWindowSize'; @@ -16,11 +17,12 @@ export interface ElementPosition { function useElementPositionImpl (ref: React.MutableRefObject): ElementPosition | undefined { const [elementPosition, setElementPosition] = useState(); + const mountedRef = useIsMountedRef(); const windowSize = useWindowSize(); const scrollY = useScroll(); useEffect(() => { - if (ref && ref.current) { + if (mountedRef.current && ref && ref.current) { const { height, width, x, y } = ref.current.getBoundingClientRect(); setElementPosition({ @@ -30,7 +32,7 @@ function useElementPositionImpl (ref: React.MutableRefObject[], e: MouseEvent) { - return refs.find((ref) => ref.current && ref.current.contains(e.target as HTMLElement)); +import { createNamedHook } from './createNamedHook'; + +function isRefClicked (refs: React.RefObject[], e: MouseEvent): boolean { + return refs.some((r) => + r.current && + r.current.contains(e.target as HTMLElement) + ); } -export const useOutsideClick = (elements: RefObject[], callback: () => void): void => { - const handleClick = useCallback((e: MouseEvent) => { - if (elements.length && !getClickedElement(elements, e)) { - callback(); - } - }, [elements, callback]); +function useOutsideClickImpl (refs: RefObject[], callback: () => void): void { + const handleClick = useCallback( + (e: MouseEvent): void => { + if (refs.length && !isRefClicked(refs, e)) { + callback(); + } + }, + [refs, callback] + ); - useEffect(() => { + useEffect((): () => void => { document.addEventListener('click', handleClick, true); - return () => { + return (): void => { document.removeEventListener('click', handleClick, true); }; }, [handleClick, callback]); -}; +} + +export const useOutsideClick = createNamedHook('useOutsideClick', useOutsideClickImpl); diff --git a/packages/react-hooks/src/usePopupWindow.ts b/packages/react-hooks/src/usePopupWindow.ts index 97b18341ff..4c399007c2 100644 --- a/packages/react-hooks/src/usePopupWindow.ts +++ b/packages/react-hooks/src/usePopupWindow.ts @@ -17,34 +17,35 @@ interface Coord { } interface Result { - renderWindowPosition?: Coord; - verticalPosition: VerticalPosition | undefined + pointerStyle: VerticalPosition; + renderCoords: Coord; } -function usePopupWindowImpl (windowRef: React.RefObject, triggerRef: React.RefObject, position: HorizontalPosition): Result { - const [renderWindowPosition, setRenderWindowPosition] = useState(); - const [verticalPosition, setVerticalPosition] = useState(); - const windowPosition = useElementPosition(windowRef); - const triggerPosition = useElementPosition(triggerRef); +const COORD_0: Coord = { x: 0, y: 0 }; +function usePopupWindowImpl (windowRef: React.RefObject, triggerRef: React.RefObject, position: HorizontalPosition): Result { + const [renderCoords, setRenderCoords] = useState(COORD_0); + const [pointerStyle, setPointerStyle] = useState('top'); + const windowCoords = useElementPosition(windowRef); + const triggerCoords = useElementPosition(triggerRef); const scrollY = useScroll(); const windowSize = useWindowSize(); useEffect(() => { - if (windowSize && triggerPosition) { - setVerticalPosition((triggerPosition.y > windowSize.height / 2) ? 'top' : 'bottom'); + if (windowSize && triggerCoords) { + setPointerStyle((triggerCoords.y > windowSize.height / 2) ? 'top' : 'bottom'); } - }, [triggerPosition, windowSize]); + }, [triggerCoords, windowSize]); useEffect(() => { - if (windowPosition && triggerPosition && verticalPosition) { - setRenderWindowPosition(getPosition(triggerPosition, position, verticalPosition, windowPosition, scrollY, windowSize)); + if (windowCoords && triggerCoords) { + setRenderCoords(getPosition(triggerCoords, position, pointerStyle, windowCoords, scrollY, windowSize)); } - }, [position, scrollY, triggerPosition, verticalPosition, windowPosition, windowSize]); + }, [position, scrollY, triggerCoords, pointerStyle, windowCoords, windowSize]); return useMemo( - () => ({ renderWindowPosition, verticalPosition }), - [renderWindowPosition, verticalPosition] + () => ({ pointerStyle, renderCoords }), + [renderCoords, pointerStyle] ); } diff --git a/packages/react-hooks/src/useQueue.ts b/packages/react-hooks/src/useQueue.ts new file mode 100644 index 0000000000..5efcea8e1c --- /dev/null +++ b/packages/react-hooks/src/useQueue.ts @@ -0,0 +1,15 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { QueueProps } from '@polkadot/react-components/Status/types'; + +import { useContext } from 'react'; + +import { QueueCtx } from './ctx/Queue'; +import { createNamedHook } from './createNamedHook'; + +function useQueueImpl (): QueueProps { + return useContext(QueueCtx); +} + +export const useQueue = createNamedHook('useQueue', useQueueImpl); diff --git a/packages/react-hooks/src/useTheme.ts b/packages/react-hooks/src/useTheme.ts index fb8fb939fc..e2195ba7aa 100644 --- a/packages/react-hooks/src/useTheme.ts +++ b/packages/react-hooks/src/useTheme.ts @@ -1,19 +1,28 @@ // Copyright 2017-2023 @polkadot/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ThemeDef } from '@polkadot/react-components/types'; +import type { ThemeDef } from './ctx/types'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import { ThemeContext } from 'styled-components'; import { createNamedHook } from './createNamedHook'; -type ThemeClassName = `theme--${ThemeDef['theme']}`; +interface Theme { + theme: ThemeDef['theme']; + themeClassName: `theme--${ThemeDef['theme']}`; +} -function useThemeImpl (): ThemeClassName { +function useThemeImpl (): Theme { const { theme } = useContext(ThemeContext as React.Context); - return `theme--${theme}`; + return useMemo( + (): Theme => ({ + theme, + themeClassName: `theme--${theme}` + }), + [theme] + ); } export const useTheme = createNamedHook('useTheme', useThemeImpl); diff --git a/packages/react-hooks/src/useWindowSize.ts b/packages/react-hooks/src/useWindowSize.ts index 10155900d9..ed56fe3677 100644 --- a/packages/react-hooks/src/useWindowSize.ts +++ b/packages/react-hooks/src/useWindowSize.ts @@ -1,36 +1,15 @@ // Copyright 2017-2023 @polkadot/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { useEffect, useState } from 'react'; +import type { WindowSize } from './ctx/types'; +import { useContext } from 'react'; + +import { WindowSizeCtx } from './ctx/WindowSize'; import { createNamedHook } from './createNamedHook'; -export interface WindowSize { - width: number; - height: number; -} - function useWindowSizeImpl (): WindowSize { - const [windowSize, setWindowSize] = useState({ - height: 0, - width: 0 - }); - - useEffect(() => { - function handleResize () { - setWindowSize({ - height: window.innerHeight, - width: window.innerWidth - }); - } - - window.addEventListener('resize', handleResize); - handleResize(); - - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowSize; + return useContext(WindowSizeCtx); } export const useWindowSize = createNamedHook('useWindowSize', useWindowSizeImpl); diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index fd508e9d45..3002674414 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -4,14 +4,12 @@ export { default as Available } from './Available'; export { default as BalanceFree } from './BalanceFree'; export { default as BalanceVoting } from './BalanceVoting'; -export { BlockAuthors, BlockAuthorsContext, ValidatorsContext } from './BlockAuthors'; export { default as BlockToTime } from './BlockToTime'; export { default as Bonded } from './Bonded'; export { default as BestFinalized } from './BestFinalized'; export { default as BestNumber } from './BestNumber'; export { default as Chain } from './Chain'; export { default as Elapsed } from './Elapsed'; -export { Events, EventsContext } from './Events'; export { default as FormatBalance } from './FormatBalance'; export { default as LockedVote } from './LockedVote'; export { default as NodeName } from './NodeName'; diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts deleted file mode 100644 index 9c28266468..0000000000 --- a/packages/react-query/src/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2017-2023 @polkadot/react-query authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import type { BlockNumber, EventRecord } from '@polkadot/types/interfaces'; - -export interface IndexedEvent { - indexes: number[]; - record: EventRecord; -} - -export interface KeyedEvent extends IndexedEvent { - blockHash?: string; - blockNumber?: BlockNumber; - key: string; -} diff --git a/packages/react-signer/src/TxSigned.tsx b/packages/react-signer/src/TxSigned.tsx index 17bc5f99a6..a5e629d013 100644 --- a/packages/react-signer/src/TxSigned.tsx +++ b/packages/react-signer/src/TxSigned.tsx @@ -11,13 +11,13 @@ import type { Multisig, Timepoint } from '@polkadot/types/interfaces'; import type { HexString } from '@polkadot/util/types'; import type { AddressFlags, AddressProxy, QrState } from './types'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { ApiPromise } from '@polkadot/api'; import { web3FromSource } from '@polkadot/extension-dapp'; -import { Button, ErrorBoundary, Modal, Output, StatusContext, Toggle } from '@polkadot/react-components'; -import { useApi, useLedger, useToggle } from '@polkadot/react-hooks'; +import { Button, ErrorBoundary, Modal, Output, Toggle } from '@polkadot/react-components'; +import { useApi, useLedger, useQueue, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { assert, BN_ZERO, nextTick } from '@polkadot/util'; import { addressEq } from '@polkadot/util-crypto'; @@ -192,7 +192,7 @@ function TxSigned ({ className, currentItem, requestAddress }: Props): React.Rea const { t } = useTranslation(); const { api } = useApi(); const { getLedger } = useLedger(); - const { queueSetTxStatus } = useContext(StatusContext); + const { queueSetTxStatus } = useQueue(); const [flags, setFlags] = useState(() => tryExtract(requestAddress)); const [error, setError] = useState(null); const [{ isQrHashed, qrAddress, qrPayload, qrResolve }, setQrState] = useState(() => ({ isQrHashed: false, qrAddress: '', qrPayload: new Uint8Array() })); diff --git a/packages/react-signer/src/TxUnsigned.tsx b/packages/react-signer/src/TxUnsigned.tsx index e294f2f8d7..84a664b16e 100644 --- a/packages/react-signer/src/TxUnsigned.tsx +++ b/packages/react-signer/src/TxUnsigned.tsx @@ -4,10 +4,10 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { QueueTx, QueueTxMessageSetStatus } from '@polkadot/react-components/Status/types'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback } from 'react'; -import { Button, ErrorBoundary, Modal, StatusContext } from '@polkadot/react-components'; -import { useToggle } from '@polkadot/react-hooks'; +import { Button, ErrorBoundary, Modal } from '@polkadot/react-components'; +import { useQueue, useToggle } from '@polkadot/react-hooks'; import Transaction from './Transaction'; import { useTranslation } from './translate'; @@ -35,7 +35,7 @@ async function send (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: Que function TxUnsigned ({ className, currentItem }: Props): React.ReactElement | null { const { t } = useTranslation(); - const { queueSetTxStatus } = useContext(StatusContext); + const { queueSetTxStatus } = useQueue(); const [isRenderError, toggleRenderError] = useToggle(); const _onSend = useCallback( diff --git a/packages/react-signer/src/index.tsx b/packages/react-signer/src/index.tsx index 76e09ffb2f..db2aa47972 100644 --- a/packages/react-signer/src/index.tsx +++ b/packages/react-signer/src/index.tsx @@ -5,12 +5,12 @@ import type { QueueTx, QueueTxMessageSetStatus, QueueTxResult } from '@polkadot/ import type { BareProps as Props } from '@polkadot/react-components/types'; import type { DefinitionRpcExt } from '@polkadot/types/types'; -import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { ApiPromise } from '@polkadot/api'; -import { Modal, StatusContext } from '@polkadot/react-components'; -import { useApi } from '@polkadot/react-hooks'; +import { Modal } from '@polkadot/react-components'; +import { useApi, useQueue } from '@polkadot/react-hooks'; import { assert, isFunction, loggerFormat } from '@polkadot/util'; import { useTranslation } from './translate'; @@ -90,7 +90,7 @@ function extractCurrent (txqueue: QueueTx[]): ItemState { function Signer ({ children, className = '' }: Props): React.ReactElement { const { api } = useApi(); const { t } = useTranslation(); - const { queueSetTxStatus, txqueue } = useContext(StatusContext); + const { queueSetTxStatus, txqueue } = useQueue(); const { count, currentItem, isRpc, isVisible, requestAddress } = useMemo( () => extractCurrent(txqueue), diff --git a/packages/test-support/src/pages/Page.tsx b/packages/test-support/src/pages/Page.tsx index e597e42c86..cb80b08642 100644 --- a/packages/test-support/src/pages/Page.tsx +++ b/packages/test-support/src/pages/Page.tsx @@ -1,6 +1,9 @@ // Copyright 2017-2023 @polkadot/page-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ApiProps } from '@polkadot/react-api/types'; +import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types'; + import { queryByAttribute, render, RenderResult, screen } from '@testing-library/react'; import React, { Suspense } from 'react'; import { MemoryRouter } from 'react-router-dom'; @@ -9,10 +12,8 @@ import { ThemeProvider } from 'styled-components'; import AccountSidebar from '@polkadot/app-accounts/Sidebar'; import { lightTheme } from '@polkadot/apps/themes'; import { POLKADOT_GENESIS } from '@polkadot/apps-config'; -import { ApiContext } from '@polkadot/react-api'; -import { ApiProps } from '@polkadot/react-api/types'; -import { QueueProvider } from '@polkadot/react-components/Status/Context'; -import { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types'; +import { ApiCtx } from '@polkadot/react-api'; +import { QueueCtx } from '@polkadot/react-hooks/ctx/Queue'; import { UseAccountInfo } from '@polkadot/react-hooks/types'; import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks'; import { TypeRegistry } from '@polkadot/types/create'; @@ -104,7 +105,7 @@ jest.mock('@polkadot/react-hooks/useRegistrars', () => ({ })); jest.mock('@polkadot/react-hooks/useTheme', () => ({ - useTheme: () => 'theme--light' + useTheme: () => ({ theme: 'light', themeClassName: 'theme--light' }) })); export abstract class Page { @@ -208,17 +209,17 @@ export abstract class Page { <>
- + - + {React.cloneElement(this.overview, { onStatusChange: noop }) } - + - + ); diff --git a/packages/test-support/src/utils/accountDefaults.ts b/packages/test-support/src/utils/accountDefaults.ts index 9dc3e24dc5..8f712c7314 100644 --- a/packages/test-support/src/utils/accountDefaults.ts +++ b/packages/test-support/src/utils/accountDefaults.ts @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types'; +import type { Accounts } from '@polkadot/react-hooks/ctx/types'; import type { UseAccountInfo } from '@polkadot/react-hooks/types'; +import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types'; -import { UseAccounts } from '@polkadot/react-hooks/useAccounts'; -import { KeyringJson$Meta } from '@polkadot/ui-keyring/types'; import { BN } from '@polkadot/util'; import { balanceOf } from '../creation/balance'; @@ -33,7 +33,7 @@ export interface AccountOverrides { info?: Override; } -export const emptyAccounts: UseAccounts = { +export const emptyAccounts: Accounts = { allAccounts: [], allAccountsHex: [], areAccountsLoaded: true, @@ -89,7 +89,7 @@ export const defaultAccountInfo: UseAccountInfo = { } as any; class MockAccountHooks { - public useAccounts: UseAccounts = emptyAccounts; + public useAccounts: Accounts = emptyAccounts; public accountsMap: AccountsMap = {}; public nonce: BN = new BN(1);