diff --git a/native/components/auto-join-community-handler.react.js b/lib/components/base-auto-join-community-handler.react.js similarity index 82% copy from native/components/auto-join-community-handler.react.js copy to lib/components/base-auto-join-community-handler.react.js index 3cd2b0aa7..638e3811f 100644 --- a/native/components/auto-join-community-handler.react.js +++ b/lib/components/base-auto-join-community-handler.react.js @@ -1,298 +1,293 @@ // @flow import invariant from 'invariant'; import _pickBy from 'lodash/fp/pickBy.js'; import * as React from 'react'; -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 { 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 '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'; +} 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 '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 { promiseAll } from 'lib/utils/promises.js'; +} 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 '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'; +} from '../utils/services-utils.js'; type CommunityToAutoJoin = { +communityID: string, +keyserverOverride: ?KeyserverOverride, +joinStatus: 'inactive' | 'joining' | 'joined', }; type CommunitiesToAutoJoin = { +[communityID: string]: CommunityToAutoJoin, }; -function AutoJoinCommunityHandler(): React.Node { +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 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 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: { [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 } = {}; for (const key in filteredCommunitiesObj) { const communityID = filteredCommunitiesObj[key].communityID; communitesToJoin[communityID] = filteredCommunitiesObj[key]; } setCommunitiesToAutoJoin(communitesToJoin); })(); }, [ threadInfos, fid, isActive, 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 }; +export { BaseAutoJoinCommunityHandler }; diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js index 3cd2b0aa7..8cb46eb5d 100644 --- a/native/components/auto-join-community-handler.react.js +++ b/native/components/auto-join-community-handler.react.js @@ -1,298 +1,24 @@ // @flow -import invariant from 'invariant'; -import _pickBy from 'lodash/fp/pickBy.js'; import * as React from 'react'; -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, - 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 { promiseAll } from 'lib/utils/promises.js'; -import { - usingCommServicesAccessToken, - createDefaultHTTPRequestHeaders, -} from 'lib/utils/services-utils.js'; +import { BaseAutoJoinCommunityHandler } from 'lib/components/base-auto-join-community-handler.react.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 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: { [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 } = {}; - - for (const key in filteredCommunitiesObj) { - const communityID = filteredCommunitiesObj[key].communityID; - communitesToJoin[communityID] = filteredCommunitiesObj[key]; - } - - setCommunitiesToAutoJoin(communitesToJoin); - })(); - }, [ - threadInfos, - fid, - isActive, - 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; + return ; } export { AutoJoinCommunityHandler };