diff --git a/lib/hooks/ens-cache.js b/lib/hooks/ens-cache.js --- a/lib/hooks/ens-cache.js +++ b/lib/hooks/ens-cache.js @@ -6,11 +6,19 @@ import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; import { getETHAddressForUserInfo } from '../shared/account-utils.js'; import { stringForUser } from '../shared/user-utils.js'; +import { getENSNames } from '../utils/ens-helpers.js'; type BaseUserInfo = { +username?: ?string, ... }; -function useENSNames(users: $ReadOnlyArray): T[] { +export type UseENSNamesOptions = { + +allAtOnce?: ?boolean, +}; +function useENSNames( + users: $ReadOnlyArray, + options?: ?UseENSNamesOptions, +): T[] { const cacheContext = React.useContext(ENSCacheContext); const { ensCache } = cacheContext; + const allAtOnce = options?.allAtOnce ?? false; const cachedInfo = React.useMemo( () => @@ -43,7 +51,8 @@ if (!ensCache) { return; } - const needFetch = cachedInfo + + const needFetchUsers: $ReadOnlyArray<{ +username: string }> = cachedInfo .map(user => { if (!user) { return null; @@ -52,20 +61,39 @@ if (cachedResult || !ethAddress || fetchedAddresses.has(ethAddress)) { return null; } - return ethAddress; + return { username: ethAddress }; }) .filter(Boolean); - if (needFetch.length === 0) { + if (needFetchUsers.length === 0) { return; } + + const needFetchAddresses = needFetchUsers.map(({ username }) => username); setFetchedAddresses(oldFetchedAddresses => { const newFetchedAddresses = new Set(oldFetchedAddresses); - for (const ethAddress of needFetch) { + for (const ethAddress of needFetchAddresses) { newFetchedAddresses.add(ethAddress); } return newFetchedAddresses; }); - for (const ethAddress of needFetch) { + + if (allAtOnce) { + (async () => { + const withENSNames = await getENSNames(ensCache, needFetchUsers); + setENSNames(oldENSNames => { + const newENSNames = new Map(oldENSNames); + for (let i = 0; i < withENSNames.length; i++) { + const ethAddress = needFetchAddresses[i]; + const result = withENSNames[i].username; + newENSNames.set(ethAddress, result); + } + return newENSNames; + }); + })(); + return; + } + + for (const ethAddress of needFetchAddresses) { (async () => { const result = await ensCache.getNameForAddress(ethAddress); if (!result) { @@ -78,7 +106,7 @@ }); })(); } - }, [cachedInfo, fetchedAddresses, ensCache]); + }, [cachedInfo, fetchedAddresses, ensCache, allAtOnce]); return React.useMemo( () => diff --git a/lib/selectors/nav-selectors.js b/lib/selectors/nav-selectors.js --- a/lib/selectors/nav-selectors.js +++ b/lib/selectors/nav-selectors.js @@ -3,7 +3,7 @@ import * as React from 'react'; import { createSelector } from 'reselect'; -import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; +import { useENSNames } from '../hooks/ens-cache.js'; import SearchIndex from '../shared/search-index.js'; import { memberHasAdminPowers } from '../shared/thread-utils.js'; import type { Platform } from '../types/device-types.js'; @@ -16,7 +16,6 @@ import type { BaseAppState } from '../types/redux-types.js'; import type { RawThreadInfo, ThreadInfo } from '../types/thread-types.js'; import { getConfig } from '../utils/config.js'; -import { getENSNames } from '../utils/ens-helpers.js'; import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; @@ -72,6 +71,12 @@ }, ); +// Without allAtOnce, useThreadSearchIndex is very expensive. useENSNames would +// trigger its recalculation for each ENS name as it streams in, but we would +// prefer to trigger its recaculation just once for every update of the +// underlying Redux data. +const useENSNamesOptions = { allAtOnce: true }; + function useThreadSearchIndex( threadInfos: $ReadOnlyArray, ): SearchIndex { @@ -101,34 +106,18 @@ return [...allMembersOfAllThreads.values()]; }, [threadInfos, userInfos, viewerID]); - const cacheContext = React.useContext(ENSCacheContext); - const { ensCache } = cacheContext; - - // We avoid using useENSNames here because the SearchIndex memo below is very - // expensive. useENSNames would trigger its recalculation for each ENS name as - // it streams in, but we would prefer to trigger its recaculation just once - // for every update of the underlying Redux data. - const [nonViewerMembersWithENSNames, setNonViewerMembersWithENSNames] = - React.useState(); - React.useEffect(() => { - if (!ensCache) { - return; - } - (async () => { - const withENSNames = await getENSNames(ensCache, nonViewerMembers); - setNonViewerMembersWithENSNames(withENSNames); - })(); - }, [ensCache, nonViewerMembers]); + const nonViewerMembersWithENSNames = useENSNames( + nonViewerMembers, + useENSNamesOptions, + ); - const resolvedNonViewerMembers = - nonViewerMembersWithENSNames ?? nonViewerMembers; const memberMap = React.useMemo(() => { const result = new Map(); - for (const userInfo of resolvedNonViewerMembers) { + for (const userInfo of nonViewerMembersWithENSNames) { result.set(userInfo.id, userInfo); } return result; - }, [resolvedNonViewerMembers]); + }, [nonViewerMembersWithENSNames]); return React.useMemo(() => { const searchIndex = new SearchIndex();