diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js new file mode 100644 --- /dev/null +++ b/native/components/auto-join-community-handler.react.js @@ -0,0 +1,161 @@ +// @flow + +import invariant from 'invariant'; +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 { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; +import { cookieSelector } from 'lib/selectors/keyserver-selectors.js'; +import { farcasterChannelTagBlobHash } 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 { authoritativeKeyserverID } from 'lib/utils/authoritative-keyserver.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 { + 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'; + +function AutoJoinCommunityHandler(): React.Node { + const isActive = useSelector(state => state.lifecycleState !== 'background'); + + const cookie = useSelector(cookieSelector(authoritativeKeyserverID())); + const hasUserCookie = !!(cookie && cookie.startsWith('user=')); + const currentUserID = useSelector(state => state.currentUserInfo?.id); + const loggedIn = !!currentUserID && hasUserCookie; + + 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] }, + ], + }, + }); + }, + [calendarQuery, joinThread], + ); + + const dispatchActionPromise = useDispatchActionPromise(); + + const threadInfos = useSelector(state => state.threadStore.threadInfos); + + const keyserverInfos = useSelector( + state => state.keyserverStore.keyserverInfos, + ); + + React.useEffect(() => { + 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 blobResult = await fetch(blobURL, { + method: blobService.httpEndpoints.GET_BLOB.method, + headers, + }); + + if (blobResult.status !== 200) { + return; + } + + const { commCommunityID } = await blobResult.json(); + const keyserverID = extractKeyserverIDFromID(commCommunityID); + + if (!keyserverInfos[keyserverID]) { + return; + } + + // The user is already in the community + if (threadInfos[commCommunityID]) { + return; + } + + void dispatchActionPromise( + joinThreadActionTypes, + joinThreadActionPromise(commCommunityID), + ); + }); + + await Promise.all(promises); + })(); + }, [ + threadInfos, + dispatchActionPromise, + fid, + isActive, + joinThreadActionPromise, + loggedIn, + neynarClient, + getAuthMetadata, + keyserverInfos, + ]); + + return null; +} + +export { AutoJoinCommunityHandler }; diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -48,6 +48,7 @@ import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import AccessTokenHandler from './components/access-token-handler.react.js'; +import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js'; import BackgroundIdentityLoginHandler from './components/background-identity-login-handler.react.js'; import ConnectFarcasterAlertHandler from './components/connect-farcaster-alert-handler.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; @@ -346,6 +347,7 @@ + {navigation}