diff --git a/packages/page-assets/src/useAssetIds.ts b/packages/page-assets/src/useAssetIds.ts index 86c8ff79f7..af39a7e47a 100644 --- a/packages/page-assets/src/useAssetIds.ts +++ b/packages/page-assets/src/useAssetIds.ts @@ -1,26 +1,40 @@ // Copyright 2017-2022 @polkadot/app-assets authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { Changes } from '@polkadot/react-hooks/useEventChanges'; import type { StorageKey } from '@polkadot/types'; -import type { AssetId } from '@polkadot/types/interfaces'; +import type { AssetId, EventRecord } from '@polkadot/types/interfaces'; -import { createNamedHook, useApi, useEventTrigger, useMapKeys } from '@polkadot/react-hooks'; +import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@polkadot/react-hooks'; -const options = { +const keyOptions = { transform: (keys: StorageKey<[AssetId]>[]): AssetId[] => - keys - .map(({ args: [assetId] }) => assetId) - .sort((a, b) => a.cmp(b)) + keys.map(({ args: [id] }) => id) }; +function filter (records: EventRecord[]): Changes { + const added: AssetId[] = []; + const removed: AssetId[] = []; + + records.forEach(({ event: { data: [id], method } }): void => { + if (method === 'Created') { + added.push(id as AssetId); + } else { + removed.push(id as AssetId); + } + }); + + return { added, removed }; +} + function useAssetIdsImpl (): AssetId[] | undefined { const { api } = useApi(); - const trigger = useEventTrigger([ + const startValue = useMapKeys(api.query.assets.asset, keyOptions); + + return useEventChanges([ api.events.assets.Created, api.events.assets.Destroyed - ]); - - return useMapKeys(api.query.assets.asset, options, trigger.blockHash); + ], filter, startValue); } export default createNamedHook('useAssetIds', useAssetIdsImpl); diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 82cca3e3e2..04bcd81dcc 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -26,6 +26,7 @@ export { useDeriveAccountFlags } from './useDeriveAccountFlags'; export { useDeriveAccountInfo } from './useDeriveAccountInfo'; export { useElementPosition } from './useElementPosition'; export { useEndpoint } from './useEndpoint'; +export { useEventChanges } from './useEventChanges'; export { useEventTrigger } from './useEventTrigger'; export { useExtrinsicTrigger } from './useExtrinsicTrigger'; export { useFavorites } from './useFavorites'; diff --git a/packages/react-hooks/src/useEventChanges.ts b/packages/react-hooks/src/useEventChanges.ts new file mode 100644 index 0000000000..f32dec9549 --- /dev/null +++ b/packages/react-hooks/src/useEventChanges.ts @@ -0,0 +1,63 @@ +// Copyright 2017-2022 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { u32 } from '@polkadot/types'; +import type { EventRecord } from '@polkadot/types/interfaces'; +import type { Codec } from '@polkadot/types/types'; +import type { EventCheck } from './useEventTrigger'; + +import { useEffect, useState } from 'react'; + +import { isFunction } from '@polkadot/util'; + +import { useEventTrigger } from './useEventTrigger'; + +export interface Changes { + added?: T[]; + removed?: T[]; +} + +function interleave (existing: T[], { added = [], removed = [] }: Changes): T[] { + if (!added.length && !removed.length) { + return existing; + } + + const map: Record = {}; + + [existing, added].forEach((m) => + m.forEach((v): void => { + map[v.toHex()] = v; + }) + ); + + removed.forEach((v): void => { + delete map[v.toHex()]; + }); + + return Object + .entries(map) + .sort((a, b) => + // for BN-like objects, we use the built-in compare for sorting + isFunction((a[1] as unknown as u32).cmp) + ? (a[1] as unknown as u32).cmp(b[1] as unknown as u32) + : a[0].localeCompare(b[0]) + ) + .map(([, v]) => v); +} + +export function useEventChanges (checks: EventCheck[], filter: (records: EventRecord[]) => Changes, startValue?: T[]): T[] { + const [state, setState] = useState([]); + const { blockHash, events } = useEventTrigger(checks); + + // when startValue changes, we do a full refresh + useEffect((): void => { + startValue && setState((prev) => interleave(prev, { added: startValue })); + }, [startValue]); + + // add/remove any additional items detected (only when actual events occur) + useEffect((): void => { + blockHash && setState((prev) => interleave(prev, filter(events))); + }, [blockHash, events, filter]); + + return state; +} diff --git a/packages/react-hooks/src/useEventTrigger.ts b/packages/react-hooks/src/useEventTrigger.ts index ff5d2ef13d..aefc995f9b 100644 --- a/packages/react-hooks/src/useEventTrigger.ts +++ b/packages/react-hooks/src/useEventTrigger.ts @@ -12,7 +12,7 @@ import { useApi } from './useApi'; import { useCall } from './useCall'; import { useIsMountedRef } from './useIsMountedRef'; -type EventCheck = AugmentedEvent<'promise'> | false | undefined | null; +export type EventCheck = AugmentedEvent<'promise'> | false | undefined | null; interface Result { blockHash: string;