diff --git a/lib/components/base-auto-join-community-handler.react.js b/lib/components/base-auto-join-community-handler.react.js index 638e3811f..da0a1a5d9 100644 --- a/lib/components/base-auto-join-community-handler.react.js +++ b/lib/components/base-auto-join-community-handler.react.js @@ -1,293 +1,340 @@ // @flow import invariant from 'invariant'; import _pickBy from 'lodash/fp/pickBy.js'; import * as React from 'react'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import blobService from '../facts/blob-service.js'; import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { farcasterChannelTagBlobHash, useJoinCommunity, } from '../shared/community-utils.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import type { KeyserverOverride } from '../shared/invite-links.js'; import type { OngoingJoinCommunityData, JoinCommunityStep, } from '../types/community-types.js'; import type { CalendarQuery } from '../types/entry-types.js'; import type { SetState } from '../types/hook-types.js'; import { defaultThreadSubscription } from '../types/subscription-types.js'; import { getBlobFetchableURL } from '../utils/blob-service.js'; import { useCurrentUserFID } from '../utils/farcaster-utils.js'; import { promiseAll } from '../utils/promises.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken, createDefaultHTTPRequestHeaders, } from '../utils/services-utils.js'; +import sleep from '../utils/sleep.js'; + +type JoinStatus = 'inactive' | 'joining' | 'joined'; type CommunityToAutoJoin = { + +batch: number, +communityID: string, +keyserverOverride: ?KeyserverOverride, - +joinStatus: 'inactive' | 'joining' | 'joined', + +joinStatus: JoinStatus, }; -type CommunitiesToAutoJoin = { +type CommunityDatas = { +[communityID: string]: CommunityToAutoJoin, }; +type CommunitiesToAutoJoin = { + +curBatch: number, + +communityDatas: CommunityDatas, +}; + type Props = { +calendarQuery: () => CalendarQuery, }; function BaseAutoJoinCommunityHandler(props: Props): React.Node { const { calendarQuery } = props; const isActive = useSelector(state => state.lifecycleState !== 'background'); const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const fid = useCurrentUserFID(); const neynarClient = React.useContext(NeynarClientContext)?.client; const identityClientContext = React.useContext(IdentityClientContext); invariant(identityClientContext, 'IdentityClientContext should be set'); const { getAuthMetadata } = identityClientContext; const threadInfos = useSelector(state => state.threadStore.threadInfos); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); - const [communitiesToAutoJoin, setCommunitiesToAutoJoin] = + const [communitiesToAutoJoin, baseSetCommunitiesToAutoJoin] = React.useState(); const prevCanQueryRef = React.useRef(); const canQuery = loggedIn; React.useEffect(() => { if (canQuery === prevCanQueryRef.current) { return; } prevCanQueryRef.current = canQuery; if (!loggedIn || !isActive || !fid || !neynarClient || !threadInfos) { return; } void (async () => { const authMetadataPromise: Promise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } return await getAuthMetadata(); })(); const followedFarcasterChannelsPromise = neynarClient.fetchFollowedFarcasterChannels(fid); const [authMetadata, followedFarcasterChannels] = await Promise.all([ authMetadataPromise, followedFarcasterChannelsPromise, ]); const headers = authMetadata ? createDefaultHTTPRequestHeaders(authMetadata) : {}; const followedFarcasterChannelIDs = followedFarcasterChannels.map( channel => channel.id, ); - const promises: { [string]: Promise } = {}; + const promises: { + [string]: Promise>, + } = {}; for (const channelID of followedFarcasterChannelIDs) { promises[channelID] = (async () => { const blobHash = farcasterChannelTagBlobHash(channelID); const blobURL = getBlobFetchableURL(blobHash); const blobResult = await fetch(blobURL, { method: blobService.httpEndpoints.GET_BLOB.method, headers, }); if (blobResult.status !== 200) { return null; } const { commCommunityID, keyserverURL } = await blobResult.json(); const keyserverID = extractKeyserverIDFromID(commCommunityID); // The user is already in the community if (threadInfos[commCommunityID]) { return null; } const keyserverOverride = !keyserverInfos[keyserverID] ? { keyserverID, keyserverURL: keyserverURL.replace(/\/$/, ''), } : null; return { communityID: commCommunityID, keyserverOverride, joinStatus: 'inactive', }; })(); } const communitiesObj = await promiseAll(promises); const filteredCommunitiesObj = _pickBy(Boolean)(communitiesObj); - const communitesToJoin: { ...CommunitiesToAutoJoin } = {}; + const communityDatas: { ...CommunityDatas } = {}; + let i = 0; for (const key in filteredCommunitiesObj) { - const communityID = filteredCommunitiesObj[key].communityID; - communitesToJoin[communityID] = filteredCommunitiesObj[key]; + const communityObject = filteredCommunitiesObj[key]; + const communityID = communityObject.communityID; + communityDatas[communityID] = { + ...communityObject, + batch: Math.floor(i++ / 5), + }; } - setCommunitiesToAutoJoin(communitesToJoin); + baseSetCommunitiesToAutoJoin({ communityDatas, curBatch: 0 }); })(); }, [ threadInfos, fid, isActive, loggedIn, neynarClient, getAuthMetadata, keyserverInfos, canQuery, ]); + const potentiallyIncrementBatch: ( + ?CommunitiesToAutoJoin, + ) => ?CommunitiesToAutoJoin = React.useCallback(input => { + if (!input) { + return input; + } + + let shouldIncrementBatch = false; + const { curBatch, communityDatas } = input; + for (const communityToAutoJoin of Object.values(communityDatas)) { + const { batch, joinStatus } = communityToAutoJoin; + + if (batch !== curBatch) { + continue; + } + + if (joinStatus !== 'joined') { + // One of the current batch isn't complete yet + return input; + } + + // We have at least one complete in the current batch + shouldIncrementBatch = true; + } + + // If we get here, all of the current batch is complete + if (shouldIncrementBatch) { + return { communityDatas, curBatch: curBatch + 1 }; + } + + return input; + }, []); + + const setCommunitiesToAutoJoin: SetState = + React.useCallback( + next => { + if (typeof next !== 'function') { + baseSetCommunitiesToAutoJoin(potentiallyIncrementBatch(next)); + return; + } + baseSetCommunitiesToAutoJoin(prev => { + const result = next(prev); + return potentiallyIncrementBatch(result); + }); + }, + [potentiallyIncrementBatch], + ); + const joinHandlers = React.useMemo(() => { if (!communitiesToAutoJoin) { return null; } - return Object.keys(communitiesToAutoJoin).map(id => { - const communityToAutoJoin = communitiesToAutoJoin[id]; + const { curBatch, communityDatas } = communitiesToAutoJoin; - const { communityID, keyserverOverride, joinStatus } = - communityToAutoJoin; + return Object.values(communityDatas).map(communityData => { + const { batch, communityID, keyserverOverride, joinStatus } = + communityData; - if (joinStatus === 'joined') { + if (batch !== curBatch || joinStatus === 'joined') { return null; } return ( ); }); - }, [calendarQuery, communitiesToAutoJoin]); + }, [calendarQuery, communitiesToAutoJoin, setCommunitiesToAutoJoin]); return joinHandlers; } type JoinHandlerProps = { +communityID: string, +keyserverOverride: ?KeyserverOverride, +calendarQuery: () => CalendarQuery, - +communitiesToAutoJoin: CommunitiesToAutoJoin, + +joinStatus: JoinStatus, +setCommunitiesToAutoJoin: SetState, }; function JoinHandler(props: JoinHandlerProps) { const { communityID, keyserverOverride, calendarQuery, - communitiesToAutoJoin, + joinStatus, setCommunitiesToAutoJoin, } = props; const [ongoingJoinData, setOngoingJoinData] = React.useState(null); const [step, setStep] = React.useState('inactive'); const joinCommunity = useJoinCommunity({ communityID, keyserverOverride, calendarQuery, ongoingJoinData, setOngoingJoinData, step, setStep, defaultSubscription: defaultThreadSubscription, }); - React.useEffect(() => { - const joinStatus = communitiesToAutoJoin[communityID]?.joinStatus; - if (joinStatus !== 'inactive') { - return; - } - - void joinCommunity(); - }, [ - communitiesToAutoJoin, - communityID, - joinCommunity, - setCommunitiesToAutoJoin, - ]); - - React.useEffect(() => { - if (step !== 'add_keyserver') { - return; - } - - setCommunitiesToAutoJoin(prev => { - if (!prev) { - return null; - } + const setJoinStatus = React.useCallback( + (newJoinStatus: JoinStatus) => { + setCommunitiesToAutoJoin(prev => { + if (!prev) { + return null; + } - return { - ...prev, - [communityID]: { - ...prev[communityID], - joinStatus: 'joining', - }, - }; - }); - }, [communityID, setCommunitiesToAutoJoin, step]); + return { + ...prev, + communityDatas: { + ...prev.communityDatas, + [communityID]: { + ...prev.communityDatas[communityID], + joinStatus: newJoinStatus, + }, + }, + }; + }); + }, + [communityID, setCommunitiesToAutoJoin], + ); React.useEffect(() => { - if (step !== 'finished') { + if (joinStatus !== 'inactive') { return; } - - setCommunitiesToAutoJoin(prev => { - if (!prev) { - return null; + void (async () => { + try { + setJoinStatus('joining'); + await sleep(1000); + await joinCommunity(); + } finally { + setJoinStatus('joined'); } - - return { - ...prev, - [communityID]: { - ...prev[communityID], - joinStatus: 'joined', - }, - }; - }); - }, [communityID, step, setCommunitiesToAutoJoin]); + })(); + }, [joinStatus, communityID, setJoinStatus, joinCommunity]); return null; } export { BaseAutoJoinCommunityHandler };