Rework context usage (& move most to react-hooks) (#8731)

* Rework context usage (& move most to react-hooks)

* Move use{Accounts, Addresses} to use contexts

* Cleanups

* Global alignment renames

* Combine keyring subjects into KeyringCtx

* Cleanups to contexts

* Small adjustment to window size usage

* Popup test via data-testid

* Fix Bounties tests

* Move getStats to correct location

* Adjust ApiStats

* Do add unsubs (although unneeded)
This commit is contained in:
Jaco 2023-01-09 12:56:38 +02:00 committed by GitHub
parent 6718725aa3
commit 37f44367b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 908 additions and 778 deletions

View File

@ -20,7 +20,7 @@ import WarmUp from './WarmUp';
export const PORTAL_ID = 'portals';
function Apps ({ className = '' }: Props): React.ReactElement<Props> {
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<Props> {
return (
<>
<GlobalStyle uiHighlight={uiHighlight} />
<div className={`apps--Wrapper ${theme} ${className}`}>
<div className={`apps--Wrapper ${themeClassName} ${className}`}>
<Menu />
<AccountSidebar>
<Signer>

View File

@ -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: <T = string> (key: string, opts
}
function Status ({ optionsAll }: Props): React.ReactElement<Props> {
const { queueAction } = useContext(StatusContext);
const { queueAction } = useQueue();
const { api, isApiReady } = useApi();
const { allAccounts } = useAccounts();
const { t } = useTranslation();

View File

@ -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<Props> {
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<Props> {
<>
<Suspense fallback='...'>
<ErrorBoundary trigger={name}>
<TabsContext.Provider value={{ icon, text }}>
<TabsCtx.Provider value={{ icon, text }}>
{missingApis.length
? (
<NotFound
@ -88,7 +88,7 @@ function Content ({ className }: Props): React.ReactElement<Props> {
/>
)
}
</TabsContext.Provider>
</TabsCtx.Provider>
</ErrorBoundary>
</Suspense>
<Status />

View File

@ -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<Props> {
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 (
<Suspense fallback='...'>
<ThemeProvider theme={theme}>
<Queue>
<Api
apiUrl={settings.apiUrl}
isElectron={isElectron}
store={store}
>
<ApiStats>
<BlockAuthors>
<Events>
<HashRouter>
<WindowDimensions>
<Apps />
</WindowDimensions>
</HashRouter>
</Events>
</BlockAuthors>
</ApiStats>
</Api>
</Queue>
<KeyringCtxRoot>
<QueueCtxRoot>
<ApiCtxRoot
apiUrl={settings.apiUrl}
isElectron={isElectron}
store={store}
>
<ApiStatsCtxRoot>
<BlockAuthorsCtxRoot>
<BlockEventsCtxRoot>
<HashRouter>
<WindowSizeCtxRoot>
<Apps />
</WindowSizeCtxRoot>
</HashRouter>
</BlockEventsCtxRoot>
</BlockAuthorsCtxRoot>
</ApiStatsCtxRoot>
</ApiCtxRoot>
</QueueCtxRoot>
</KeyringCtxRoot>
</ThemeProvider>
</Suspense>
);

View File

@ -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<Dimensions>({ windowHeight: window.innerHeight, windowWidth: window.innerWidth });
function WindowDimensionsProvider ({ children }: Props): React.ReactElement<Props> {
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 (
<WindowDimensionsCtx.Provider value={dimensions}>
{children}
</WindowDimensionsCtx.Provider>
);
}
export default React.memo(WindowDimensionsProvider);
export { WindowDimensionsCtx };

View File

@ -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'

View File

@ -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<Props> | 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();

View File

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

View File

@ -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();

View File

@ -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(
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<Api
<ApiCtxRoot
apiUrl={`ws://127.0.0.1:${SUBSTRATE_PORT}`}
isElectron={false}
store={memoryStore}
@ -38,7 +38,7 @@ const renderAccounts = () => {
/>
</div>
</WaitForApi>
</Api>
</ApiCtxRoot>
</ThemeProvider>
</MemoryRouter>
);

View File

@ -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<Props> {
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 (
<>

View File

@ -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<ToggleContext> = React.createContext<ToggleContext>(undefined);
const EMPTY_STATE: State = [null, null];
export const AccountSidebarCtx = React.createContext<SetStateContext>(undefined);
function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
const [[address, onUpdateName], setAddress] = useState<State>([null, null]);
const [[address, onUpdateName], setAddress] = useState<State>(EMPTY_STATE);
const onClose = useCallback(
() => setAddress([null, null]),
@ -24,7 +26,7 @@ function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
);
return (
<AccountSidebarToggle.Provider value={setAddress}>
<AccountSidebarCtx.Provider value={setAddress}>
{children}
{address && (
<Sidebar
@ -34,10 +36,8 @@ function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
onUpdateName={onUpdateName}
/>
)}
</AccountSidebarToggle.Provider>
</AccountSidebarCtx.Provider>
);
}
export { AccountSidebarToggle };
export default React.memo(AccountSidebar);

View File

@ -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: '' });

View File

@ -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(

View File

@ -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 = () => {
<Suspense fallback='...'>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<Api
<ApiCtxRoot
apiUrl={`ws://127.0.0.1:${SUBSTRATE_PORT}`}
isElectron={false}
store={memoryStore}
@ -37,7 +37,7 @@ const renderBounties = () => {
<BountiesApp basePath='/bounties' />
</div>
</WaitForApi>
</Api>
</ApiCtxRoot>
</ThemeProvider>
</MemoryRouter>
</Suspense>

View File

@ -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)');
});

View File

@ -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<Record<ValidUnassignCuratorAction, string>>({
@ -126,13 +125,13 @@ function Index ({ bestNumber, className, description, index, proposals, status }
<Popup
value={
<Menu className='settings-menu'>
{showCloseBounty &&
<Menu.Item
key='closeBounty'
label={t<string>('Close')}
onClick={toggleCloseBounty}
/>
}
{showCloseBounty && (
<Menu.Item
key='closeBounty'
label={t<string>('Close')}
onClick={toggleCloseBounty}
/>
)}
{showRejectCurator && (
<Menu.Item
key='rejectCurator'
@ -163,13 +162,7 @@ function Index ({ bestNumber, className, description, index, proposals, status }
))}
</Menu>
}
>
<Button
dataTestId='extra-actions'
icon='ellipsis-v'
isReadOnly={false}
/>
</Popup>
/>
</div>
)
: null;

View File

@ -1,9 +1,10 @@
// Copyright 2017-2023 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
import type { PalletBountiesBounty } from '@polkadot/types/lookup';
import { fireEvent, render, within } from '@testing-library/react';
import { fireEvent, render, RenderResult, within } from '@testing-library/react';
import React, { Suspense } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
@ -14,10 +15,10 @@ import Bounties from '@polkadot/app-bounties/Bounties';
import { BountyApi } from '@polkadot/app-bounties/hooks';
import { lightTheme } from '@polkadot/apps/themes';
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
import { ApiContext } from '@polkadot/react-api';
import { ApiCtx } 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 { KeyringCtxRoot } from '@polkadot/react-hooks';
import { QueueCtx } from '@polkadot/react-hooks/ctx/Queue';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
import { TypeRegistry } from '@polkadot/types/create';
@ -65,6 +66,7 @@ export class BountiesPage {
getAllByRole?: GetMany;
findAllByTestId?: FindManyWithMatcher;
queryAllByText?: GetMany;
renderResult?: RenderResult;
constructor (api: ApiPromise) {
({ aBounty: this.aBounty, aBountyIndex: this.aBountyIndex, aBountyStatus: this.aBountyStatus, bountyStatusWith: this.bountyStatusWith, bountyWith: this.bountyWith } = new BountyFactory(api));
@ -75,7 +77,8 @@ export class BountiesPage {
}
renderMany (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}): RenderedBountiesPage {
const { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText } = this.renderBounties(bountyApi, { balance });
const renderResult = this.renderBounties(bountyApi, { balance });
const { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText } = renderResult;
this.findByRole = findByRole;
this.findByText = findByText;
@ -83,6 +86,7 @@ export class BountiesPage {
this.getAllByRole = getAllByRole;
this.findAllByTestId = findAllByTestId;
this.queryAllByText = queryAllByText;
this.renderResult = renderResult;
return { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText };
}
@ -119,15 +123,17 @@ export class BountiesPage {
<>
<div id='tooltips' />
<Suspense fallback='...'>
<QueueProvider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<Bounties />
</ApiContext.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueProvider>
<QueueCtx.Provider value={queue}>
<KeyringCtxRoot>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiCtx.Provider value={mockApi}>
<Bounties />
</ApiCtx.Provider>
</ThemeProvider>
</MemoryRouter>
</KeyringCtxRoot>
</QueueCtx.Provider>
</Suspense>
</>
);
@ -251,7 +257,7 @@ export class BountiesPage {
}
async openExtraActions (): Promise<void> {
await this.clickButtonByTestId('extra-actions');
await this.clickButtonByTestId('popup-open');
}
async openAcceptCuratorRole (): Promise<void> {

View File

@ -3,8 +3,15 @@
import { fireEvent } from '@testing-library/react';
export const clickElementWithTestId = async (testId: string, findByTestId: (testId: string) => Promise<HTMLElement>): Promise<void> => {
import { nextTick } from '@polkadot/util';
export async function clickElementWithTestId (testId: string, findByTestId: (testId: string) => Promise<HTMLElement>): Promise<void> {
const element = await findByTestId(testId);
// we have an issue with Popups... wait for rendering
await new Promise<void>((resolve): void => {
nextTick(resolve);
});
fireEvent.click(element);
};
}

View File

@ -3,8 +3,8 @@
import { fireEvent } from '@testing-library/react';
export const clickElementWithText = async (text: string, findByText: (name: string) => Promise<HTMLElement>): Promise<void> => {
export async function clickElementWithText (text: string, findByText: (name: string) => Promise<HTMLElement>): Promise<void> {
const element = await findByText(text);
fireEvent.click(element);
};
}

View File

@ -1,10 +1,10 @@
// Copyright 2017-2023 @polkadot/app-collator authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useRef } from 'react';
import React, { useRef } from 'react';
import { Table } from '@polkadot/react-components';
import { BlockAuthorsContext } from '@polkadot/react-query';
import { useBlockAuthors } from '@polkadot/react-hooks';
import Collator from './Collator';
import Summary from './Summary';
@ -18,7 +18,7 @@ interface Props {
function Collators ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const collators = useCollators();
const { byAuthor } = useContext(BlockAuthorsContext);
const { byAuthor } = useBlockAuthors();
const hdrRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t<string>('collators'), 'start', 2],

View File

@ -1,12 +1,13 @@
// Copyright 2017-2023 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Stats } from '@polkadot/react-components/ApiStats/types';
import type { ApiStats } from '@polkadot/react-hooks/ctx/types';
import React, { useContext, useMemo } from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { ApiStatsContext, CardSummary, Spinner, SummaryBox } from '@polkadot/react-components';
import { CardSummary, Spinner, SummaryBox } from '@polkadot/react-components';
import { useApiStats } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';
import Chart from '../Latency/Chart';
@ -32,7 +33,7 @@ interface ChartInfo {
const COLORS_BYTES = ['#00448c', '#008c44', '#acacac'];
const COLORS_REQUESTS = ['#008c8c', '#00448c', '#8c4400', '#acacac'];
function getPoints (all: Stats[]): ChartInfo {
function getPoints (all: ApiStats[]): ChartInfo {
const bytesChart: ChartContents = {
labels: [],
values: [[], [], []]
@ -85,7 +86,7 @@ function getPoints (all: Stats[]): ChartInfo {
function Api ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const stats = useContext(ApiStatsContext);
const stats = useApiStats();
const { bytesLegend, requestsLegend } = useMemo(
() => ({

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { EventRecord, RuntimeVersionPartial, SignedBlock } from '@polkadot/types/interfaces';
import React, { useEffect, useMemo, useState } from 'react';

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { BlockNumber, DispatchInfo, Extrinsic } from '@polkadot/types/interfaces';
import type { ICompact, INumber } from '@polkadot/types/types';

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { BlockNumber, Extrinsic } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import type { Balance, DispatchInfo, SignedBlock } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { HeaderExtended } from '@polkadot/api-derive/types';
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import React from 'react';

View File

@ -3,14 +3,13 @@
import type { TFunction } from 'i18next';
import type { TabItem } from '@polkadot/react-components/Tabs/types';
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import React, { useContext, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { Route, Switch } from 'react-router';
import { Tabs } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { BlockAuthorsContext, EventsContext } from '@polkadot/react-query';
import { useApi, useBlockAuthors, useBlockEvents } from '@polkadot/react-hooks';
import { isFunction } from '@polkadot/util';
import Api from './Api';
@ -75,8 +74,8 @@ function createItemsRef (t: TFunction): TabItem[] {
function ExplorerApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { lastHeaders } = useContext(BlockAuthorsContext);
const { eventCount, events } = useContext(EventsContext);
const { lastHeaders } = useBlockAuthors();
const { eventCount, events } = useBlockEvents();
const itemsRef = useRef(createItemsRef(t));
const pathRef = useRef(createPathRef(basePath));
const hidden = useState(() => isFunction(api.query.babe?.authorities) ? [] : ['forks']);

View File

@ -1,12 +1,14 @@
// Copyright 2017-2023 @polkadot/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ActionStatusBase } from '@polkadot/react-components/Status/types';
import FileSaver from 'file-saver';
import React, { useCallback, useContext, useRef, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import styled from 'styled-components';
import { Badge, Button, CopyButton, Icon, StatusContext, Table } from '@polkadot/react-components';
import { ActionStatusBase, QueueProps } from '@polkadot/react-components/Status/types';
import { Badge, Button, CopyButton, Icon, Table } from '@polkadot/react-components';
import { useQueue } from '@polkadot/react-hooks';
import { useFiles } from './hooks';
import { useTranslation } from './translate';
@ -62,7 +64,7 @@ export interface Props {
function CrustFiles ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { queueAction } = useContext<QueueProps>(StatusContext);
const { queueAction } = useQueue();
const [showUpMode, setShowUpMode] = useState(false);
const wFiles = useFiles();
const [file, setFile] = useState<FileInfo | undefined>(undefined);

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@polkadot/react-query/types';
import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types';
import React, { useRef } from 'react';

View File

@ -1,15 +1,15 @@
// Copyright 2017-2023 @polkadot/app-rpc authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useContext } from 'react';
import React from 'react';
import { StatusContext } from '@polkadot/react-components';
import { useQueue } from '@polkadot/react-hooks';
import Results from './Results';
import Selection from './Selection';
function RpcApp (): React.ReactElement {
const { queueRpc, txqueue } = useContext(StatusContext);
const { queueRpc, txqueue } = useQueue();
return (
<>

View File

@ -3,9 +3,10 @@
import type { KeypairType } from '@polkadot/util-crypto/types';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Dropdown, Input, MarkWarning, Modal, StatusContext } from '@polkadot/react-components';
import { Button, Dropdown, Input, MarkWarning, Modal } from '@polkadot/react-components';
import { useQueue } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { assert, u8aToHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicValidate } from '@polkadot/util-crypto';
@ -28,7 +29,7 @@ const EMPTY_KEY = '0x';
function InjectKeys ({ onClose }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { queueRpc } = useContext(StatusContext);
const { queueRpc } = useQueue();
// this needs to align with what is set as the first value in `type`
const [crypto, setCrypto] = useState<KeypairType>('sr25519');
const [publicKey, setPublicKey] = useState(EMPTY_KEY);

View File

@ -7,12 +7,12 @@ import type { PalletStakingUnappliedSlash } from '@polkadot/types/lookup';
import type { SortedTargets } from '../../types';
import type { Slash } from '../types';
import React, { useCallback, useContext, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, StatusContext, TxButton } from '@polkadot/react-components';
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, TxButton } from '@polkadot/react-components';
import { useApi, useCall, useQueue, useToggle } from '@polkadot/react-hooks';
import { BN, formatNumber, isFunction } from '@polkadot/util';
import { useTranslation } from '../../translate';
@ -65,7 +65,7 @@ function useStashCalls (api: ApiPromise, stashId: string) {
function Account ({ allSlashes, className = '', info: { controllerId, destination, hexSessionIdNext, hexSessionIdQueue, isLoading, isOwnController, isOwnStash, isStashNominating, isStashValidating, nominating, sessionIds, stakingLedger, stashId }, isDisabled, minCommission, targets }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { queueExtrinsic } = useContext(StatusContext);
const { queueExtrinsic } = useQueue();
const [isBondExtraOpen, toggleBondExtra] = useToggle();
const [isInjectOpen, toggleInject] = useToggle();
const [isKickOpen, toggleKick] = useToggle();

View File

@ -6,10 +6,10 @@ import type { PalletNominationPoolsPoolMember, PalletNominationPoolsPoolRoles }
import type { PoolInfo } from '../../Pools/types';
import type { SortedTargets } from '../../types';
import React, { useCallback, useContext, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { AddressSmall, Badge, Menu, Popup, StakingRedeemable, StakingUnbonding, StatusContext } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { AddressSmall, Badge, Menu, Popup, StakingRedeemable, StakingUnbonding } from '@polkadot/react-components';
import { useApi, useQueue, useToggle } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { BN, formatNumber } from '@polkadot/util';
@ -66,7 +66,7 @@ function Pool ({ accountId, className, info: { bonded: { roles }, metadata, nomi
const { t } = useTranslation();
const { api } = useApi();
const spanCount = useSlashingSpans(stashId);
const { queueExtrinsic } = useContext(StatusContext);
const { queueExtrinsic } = useQueue();
const [isBondOpen, toggleBond] = useToggle();
const [isNominateOpen, toggleNominate] = useToggle();
const [isUnbondOpen, toggleUnbond] = useToggle();

View File

@ -2,16 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import type { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
import type { Authors } from '@polkadot/react-query/BlockAuthors';
import type { AccountId } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { NominatedByMap, SortedTargets, ValidatorInfo } from '../types';
import React, { useContext, useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { useApi, useLoadingDelay } from '@polkadot/react-hooks';
import { BlockAuthorsContext } from '@polkadot/react-query';
import { useApi, useBlockAuthors, useLoadingDelay } from '@polkadot/react-hooks';
import Filtering from '../Filtering';
import Legend from '../Legend';
@ -20,6 +18,8 @@ import Address from './Address';
interface Props {
className?: string;
byAuthor: Record<string, string>;
eraPoints: Record<string, string>;
favorites: string[];
hasQueries: boolean;
isIntentions?: boolean;
@ -28,7 +28,7 @@ interface Props {
minCommission?: BN;
nominatedBy?: NominatedByMap;
ownStashIds?: string[];
paraValidators?: Record<string, boolean>;
paraValidators: Record<string, boolean>;
recentlyOnline?: DeriveHeartbeats;
setNominators?: (nominators: string[]) => void;
stakingOverview?: DeriveStakingOverview;
@ -43,8 +43,6 @@ interface Filtered {
waiting?: AccountExtend[];
}
const EmptyAuthorsContext: React.Context<Authors> = React.createContext<Authors>({ byAuthor: {}, eraPoints: {}, lastBlockAuthors: [], lastHeaders: [] });
function filterAccounts (isOwn: boolean, accounts: string[] = [], ownStashIds: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] {
return accounts
.filter((accountId) =>
@ -113,7 +111,7 @@ const DEFAULT_PARAS = {};
function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, minCommission, nominatedBy, ownStashIds, paraValidators = DEFAULT_PARAS, recentlyOnline, stakingOverview, targets, toggleFavorite }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { byAuthor, eraPoints } = useContext(isIntentions ? EmptyAuthorsContext : BlockAuthorsContext);
const { byAuthor, eraPoints } = useBlockAuthors();
const [nameFilter, setNameFilter] = useState<string>('');
// we have a very large list, so we use a loading delay
@ -196,7 +194,7 @@ function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, m
isElected={isElected}
isFavorite={isFavorite}
isMain={!isIntentions}
isPara={isIntentions ? false : paraValidators[address]}
isPara={paraValidators[address]}
key={address}
lastBlock={byAuthor[address]}
minCommission={minCommission}

View File

@ -9,7 +9,7 @@ import type { NominatedByMap, SortedTargets } from '../types';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, ToggleGroup } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useApi, useBlockAuthors, useCall } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import ActionsBanner from './ActionsBanner';
@ -32,9 +32,14 @@ interface Props {
toggleNominatedBy: () => void;
}
const EMPTY_PARA_VALS: Record<string, boolean> = {};
const EMPTY_BY_AUTHOR: Record<string, string> = {};
const EMPTY_ERA_POINTS: Record<string, string> = {};
function Overview ({ className = '', favorites, hasAccounts, hasQueries, minCommission, nominatedBy, ownStashes, paraValidators, stakingOverview, targets, toggleFavorite, toggleLedger, toggleNominatedBy }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { byAuthor, eraPoints } = useBlockAuthors();
const [intentIndex, _setIntentIndex] = useState(0);
const [typeIndex, setTypeIndex] = useState(1);
const recentlyOnline = useCall<DeriveHeartbeats>(api.derive.imOnline?.receivedHeartbeats);
@ -90,6 +95,8 @@ function Overview ({ className = '', favorites, hasAccounts, hasQueries, minComm
/>
</Button.Group>
<CurrentList
byAuthor={intentIndex === 0 ? byAuthor : EMPTY_BY_AUTHOR}
eraPoints={intentIndex === 0 ? eraPoints : EMPTY_ERA_POINTS}
favorites={favorites}
hasQueries={hasQueries}
isIntentions={intentIndex === 1}
@ -98,7 +105,7 @@ function Overview ({ className = '', favorites, hasAccounts, hasQueries, minComm
minCommission={intentIndex === 0 ? minCommission : undefined}
nominatedBy={intentIndex === 1 ? nominatedBy : undefined}
ownStashIds={ownStashIds}
paraValidators={paraValidators}
paraValidators={(intentIndex === 0 && paraValidators) || EMPTY_PARA_VALS}
recentlyOnline={intentIndex === 0 ? recentlyOnline : undefined}
stakingOverview={stakingOverview}
targets={targets}

View File

@ -3,13 +3,12 @@
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { InjectedExtension } from '@polkadot/extension-inject/types';
import type { ProviderStats } from '@polkadot/rpc-provider/types';
import type { ChainProperties, ChainType } from '@polkadot/types/interfaces';
import type { KeyringStore } from '@polkadot/ui-keyring/types';
import type { ApiProps, ApiState } from './types';
import * as Sc from '@substrate/connect';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import store from 'store';
import { ApiPromise, ScProvider, WsProvider } from '@polkadot/api';
@ -17,15 +16,13 @@ import { deriveMapCache, setDeriveCache } from '@polkadot/api-derive/util';
import { ethereumChains, typesBundle } from '@polkadot/apps-config';
import { web3Accounts, web3Enable } from '@polkadot/extension-dapp';
import { TokenUnit } from '@polkadot/react-components/InputNumber';
import { StatusContext } from '@polkadot/react-components/Status';
import { useApiUrl, useEndpoint } from '@polkadot/react-hooks';
import { useApiUrl, useEndpoint, useQueue } from '@polkadot/react-hooks';
import ApiSigner from '@polkadot/react-signer/signers/ApiSigner';
import { keyring } from '@polkadot/ui-keyring';
import { settings } from '@polkadot/ui-settings';
import { formatBalance, isNumber, isTestChain, objectSpread, stringify } from '@polkadot/util';
import { defaults as addressDefaults } from '@polkadot/util-crypto/address/defaults';
import ApiContext from './ApiContext';
import { lightSpecs, relaySpecs } from './light';
import registry from './typeRegistry';
import { decodeUrlTypes } from './urlTypes';
@ -59,7 +56,10 @@ export const DEFAULT_DECIMALS = registry.createType('u32', 12);
export const DEFAULT_SS58 = registry.createType('u32', addressDefaults.prefix);
export const DEFAULT_AUX = ['Aux1', 'Aux2', 'Aux3', 'Aux4', 'Aux5', 'Aux6', 'Aux7', 'Aux8', 'Aux9'];
export const ApiCtx = React.createContext<ApiProps>({} as unknown as ApiProps);
const DISALLOW_EXTENSIONS: string[] = [];
const EMPTY_STATE = { hasInjectedAccounts: false, isApiReady: false } as unknown as ApiState;
let api: ApiPromise;
@ -102,7 +102,7 @@ async function getInjectedAccounts (injectedPromise: Promise<InjectedExtension[]
}
}
function createLink (baseApiUrl: string, isElectron: boolean): (path: string) => string {
function makeCreateLink (baseApiUrl: string, isElectron: boolean): (path: string) => string {
return (path: string, apiUrl?: string): string =>
`${isElectron
? 'https://polkadot.js.org/apps/'
@ -110,44 +110,6 @@ function createLink (baseApiUrl: string, isElectron: boolean): (path: string) =>
}?rpc=${encodeURIComponent(apiUrl || baseApiUrl)}#${path}`;
}
function getStats (...apis: ApiPromise[]): [ProviderStats, number] {
const stats = apis.reduce<ProviderStats>((r, api) => {
if (api) {
const stats = api.stats;
if (stats) {
r.active.requests += stats.active.requests;
r.active.subscriptions += stats.active.subscriptions;
r.total.bytesRecv += stats.total.bytesRecv;
r.total.bytesSent += stats.total.bytesSent;
r.total.cached += stats.total.cached;
r.total.errors += stats.total.errors;
r.total.requests += stats.total.requests;
r.total.subscriptions += stats.total.subscriptions;
r.total.timeout += stats.total.timeout;
}
}
return r;
}, {
active: {
requests: 0,
subscriptions: 0
},
total: {
bytesRecv: 0,
bytesSent: 0,
cached: 0,
errors: 0,
requests: 0,
subscriptions: 0,
timeout: 0
}
});
return [stats, Date.now()];
}
async function retrieve (api: ApiPromise, injectedPromise: Promise<InjectedExtension[]>): Promise<ChainData> {
const [systemChain, systemChainType, systemName, systemVersion, injectedAccounts] = await Promise.all([
api.rpc.system.chain(),
@ -292,9 +254,9 @@ async function createApi (apiUrl: string, signer: ApiSigner, onError: (error: un
return types;
}
function Api ({ apiUrl, children, isElectron, store }: Props): React.ReactElement<Props> | null {
const { queuePayload, queueSetTxStatus } = useContext(StatusContext);
const [state, setState] = useState<ApiState>({ hasInjectedAccounts: false, isApiReady: false } as unknown as ApiState);
export function ApiCtxRoot ({ apiUrl, children, isElectron, store }: Props): React.ReactElement<Props> | null {
const { queuePayload, queueSetTxStatus } = useQueue();
const [state, setState] = useState<ApiState>(EMPTY_STATE);
const [isApiConnected, setIsApiConnected] = useState(false);
const [isApiInitialized, setIsApiInitialized] = useState(false);
const [apiError, setApiError] = useState<null | string>(null);
@ -307,22 +269,23 @@ function Api ({ apiUrl, children, isElectron, store }: Props): React.ReactElemen
[apiEndpoint]
);
const apiRelay = useApiUrl(relayUrls);
const value = useMemo<ApiProps>(
() => objectSpread({}, state, { api, apiEndpoint, apiError, apiRelay, apiUrl, createLink: createLink(apiUrl, isElectron), extensions, getStats, isApiConnected, isApiInitialized, isElectron, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, isElectron, state, apiEndpoint, apiRelay, apiUrl]
const createLink = useMemo(
() => makeCreateLink(apiUrl, isElectron),
[apiUrl, isElectron]
);
const onError = useCallback(
(error: unknown): void => {
console.error(error);
setApiError((error as Error).message);
},
[setApiError]
const value = useMemo<ApiProps>(
() => objectSpread({}, state, { api, apiEndpoint, apiError, apiRelay, apiUrl, createLink, extensions, isApiConnected, isApiInitialized, isElectron, isWaitingInjected: !extensions }),
[apiError, createLink, extensions, isApiConnected, isApiInitialized, isElectron, state, apiEndpoint, apiRelay, apiUrl]
);
// initial initialization
useEffect((): void => {
const onError = (error: unknown): void => {
console.error(error);
setApiError((error as Error).message);
};
createApi(apiUrl, new ApiSigner(registry, queuePayload, queueSetTxStatus), onError)
.then((types): void => {
api.on('connected', () => setIsApiConnected(true));
@ -343,17 +306,15 @@ function Api ({ apiUrl, children, isElectron, store }: Props): React.ReactElemen
setIsApiInitialized(true);
})
.catch(onError);
}, [apiEndpoint, apiUrl, onError, queuePayload, queueSetTxStatus, store]);
}, [apiEndpoint, apiUrl, queuePayload, queueSetTxStatus, store]);
if (!value.isApiInitialized) {
return null;
}
return (
<ApiContext.Provider value={value}>
<ApiCtx.Provider value={value}>
{children}
</ApiContext.Provider>
</ApiCtx.Provider>
);
}
export default React.memo(Api);

View File

@ -1,17 +0,0 @@
// Copyright 2017-2023 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiProps } from './types';
import React from 'react';
const ApiContext: React.Context<ApiProps> = React.createContext({} as unknown as ApiProps);
const ApiConsumer: React.Consumer<ApiProps> = ApiContext.Consumer;
const ApiProvider: React.Provider<ApiProps> = ApiContext.Provider;
export default ApiContext;
export {
ApiConsumer,
ApiProvider
};

View File

@ -8,7 +8,7 @@ import React from 'react';
import { assert } from '@polkadot/util';
import { ApiConsumer } from '../ApiContext';
import { ApiCtx } from '../Api';
export default function withApi <P extends ApiProps> (Inner: React.ComponentType<P>, defaultProps: DefaultProps = {}): React.ComponentType<any> {
class WithApi extends React.PureComponent<SubtractProps<P, ApiProps>> {
@ -16,7 +16,7 @@ export default function withApi <P extends ApiProps> (Inner: React.ComponentType
public override render (): React.ReactNode {
return (
<ApiConsumer>
<ApiCtx.Consumer>
{(apiProps?: ApiProps): React.ReactNode => {
assert(apiProps && apiProps.api, 'Application root must be wrapped inside \'react-api/Api\' to provide API context');
@ -30,7 +30,7 @@ export default function withApi <P extends ApiProps> (Inner: React.ComponentType
/>
);
}}
</ApiConsumer>
</ApiCtx.Consumer>
);
}
}

View File

@ -1,19 +1,5 @@
// Copyright 2017-2023 @polkadot/react-api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import Api, { api, DEFAULT_DECIMALS, DEFAULT_SS58 } from './Api';
import ApiContext from './ApiContext';
import { withApi, withCallDiv, withCalls, withMulti, withObservable } from './hoc';
export {
api,
Api,
ApiContext,
DEFAULT_DECIMALS,
DEFAULT_SS58,
withApi,
withCalls,
withCallDiv,
withMulti,
withObservable
};
export { api, ApiCtx, ApiCtxRoot, DEFAULT_DECIMALS, DEFAULT_SS58 } from './Api';
export { withApi, withCallDiv, withCalls, withMulti, withObservable } from './hoc';

View File

@ -5,7 +5,6 @@ import type { ApiPromise } from '@polkadot/api';
import type { SubmittableExtrinsicFunction } from '@polkadot/api/promise/types';
import type { LinkOption } from '@polkadot/apps-config/settings/types';
import type { InjectedExtension } from '@polkadot/extension-inject/types';
import type { ProviderStats } from '@polkadot/rpc-provider/types';
// helpers for HOC props
export type OmitProps<T, K> = Pick<T, Exclude<keyof T, K>>;
@ -38,7 +37,6 @@ export interface ApiProps extends ApiState {
apiUrl?: string;
createLink: (path: string, apiUrl?: string) => string;
extensions?: InjectedExtension[];
getStats: (...apis: ApiPromise[]) => [ProviderStats, number];
isApiConnected: boolean;
isApiInitialized: boolean;
isElectron: boolean;

View File

@ -8,7 +8,7 @@ import type { AccountId, AccountIndex, Address } from '@polkadot/types/interface
import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { AccountSidebarToggle } from '@polkadot/app-accounts/Sidebar';
import { AccountSidebarCtx } from '@polkadot/app-accounts/Sidebar';
import registry from '@polkadot/react-api/typeRegistry';
import { useDeriveAccountInfo, useSystemApi } from '@polkadot/react-hooks';
import { formatNumber, isCodec, isFunction, stringToU8a, u8aEmpty, u8aEq, u8aToBn } from '@polkadot/util';
@ -174,7 +174,7 @@ function AccountName ({ children, className = '', defaultName, label, onClick, o
const api = useSystemApi();
const info = useDeriveAccountInfo(value);
const [name, setName] = useState<React.ReactNode>(() => extractName((value || '').toString(), undefined, defaultName));
const toggleSidebar = useContext(AccountSidebarToggle);
const toggleSidebar = useContext(AccountSidebarCtx);
// set the actual nickname, local name, accountIndex, accountId
useEffect((): void => {

View File

@ -1,17 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Stats } from './types';
import React from 'react';
const ApiStatsContext: React.Context<Stats[]> = React.createContext<Stats[]>([]);
const ApiStatsConsumer: React.Consumer<Stats[]> = ApiStatsContext.Consumer;
const ApiStatsProvider: React.Provider<Stats[]> = ApiStatsContext.Provider;
export default ApiStatsContext;
export {
ApiStatsConsumer,
ApiStatsProvider
};

View File

@ -1,23 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import ApiStatsContext, { ApiStatsProvider } from './Context';
import useApiStats from './useApiStats';
export { ApiStatsContext };
interface Props {
children?: React.ReactNode;
}
export default function ApiStats ({ children }: Props): React.ReactElement<Props> {
const stats = useApiStats();
return (
<ApiStatsProvider value={stats}>
{children}
</ApiStatsProvider>
);
}

View File

@ -1,9 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ProviderStats } from '@polkadot/rpc-provider/types';
export interface Stats {
stats: ProviderStats;
when: number;
}

View File

@ -1,56 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Stats } from './types';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createNamedHook, useApi } from '@polkadot/react-hooks';
const MAX_NUM = 60; // 5 minutes
const INTERVAL = 5_000;
function useApiStatsImpl (): Stats[] {
const { api, getStats } = useApi();
const [stats, setStats] = useState<Stats[]>([]);
const timerId = useRef<NodeJS.Timeout | null>(null);
const fireTimer = useCallback(
(): void => {
timerId.current = null;
const [stats, when] = getStats(api);
setStats((prev): Stats[] => {
const curr = {
stats,
when
};
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);
},
[api, getStats, timerId]
);
useEffect((): () => void => {
fireTimer();
return (): void => {
timerId.current && clearTimeout(timerId.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return stats;
}
export default createNamedHook('useApiStats', useApiStatsImpl);

View File

@ -3,12 +3,13 @@
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import React, { useContext, useMemo, useState } from 'react';
import styled, { ThemeContext } from 'styled-components';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { useTheme } from '@polkadot/react-hooks';
import Icon from './Icon';
import Tooltip from './Tooltip';
import { ThemeDef } from './types';
interface Props {
className?: string;
@ -26,7 +27,7 @@ let badgeId = 0;
function Badge ({ className = '', color = 'normal', hover, hoverAction, icon, info, isBlock, isSmall, onClick }: Props): React.ReactElement<Props> | null {
const badgeTestId = `${icon ? `${icon}-` : ''}badge`;
const { theme } = useContext(ThemeContext as React.Context<ThemeDef>);
const { theme } = useTheme();
const [trigger] = useState(() => `${badgeTestId}-hover-${Date.now()}-${badgeId++}`);
const extraProps = hover

View File

@ -55,10 +55,12 @@ function Button ({ activeOnEnter, children, className = '', dataTestId = '', ico
{icon && <Icon icon={icon} />}
{label}
{children}
<Spinner
className='ui--Button-spinner'
variant='cover'
/>
{isBusy && (
<Spinner
className='ui--Button-spinner'
variant='cover'
/>
)}
</button>
);
}
@ -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;
}

View File

@ -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<Props> {
const { t } = useTranslation();
const { queueAction } = useContext(StatusContext);
const { queueAction } = useQueue();
const _onCopy = useCallback(
(): void => {

View File

@ -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<Props> {
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

View File

@ -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 {

View File

@ -12,7 +12,7 @@ import { useTheme } from '@polkadot/react-hooks';
import Header from './Header';
function Base (props: ModalProps): React.ReactElement<ModalProps> {
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<ModalProps> {
return createPortal(
<div
className={`${theme} ui--Modal ${className} ${size}Size`}
className={`${themeClassName} ui--Modal ${className} ${size}Size`}
data-testid={testId}
>
<DisableGlobalScroll />

View File

@ -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 () {

View File

@ -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(
<div
className={`${className}${verticalPosition === 'top' ? ' pointerTop' : ' pointerBottom'}`}
className={`${className} ${pointerStyle}Pointer`}
data-testid='popup-window'
ref={windowRef}
style={renderWindowPosition && { transform: `translate3d(${renderWindowPosition.x}px, ${renderWindowPosition.y}px, 0)`, zIndex: 1000 }}
style={style}
>
{children}
</div>,
@ -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;

View File

@ -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<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(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 (
<div className={`ui--Popup ${theme} ${className}`}>
<div className={`ui--Popup ${themeClassName} ${className}`}>
{isOpen && (
<PopupWindow
position={position}
@ -46,6 +55,7 @@ function Popup ({ children, className = '', closeOnScroll = false, isDisabled =
</PopupWindow>
)}
<div
data-testid='popup-open'
onClick={toggleIsOpen}
ref={triggerRef}
>

View File

@ -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);

View File

@ -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<QueueProps> = {
stqueue: [] as QueueStatus[],
txqueue: [] as QueueTx[]
};
const StatusContext: React.Context<QueueProps> = React.createContext<QueueProps>(defaultState as QueueProps);
const QueueConsumer: React.Consumer<QueueProps> = StatusContext.Consumer;
const QueueProvider: React.Provider<QueueProps> = StatusContext.Provider;
export default StatusContext;
export {
QueueConsumer,
QueueProvider
};

View File

@ -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<Props> | null {
const { stqueue, txqueue } = useContext(StatusContext);
const { stqueue, txqueue } = useQueue();
const [allSt, setAllSt] = useState<QueueStatus[]>([]);
const [allTx, setAllTx] = useState<QueueTx[]>([]);

View File

@ -12,7 +12,7 @@ import CurrentSection from './CurrentSection';
import Tab from './Tab';
import Delimiter from './TabsSectionDelimiter';
export const TabsContext = React.createContext<SectionType>({});
export const TabsCtx = React.createContext<SectionType>({});
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<Props> {
const location = useLocation();
const { icon, text } = React.useContext(TabsContext);
const { icon, text } = React.useContext(TabsCtx);
useEffect(
() => redirect(basePath, location, items, hidden),

View File

@ -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 {

View File

@ -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<Props> {
const { t } = useTranslation();
const mountedRef = useIsMountedRef();
const { queueExtrinsic } = useContext(StatusContext);
const { queueExtrinsic } = useQueue();
const [isSending, setIsSending] = useState(false);
const [isStarted, setIsStarted] = useState(false);

View File

@ -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';

View File

@ -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 => `

View File

@ -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 => `

View File

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

View File

@ -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<ApiStats[]>(EMPTY_STATE);
export function ApiStatsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [stats, setStats] = useState(EMPTY_STATE);
const timerId = useRef<ReturnType<typeof setTimeout> | 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 (
<ApiStatsCtx.Provider value={stats}>
{children}
</ApiStatsCtx.Provider>
);
}

View File

@ -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<string, string>;
eraPoints: Record<string, string>;
lastBlockAuthors: string[];
lastBlockNumber?: string;
lastHeader?: HeaderExtendedWithMapping;
lastHeaders: HeaderExtendedWithMapping[];
}
interface Props {
children: React.ReactNode;
}
@ -31,27 +18,23 @@ const MAX_HEADERS = 75;
const byAuthor: Record<string, string> = {};
const eraPoints: Record<string, string> = {};
const BlockAuthorsContext: React.Context<Authors> = React.createContext<Authors>({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] });
const ValidatorsContext: React.Context<string[]> = React.createContext<string[]>([]);
function BlockAuthorsBase ({ children }: Props): React.ReactElement<Props> {
const EMPTY_STATE: BlockAuthors = { byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] };
export const BlockAuthorsCtx = React.createContext<BlockAuthors>(EMPTY_STATE);
export function BlockAuthorsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const queryPoints = useCall<EraRewardPoints>(isApiReady && api.derive.staking?.currentPoints);
const [state, setState] = useState<Authors>({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] });
const [validators, setValidators] = useState<string[]>([]);
const [state, setState] = useState<BlockAuthors>(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<Props> {
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<Props> {
}, [queryPoints]);
return (
<ValidatorsContext.Provider value={validators}>
<BlockAuthorsContext.Provider value={state}>
{children}
</BlockAuthorsContext.Provider>
</ValidatorsContext.Provider>
<BlockAuthorsCtx.Provider value={state}>
{children}
</BlockAuthorsCtx.Provider>
);
}
const BlockAuthors = React.memo(BlockAuthorsBase);
export { BlockAuthorsContext, BlockAuthors, ValidatorsContext };

View File

@ -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<Events> = React.createContext<Events>(DEFAULT_EVENTS);
export const BlockEventsCtx = React.createContext<BlockEvents>(DEFAULT_EVENTS);
async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec<EventRecord>, setState: React.Dispatch<React.SetStateAction<Events>>): Promise<void> {
async function manageEvents (api: ApiPromise, prev: PrevHashes, records: Vec<EventRecord>, setState: React.Dispatch<React.SetStateAction<BlockEvents>>): Promise<void> {
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<Eve
}
}
function EventsBase ({ children }: Props): React.ReactElement<Props> {
export function BlockEventsCtxRoot ({ children }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const [state, setState] = useState<Events>(DEFAULT_EVENTS);
const [state, setState] = useState<BlockEvents>(DEFAULT_EVENTS);
const records = useCall<Vec<EventRecord>>(isApiReady && api.query.system.events);
const prevHashes = useRef({ block: null, event: null });
@ -115,12 +112,8 @@ function EventsBase ({ children }: Props): React.ReactElement<Props> {
}, [api, prevHashes, records, setState]);
return (
<EventsContext.Provider value={state}>
<BlockEventsCtx.Provider value={state}>
{children}
</EventsContext.Provider>
</BlockEventsCtx.Provider>
);
}
const Events = React.memo(EventsBase);
export { EventsContext, Events };

View File

@ -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<State>(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<string[]>((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<Props> {
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 (
<KeyringCtx.Provider value={state}>
{children}
</KeyringCtx.Provider>
);
}

View File

@ -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<QueueProps> = {
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<QueueProps>(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<Props> {
const [stqueue, _setStQueue] = useState<QueueStatus[]>([]);
const [txqueue, _setTxQueue] = useState<QueueTx[]>([]);
const EMPTY_QUEUE_ST: QueueStatus[] = [];
const EMPTY_QUEUE_TX: QueueTx[] = [];
export function QueueCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [stqueue, _setStQueue] = useState<QueueStatus[]>(EMPTY_QUEUE_ST);
const [txqueue, _setTxQueue] = useState<QueueTx[]>(EMPTY_QUEUE_TX);
const stRef = useRef(stqueue);
const txRef = useRef(txqueue);
@ -279,21 +287,22 @@ function Queue ({ children }: Props): React.ReactElement<Props> {
[queueAction, setTxQueue]
);
const value = useMemo(
() => ({
queueAction,
queueExtrinsic,
queuePayload,
queueRpc,
queueSetTxStatus,
stqueue,
txqueue
}),
[queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue]
);
return (
<QueueProvider
value={{
queueAction,
queueExtrinsic,
queuePayload,
queueRpc,
queueSetTxStatus,
stqueue,
txqueue
}}
>
<QueueCtx.Provider value={value}>
{children}
</QueueProvider>
</QueueCtx.Provider>
);
}
export default React.memo(Queue);

View File

@ -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<WindowSize>(getDimensions());
export function WindowSizeCtxRoot ({ children }: Props): React.ReactElement<Props> {
const [dimensions, setDimensions] = useState(() => getDimensions());
useEffect((): () => void => {
function handleResize (): void {
setDimensions(getDimensions());
}
window.addEventListener('resize', handleResize);
return (): void => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<WindowSizeCtx.Provider value={dimensions}>
{children}
</WindowSizeCtx.Provider>
);
}

View File

@ -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';

View File

@ -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<string, string>;
eraPoints: Record<string, string>;
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;
}

View File

@ -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';

View File

@ -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<Nominations>(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);

View File

@ -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<UseAccounts>(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);

View File

@ -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<UseAddresses>({ 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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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<HTMLElement | undefined | null>): ElementPosition | undefined {
const [elementPosition, setElementPosition] = useState<ElementPosition>();
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<HTMLElement | undef
y
});
}
}, [ref, scrollY, windowSize]);
}, [mountedRef, ref, scrollY, windowSize]);
return elementPosition;
}

View File

@ -0,0 +1,15 @@
// Copyright 2017-2023 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Accounts, Addresses } from './ctx/types';
import { useContext } from 'react';
import { KeyringCtx } from './ctx/Keyring';
import { createNamedHook } from './createNamedHook';
function useKeyringImpl (): { accounts: Accounts, addresses: Addresses } {
return useContext(KeyringCtx);
}
export const useKeyring = createNamedHook('useKeyring', useKeyringImpl);

View File

@ -3,22 +3,32 @@
import { RefObject, useCallback, useEffect } from 'react';
function getClickedElement (refs: React.RefObject<HTMLDivElement>[], e: MouseEvent) {
return refs.find((ref) => ref.current && ref.current.contains(e.target as HTMLElement));
import { createNamedHook } from './createNamedHook';
function isRefClicked (refs: React.RefObject<HTMLDivElement>[], e: MouseEvent): boolean {
return refs.some((r) =>
r.current &&
r.current.contains(e.target as HTMLElement)
);
}
export const useOutsideClick = (elements: RefObject<HTMLDivElement>[], callback: () => void): void => {
const handleClick = useCallback((e: MouseEvent) => {
if (elements.length && !getClickedElement(elements, e)) {
callback();
}
}, [elements, callback]);
function useOutsideClickImpl (refs: RefObject<HTMLDivElement>[], 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);

View File

@ -17,34 +17,35 @@ interface Coord {
}
interface Result {
renderWindowPosition?: Coord;
verticalPosition: VerticalPosition | undefined
pointerStyle: VerticalPosition;
renderCoords: Coord;
}
function usePopupWindowImpl (windowRef: React.RefObject<HTMLDivElement>, triggerRef: React.RefObject<HTMLDivElement>, position: HorizontalPosition): Result {
const [renderWindowPosition, setRenderWindowPosition] = useState<Coord>();
const [verticalPosition, setVerticalPosition] = useState<VerticalPosition>();
const windowPosition = useElementPosition(windowRef);
const triggerPosition = useElementPosition(triggerRef);
const COORD_0: Coord = { x: 0, y: 0 };
function usePopupWindowImpl (windowRef: React.RefObject<HTMLDivElement>, triggerRef: React.RefObject<HTMLDivElement>, position: HorizontalPosition): Result {
const [renderCoords, setRenderCoords] = useState<Coord>(COORD_0);
const [pointerStyle, setPointerStyle] = useState<VerticalPosition>('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]
);
}

View File

@ -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);

View File

@ -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<ThemeDef>);
return `theme--${theme}`;
return useMemo(
(): Theme => ({
theme,
themeClassName: `theme--${theme}`
}),
[theme]
);
}
export const useTheme = createNamedHook('useTheme', useThemeImpl);

View File

@ -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<WindowSize>({
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);

View File

@ -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';

View File

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

View File

@ -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<Error | null>(null);
const [{ isQrHashed, qrAddress, qrPayload, qrResolve }, setQrState] = useState<QrState>(() => ({ isQrHashed: false, qrAddress: '', qrPayload: new Uint8Array() }));

View File

@ -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<Props> | null {
const { t } = useTranslation();
const { queueSetTxStatus } = useContext(StatusContext);
const { queueSetTxStatus } = useQueue();
const [isRenderError, toggleRenderError] = useToggle();
const _onSend = useCallback(

View File

@ -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<Props> {
const { api } = useApi();
const { t } = useTranslation();
const { queueSetTxStatus, txqueue } = useContext(StatusContext);
const { queueSetTxStatus, txqueue } = useQueue();
const { count, currentItem, isRpc, isVisible, requestAddress } = useMemo(
() => extractCurrent(txqueue),

View File

@ -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 {
<>
<div id='tooltips' />
<Suspense fallback='...'>
<QueueProvider value={queue}>
<QueueCtx.Provider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<ApiCtx.Provider value={mockApi}>
<AccountSidebar>
{React.cloneElement(this.overview, { onStatusChange: noop }) }
</AccountSidebar>
</ApiContext.Provider>
</ApiCtx.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueProvider>
</QueueCtx.Provider>
</Suspense>
</>
);

View File

@ -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<UseAccountInfo>;
}
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);