diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js index 5061bd5d9..3cd2b0aa7 100644 --- a/native/components/auto-join-community-handler.react.js +++ b/native/components/auto-join-community-handler.react.js @@ -1,159 +1,298 @@ // @flow import invariant from 'invariant'; +import _pickBy from 'lodash/fp/pickBy.js'; import * as React from 'react'; -import { - joinThreadActionTypes, - useJoinThread, -} from 'lib/actions/thread-actions.js'; import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; import blobService from 'lib/facts/blob-service.js'; import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; -import { farcasterChannelTagBlobHash } from 'lib/shared/community-utils.js'; +import { + farcasterChannelTagBlobHash, + useJoinCommunity, +} from 'lib/shared/community-utils.js'; import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { KeyserverOverride } from 'lib/shared/invite-links.js'; +import type { + OngoingJoinCommunityData, + JoinCommunityStep, +} from 'lib/types/community-types.js'; +import type { CalendarQuery } from 'lib/types/entry-types.js'; +import type { SetState } from 'lib/types/hook-types.js'; import { defaultThreadSubscription } from 'lib/types/subscription-types.js'; import { getBlobFetchableURL } from 'lib/utils/blob-service.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; -import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; +import { promiseAll } from 'lib/utils/promises.js'; import { usingCommServicesAccessToken, createDefaultHTTPRequestHeaders, } from 'lib/utils/services-utils.js'; import { nonThreadCalendarQuery } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; +type CommunityToAutoJoin = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, + +joinStatus: 'inactive' | 'joining' | 'joined', +}; + +type CommunitiesToAutoJoin = { + +[communityID: string]: CommunityToAutoJoin, +}; + function AutoJoinCommunityHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const fid = useCurrentUserFID(); const neynarClient = React.useContext(NeynarClientContext)?.client; const navContext = React.useContext(NavContext); const identityClientContext = React.useContext(IdentityClientContext); invariant(identityClientContext, 'IdentityClientContext should be set'); const { getAuthMetadata } = identityClientContext; const calendarQuery = useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); - const joinThread = useJoinThread(); - - const joinThreadActionPromise = React.useCallback( - async (communityID: string) => { - const query = calendarQuery(); - - return await joinThread({ - threadID: communityID, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [communityID] }, - ], - }, - defaultSubscription: defaultThreadSubscription, - }); - }, - [calendarQuery, joinThread], - ); - - const dispatchActionPromise = useDispatchActionPromise(); - const threadInfos = useSelector(state => state.threadStore.threadInfos); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); + const [communitiesToAutoJoin, setCommunitiesToAutoJoin] = + 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 = followedFarcasterChannelIDs.map(async channelID => { - const blobHash = farcasterChannelTagBlobHash(channelID); - const blobURL = getBlobFetchableURL(blobHash); + const promises: { [string]: Promise } = {}; - const blobResult = await fetch(blobURL, { - method: blobService.httpEndpoints.GET_BLOB.method, - headers, - }); + for (const channelID of followedFarcasterChannelIDs) { + promises[channelID] = (async () => { + const blobHash = farcasterChannelTagBlobHash(channelID); + const blobURL = getBlobFetchableURL(blobHash); - if (blobResult.status !== 200) { - return; - } + const blobResult = await fetch(blobURL, { + method: blobService.httpEndpoints.GET_BLOB.method, + headers, + }); - const { commCommunityID } = await blobResult.json(); - const keyserverID = extractKeyserverIDFromID(commCommunityID); + if (blobResult.status !== 200) { + return null; + } - if (!keyserverInfos[keyserverID]) { - return; - } + const { commCommunityID, keyserverURL } = await blobResult.json(); + const keyserverID = extractKeyserverIDFromID(commCommunityID); - // The user is already in the community - if (threadInfos[commCommunityID]) { - return; - } + // 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); - void dispatchActionPromise( - joinThreadActionTypes, - joinThreadActionPromise(commCommunityID), - ); - }); + const filteredCommunitiesObj = _pickBy(Boolean)(communitiesObj); - await Promise.all(promises); + const communitesToJoin: { ...CommunitiesToAutoJoin } = {}; + + for (const key in filteredCommunitiesObj) { + const communityID = filteredCommunitiesObj[key].communityID; + communitesToJoin[communityID] = filteredCommunitiesObj[key]; + } + + setCommunitiesToAutoJoin(communitesToJoin); })(); }, [ threadInfos, - dispatchActionPromise, fid, isActive, - joinThreadActionPromise, loggedIn, neynarClient, getAuthMetadata, keyserverInfos, + canQuery, ]); + const joinHandlers = React.useMemo(() => { + if (!communitiesToAutoJoin) { + return null; + } + + return Object.keys(communitiesToAutoJoin).map(id => { + const communityToAutoJoin = communitiesToAutoJoin[id]; + + const { communityID, keyserverOverride, joinStatus } = + communityToAutoJoin; + + if (joinStatus === 'joined') { + return null; + } + + return ( + + ); + }); + }, [calendarQuery, communitiesToAutoJoin]); + + return joinHandlers; +} + +type JoinHandlerProps = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, + +calendarQuery: () => CalendarQuery, + +communitiesToAutoJoin: CommunitiesToAutoJoin, + +setCommunitiesToAutoJoin: SetState, +}; +function JoinHandler(props: JoinHandlerProps) { + const { + communityID, + keyserverOverride, + calendarQuery, + communitiesToAutoJoin, + 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; + } + + return { + ...prev, + [communityID]: { + ...prev[communityID], + joinStatus: 'joining', + }, + }; + }); + }, [communityID, setCommunitiesToAutoJoin, step]); + + React.useEffect(() => { + if (step !== 'finished') { + return; + } + + setCommunitiesToAutoJoin(prev => { + if (!prev) { + return null; + } + + return { + ...prev, + [communityID]: { + ...prev[communityID], + joinStatus: 'joined', + }, + }; + }); + }, [communityID, step, setCommunitiesToAutoJoin]); + return null; } export { AutoJoinCommunityHandler };