diff --git a/lib/components/base-auto-join-community-handler.react.js b/lib/components/base-auto-join-community-handler.react.js new file mode 100644 --- /dev/null +++ b/lib/components/base-auto-join-community-handler.react.js @@ -0,0 +1,217 @@ +// @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 { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import { isLoggedInToIdentityAndAuthoritativeKeyserver } from '../selectors/user-selectors.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 { defaultThreadSubscription } from '../types/subscription-types.js'; +import { getBlobFetchableURL } from '../utils/blob-service.js'; +import { useCurrentUserFID } from '../utils/farcaster-utils.js'; +import { values } from '../utils/objects.js'; +import { promiseAll } from '../utils/promises.js'; +import { useSelector } from '../utils/redux-utils.js'; +import { + usingCommServicesAccessToken, + createDefaultHTTPRequestHeaders, +} from '../utils/services-utils.js'; + +type CommunityToAutoJoin = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, +}; + +type Props = { + +calendarQuery: () => CalendarQuery, +}; +function BaseAutoJoinCommunityHandler(props: Props): React.Node { + const { calendarQuery } = props; + + const isActive = useSelector(state => state.lifecycleState !== 'background'); + + const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + + 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] = + React.useState>(); + + const prevCanQueryRef = React.useRef(); + const canQuery = loggedIn; + + React.useEffect(() => { + if (canQuery === prevCanQueryRef.current) { + setCommunitiesToAutoJoin(null); + return; + } + + prevCanQueryRef.current = canQuery; + if (!loggedIn || !isActive || !fid || !neynarClient || !threadInfos) { + return; + } + + console.log('running auto join effect'); + + 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, + }; + })(); + } + + const communitiesObj = await promiseAll(promises); + + const filteredCommunitiesObj = _pickBy(Boolean)(communitiesObj); + + const communities = values(filteredCommunitiesObj); + + if (communities.length === 0) { + return; + } + + console.log('setting communities to auto join'); + setCommunitiesToAutoJoin(communities); + })(); + }, [ + threadInfos, + fid, + isActive, + loggedIn, + neynarClient, + getAuthMetadata, + keyserverInfos, + canQuery, + ]); + + const joinHandlers = React.useMemo( + () => + communitiesToAutoJoin?.map((communityToAutoJoin, index) => ( + + )), + [calendarQuery, communitiesToAutoJoin], + ); + + return joinHandlers; +} + +type JoinHandlerProps = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, + +calendarQuery: () => CalendarQuery, +}; +function JoinHandler(props: JoinHandlerProps) { + const { communityID, keyserverOverride, calendarQuery } = props; + + const [ongoingJoinData, setOngoingJoinData] = + React.useState(null); + + const [step, setStep] = React.useState('inactive'); + + console.log('regenerating useJoinCommunity'); + + const joinCommunity = useJoinCommunity({ + communityID, + keyserverOverride, + calendarQuery, + ongoingJoinData, + setOngoingJoinData, + step, + setStep, + defaultSubscription: defaultThreadSubscription, + }); + + React.useEffect(() => { + void joinCommunity(); + }, [joinCommunity]); + + return null; +} + +export { BaseAutoJoinCommunityHandler }; diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js --- a/native/components/auto-join-community-handler.react.js +++ b/native/components/auto-join-community-handler.react.js @@ -1,23 +1,29 @@ // @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 { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.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 { 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 { values } from 'lib/utils/objects.js'; +import { promiseAll } from 'lib/utils/promises.js'; import { usingCommServicesAccessToken, createDefaultHTTPRequestHeaders, @@ -27,6 +33,11 @@ import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; +type CommunityToAutoJoin = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, +}; + function AutoJoinCommunityHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); @@ -36,54 +47,35 @@ 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; } + console.log('running auto join effect'); + void (async () => { const authMetadataPromise: Promise = (async () => { if (!usingCommServicesAccessToken) { @@ -108,51 +100,121 @@ 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; + } - void dispatchActionPromise( - joinThreadActionTypes, - joinThreadActionPromise(commCommunityID), - ); - }); + const keyserverOverride = !keyserverInfos[keyserverID] + ? { + keyserverID, + keyserverURL: keyserverURL.replace(/\/$/, ''), + } + : null; - await Promise.all(promises); + return { + communityID: commCommunityID, + keyserverOverride, + }; + })(); + } + + const communitiesObj = await promiseAll(promises); + + const filteredCommunitiesObj = _pickBy(Boolean)(communitiesObj); + + const communities = values(filteredCommunitiesObj); + + if (communities.length === 0) { + return; + } + + console.log('setting communities to auto join'); + setCommunitiesToAutoJoin(communities); })(); }, [ threadInfos, - dispatchActionPromise, fid, isActive, - joinThreadActionPromise, loggedIn, neynarClient, getAuthMetadata, keyserverInfos, + canQuery, ]); + const joinHandlers = React.useMemo( + () => + communitiesToAutoJoin?.map(communityToAutoJoin => ( + + )), + [communitiesToAutoJoin], + ); + + return joinHandlers; +} + +type JoinHandlerProps = { + +communityID: string, + +keyserverOverride: ?KeyserverOverride, +}; +function JoinHandler(props: JoinHandlerProps) { + const { communityID, keyserverOverride } = props; + + const navContext = React.useContext(NavContext); + + const calendarQuery = useSelector(state => + nonThreadCalendarQuery({ + redux: state, + navContext, + }), + ); + + const [ongoingJoinData, setOngoingJoinData] = + React.useState(null); + + const [step, setStep] = React.useState('inactive'); + + console.log('regenerating useJoinCommunity'); + + const joinCommunity = useJoinCommunity({ + communityID, + keyserverOverride, + calendarQuery, + ongoingJoinData, + setOngoingJoinData, + step, + setStep, + defaultSubscription: defaultThreadSubscription, + }); + + React.useEffect(() => { + void joinCommunity(); + }, [joinCommunity]); + return null; }