diff --git a/lib/hooks/fc-cache.js b/lib/hooks/fc-cache.js new file mode 100644 --- /dev/null +++ b/lib/hooks/fc-cache.js @@ -0,0 +1,144 @@ +// @flow + +import * as React from 'react'; + +import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; +import { getFCNames } from '../utils/farcaster-helpers.js'; + +type BaseFCInfo = { + +fid?: ?string, + +farcasterUsername?: ?string, + ... +}; +export type UseFCNamesOptions = { + +allAtOnce?: ?boolean, +}; +function useFCNames( + users: $ReadOnlyArray, + options?: ?UseFCNamesOptions, +): T[] { + const neynarClientContext = React.useContext(NeynarClientContext); + const fcCache = neynarClientContext?.fcCache; + const allAtOnce = options?.allAtOnce ?? false; + + const cachedInfo = React.useMemo( + () => + users.map(user => { + if (!user) { + return user; + } + const { fid, farcasterUsername } = user; + let cachedResult = null; + if (farcasterUsername) { + cachedResult = farcasterUsername; + } else if (fid && fcCache) { + cachedResult = fcCache.getCachedFarcasterUsernameForFID(fid); + } + return { + input: user, + fid, + cachedResult, + }; + }), + [users, fcCache], + ); + + const [fetchedFIDs, setFetchedFIDs] = React.useState<$ReadOnlySet>( + new Set(), + ); + const [farcasterUsernames, setFarcasterUsernames] = React.useState< + $ReadOnlyMap, + >(new Map()); + + React.useEffect(() => { + if (!fcCache) { + return; + } + + const needFetchUsers: $ReadOnlyArray<{ + +fid: string, + +farcasterUsername?: ?string, + }> = cachedInfo + .map(user => { + if (!user) { + return null; + } + const { fid, cachedResult } = user; + if (cachedResult || !fid || fetchedFIDs.has(fid)) { + return null; + } + return { fid }; + }) + .filter(Boolean); + if (needFetchUsers.length === 0) { + return; + } + + const needFetchFIDs = needFetchUsers.map(({ fid }) => fid); + setFetchedFIDs(oldFetchedFIDs => { + const newFetchedFIDs = new Set(oldFetchedFIDs); + for (const fid of needFetchFIDs) { + newFetchedFIDs.add(fid); + } + return newFetchedFIDs; + }); + + if (allAtOnce) { + void (async () => { + const withFarcasterUsernames = await getFCNames( + fcCache, + needFetchUsers, + ); + setFarcasterUsernames(oldFarcasterUsernames => { + const newFarcasterUsernames = new Map(oldFarcasterUsernames); + for (let i = 0; i < withFarcasterUsernames.length; i++) { + const fid = needFetchFIDs[i]; + const result = withFarcasterUsernames[i].farcasterUsername; + if (result) { + newFarcasterUsernames.set(fid, result); + } + } + return newFarcasterUsernames; + }); + })(); + return; + } + + for (const fid of needFetchFIDs) { + void (async () => { + const [result] = await fcCache.getFarcasterUsernamesForFIDs([fid]); + if (!result) { + return; + } + setFarcasterUsernames(oldFarcasterUsernames => { + const newFarcasterUsernames = new Map(oldFarcasterUsernames); + newFarcasterUsernames.set(fid, result); + return newFarcasterUsernames; + }); + })(); + } + }, [cachedInfo, fetchedFIDs, fcCache, allAtOnce]); + + return React.useMemo( + () => + cachedInfo.map(user => { + if (!user) { + return user; + } + const { input, fid, cachedResult } = user; + if (cachedResult) { + return { ...input, farcasterUsername: cachedResult }; + } else if (!fid) { + return input; + } + const farcasterUsername = farcasterUsernames.get(fid); + if (farcasterUsername) { + return { ...input, farcasterUsername }; + } + return input; + }), + [cachedInfo, farcasterUsernames], + ); +} + +export { useFCNames }; diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js --- a/lib/utils/entity-text.js +++ b/lib/utils/entity-text.js @@ -7,6 +7,7 @@ import type { GetENSNames } from './ens-helpers.js'; import { tID, tShape, tString } from './validation-utils.js'; import { useENSNames } from '../hooks/ens-cache.js'; +import { useFCNames } from '../hooks/fc-cache.js'; import { threadNoun } from '../shared/thread-utils.js'; import { stringForUser } from '../shared/user-utils.js'; import type { @@ -588,11 +589,21 @@ [entityText], ); const objectsWithENSNames = useENSNames(allObjects, options); - return React.useMemo( - () => - entityText ? entityTextFromObjects(objectsWithENSNames) : entityText, - [entityText, objectsWithENSNames], - ); + const objectsWithFCNames = useFCNames(allObjects, options); + return React.useMemo(() => { + if (!entityText) { + return entityText; + } + const mergedObjects = []; + for (let i = 0; i < allObjects.length; i++) { + const originalObject = allObjects[i]; + const updatedObject = originalObject.fid + ? objectsWithFCNames[i] + : objectsWithENSNames[i]; + mergedObjects.push(updatedObject); + } + return entityTextFromObjects(mergedObjects); + }, [entityText, allObjects, objectsWithENSNames, objectsWithFCNames]); } function useEntityTextAsString(