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 @@ -1,65 +1,123 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import { ENSCacheContext } from '../components/ens-cache-provider.react'; import { userIdentifiedByETHAddress } from '../shared/account-utils'; import { stringForUser } from '../shared/user-utils'; -function useStringForUser( - user: ?{ +username?: ?string, +isViewer?: ?boolean, ... }, -): ?string { - const ethAddress = React.useMemo(() => { - if ( - !user || - user.isViewer || - !user.username || - !userIdentifiedByETHAddress(user) - ) { - return null; - } - return user.username; - }, [user]); - +type BaseUserInfo = { +username?: ?string, ... }; +function useENSNames(users: $ReadOnlyArray): Array { const cacheContext = React.useContext(ENSCacheContext); const { ensCache } = cacheContext; - const cachedResult = - ethAddress && ensCache - ? ensCache.getCachedNameForAddress(ethAddress) - : null; - const [ensName, setENSName] = React.useState(null); + const cachedInfo = React.useMemo( + () => + users.map(user => { + if (!user) { + return user; + } + const { username } = user; + const ethAddress = + username && userIdentifiedByETHAddress(user) ? username : null; + return { + input: user, + ethAddress, + cachedResult: + ethAddress && ensCache + ? ensCache.getCachedNameForAddress(ethAddress) + : null, + }; + }), + [users, ensCache], + ); - React.useEffect(() => { - // Whenever the ETH address changes, clear out ENS name before requery below - setENSName(null); - }, [ethAddress]); + const [fetchedAddresses, setFetchedAddresses] = React.useState< + $ReadOnlySet, + >(new Set()); + const [ensNames, setENSNames] = React.useState<$ReadOnlyMap>( + new Map(), + ); React.useEffect(() => { - if (cachedResult || !ethAddress || !ensCache) { + if (!ensCache) { + return; + } + const needFetch = cachedInfo + .map(user => { + if (!user) { + return null; + } + const { ethAddress, cachedResult } = user; + if (!ethAddress || cachedResult || fetchedAddresses.has(ethAddress)) { + return null; + } + return ethAddress; + }) + .filter(Boolean); + if (needFetch.length === 0) { return; } - let cancelled = false; - (async () => { - const result = await ensCache.getNameForAddress(ethAddress); - if (result && !cancelled) { - setENSName(result); + setFetchedAddresses(oldFetchedAddresses => { + const newFetchedAddresses = new Set(oldFetchedAddresses); + for (const ethAddress of needFetch) { + newFetchedAddresses.add(ethAddress); } - })(); - return () => { - cancelled = true; - }; - }, [cachedResult, ethAddress, ensCache]); + return newFetchedAddresses; + }); + for (const ethAddress of needFetch) { + (async () => { + const result = await ensCache.getNameForAddress(ethAddress); + if (!result) { + return; + } + setENSNames(oldENSNames => { + const newENSNames = new Map(oldENSNames); + newENSNames.set(ethAddress, result); + return newENSNames; + }); + })(); + } + }, [cachedInfo, fetchedAddresses, ensCache]); - if (ensName) { - return ensName; - } else if (cachedResult) { - return cachedResult; - } else if (user) { + return cachedInfo.map(user => { + if (!user) { + return user; + } + const { input, ethAddress, cachedResult } = user; + if (cachedResult) { + return { ...input, username: cachedResult }; + } else if (!ethAddress) { + return input; + } + const ensName = ensNames.get(ethAddress); + if (ensName) { + return { ...input, username: ensName }; + } + return input; + }); +} + +function useStringForUser( + user: ?{ +username?: ?string, +isViewer?: ?boolean, ... }, +): ?string { + const toFetch = user?.isViewer ? null : user; + // stringForUser ignores username is isViewer, so we skip the ENS fetch + const [result] = useENSNames([toFetch]); + if (user?.isViewer) { return stringForUser(user); + } else if (result) { + return stringForUser(result); } else { - return null; + invariant( + !user, + 'the only way result can be falsey is if useENSNames is passed a ' + + 'falsey input, and that can only happen if useStringForUser input is ' + + 'falsey or isViewer is set', + ); + return user; } } -export { useStringForUser }; +export { useENSNames, useStringForUser };