diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js --- a/lib/hooks/invite-links.js +++ b/lib/hooks/invite-links.js @@ -2,7 +2,6 @@ import * as React from 'react'; -import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { useCreateOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, @@ -16,13 +15,14 @@ import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import { threadInfoSelector } from '../selectors/thread-selectors.js'; -import { isLoggedInToKeyserver } from '../selectors/user-selectors.js'; +import { useJoinCommunity } from '../shared/community-utils.js'; import type { KeyserverOverride } from '../shared/invite-links.js'; -import { useIsKeyserverURLValid } from '../shared/keyserver-utils.js'; -import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.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 { defaultKeyserverInfo } from '../types/keyserver-types.js'; import type { InviteLink, InviteLinkVerificationResponse, @@ -30,7 +30,7 @@ import type { LoadingStatus } from '../types/loading-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useDispatch, useSelector } from '../utils/redux-utils.js'; +import { useSelector } from '../utils/redux-utils.js'; const createOrUpdatePublicLinkStatusSelector = createLoadingStatusSelector( createOrUpdatePublicLinkActionTypes, @@ -178,8 +178,8 @@ navigateToThread, } = params; - const dispatch = useDispatch(); const callJoinThread = useJoinThread(); + const dispatchActionPromise = useDispatchActionPromise(); const communityID = verificationResponse.community?.id; let keyserverID = keyserverOverride?.keyserverID; @@ -187,165 +187,23 @@ keyserverID = extractKeyserverIDFromID(communityID); } - const isKeyserverKnown = useSelector(state => - keyserverID ? !!state.keyserverStore.keyserverInfos[keyserverID] : false, - ); - const isAuthenticated = useSelector(isLoggedInToKeyserver(keyserverID)); - - const keyserverURL = keyserverOverride?.keyserverURL; - const isKeyserverURLValid = useIsKeyserverURLValid(keyserverURL); - - const [ongoingJoinData, setOngoingJoinData] = React.useState mixed, - +reject: () => mixed, - +communityID: string, - +threadID: ?string, - }>(null); - const timeoutRef = React.useRef(); + const [ongoingJoinData, setOngoingJoinData] = + React.useState(null); - React.useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - const [step, setStep] = React.useState< - | 'inactive' - | 'add_keyserver' - | 'auth_to_keyserver' - | 'join_community' - | 'join_thread' - | 'finished', - >('inactive'); + const [step, setStep] = React.useState('inactive'); - const createJoinPromise = React.useCallback(() => { - return new Promise((resolve, reject) => { - if ( - !keyserverID || - !communityID || - keyserverID !== extractKeyserverIDFromID(communityID) - ) { - reject(); - setLinkStatus('invalid'); - setOngoingJoinData(null); - return; - } - const timeoutID = setTimeout(() => { - reject(); - setOngoingJoinData(oldData => { - if (oldData) { - setLinkStatus('timed_out'); - } - return null; - }); - }, permissionsAndAuthRelatedRequestTimeout); - timeoutRef.current = timeoutID; - const resolveAndClearTimeout = () => { - clearTimeout(timeoutID); - resolve(); - }; - const rejectAndClearTimeout = () => { - clearTimeout(timeoutID); - reject(); - }; - setOngoingJoinData({ - resolve: resolveAndClearTimeout, - reject: rejectAndClearTimeout, - communityID, - threadID: verificationResponse.thread?.id, - }); - setStep('add_keyserver'); - }); - }, [ + const joinCommunity = useJoinCommunity({ communityID, - keyserverID, - setLinkStatus, - verificationResponse.thread?.id, - ]); - - React.useEffect(() => { - void (async () => { - if (!ongoingJoinData || step !== 'add_keyserver') { - return; - } - if (isKeyserverKnown) { - setStep('auth_to_keyserver'); - return; - } - - const isValid = await isKeyserverURLValid(); - if (!isValid || !keyserverURL) { - setLinkStatus('invalid'); - ongoingJoinData.reject(); - setOngoingJoinData(null); - return; - } - dispatch({ - type: addKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverID, - newKeyserverInfo: defaultKeyserverInfo(keyserverURL), - }, - }); - })(); - }, [ - dispatch, - isKeyserverKnown, - isKeyserverURLValid, - keyserverID, - keyserverURL, + keyserverOverride, + calendarQuery, ongoingJoinData, - setLinkStatus, + setOngoingJoinData, step, - ]); - - React.useEffect(() => { - if (step === 'auth_to_keyserver' && ongoingJoinData && isAuthenticated) { - setStep('join_community'); - } - }, [isAuthenticated, ongoingJoinData, step]); - - const dispatchActionPromise = useDispatchActionPromise(); - React.useEffect(() => { - void (async () => { - if (!ongoingJoinData || step !== 'join_community') { - return; - } - const threadID = ongoingJoinData.communityID; - const query = calendarQuery(); - const joinThreadPromise = callJoinThread({ - threadID, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [threadID] }, - ], - }, - inviteLinkSecret: inviteSecret, - }); - void dispatchActionPromise(joinThreadActionTypes, joinThreadPromise); - - try { - await joinThreadPromise; - setStep('join_thread'); - } catch (e) { - setLinkStatus(status => (status === 'valid' ? 'invalid' : status)); - ongoingJoinData.reject(); - setOngoingJoinData(null); - } - })(); - }, [ - calendarQuery, - callJoinThread, - dispatchActionPromise, + setStep, inviteSecret, - ongoingJoinData, setLinkStatus, - step, - ]); + threadID: verificationResponse.thread?.id, + }); React.useEffect(() => { void (async () => { @@ -393,6 +251,8 @@ inviteSecret, ongoingJoinData, setLinkStatus, + setOngoingJoinData, + setStep, step, ]); @@ -430,10 +290,10 @@ return React.useMemo( () => ({ - join: createJoinPromise, + join: joinCommunity, joinLoadingStatus, }), - [createJoinPromise, joinLoadingStatus], + [joinCommunity, joinLoadingStatus], ); } diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -9,14 +9,31 @@ deleteFarcasterChannelTagActionTypes, useDeleteFarcasterChannelTag, } from '../actions/community-actions.js'; +import { addKeyserverActionType } from '../actions/keyserver-actions.js'; +import { + joinThreadActionTypes, + useJoinThread, +} from '../actions/thread-actions.js'; +import type { LinkStatus } from '../hooks/invite-links.js'; +import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; +import { isLoggedInToKeyserver } from '../selectors/user-selectors.js'; +import type { KeyserverOverride } from '../shared/invite-links.js'; +import { useIsKeyserverURLValid } from '../shared/keyserver-utils.js'; +import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; +import type { + JoinCommunityStep, + OngoingJoinCommunityData, +} from '../types/community-types.js'; +import type { CalendarQuery } from '../types/entry-types.js'; import type { SetState } from '../types/hook-types.js'; +import { defaultKeyserverInfo } from '../types/keyserver-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import { useCurrentUserFID } from '../utils/farcaster-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useDispatch, useSelector } from '../utils/redux-utils.js'; const tagFarcasterChannelCopy = { DESCRIPTION: @@ -160,6 +177,195 @@ ); } +type UseJoinCommunityParams = { + +communityID: ?string, + +keyserverOverride: ?KeyserverOverride, + +calendarQuery: () => CalendarQuery, + +ongoingJoinData: ?OngoingJoinCommunityData, + +setOngoingJoinData: SetState, + +step: JoinCommunityStep, + +setStep: SetState, + +inviteSecret: string, + +setLinkStatus: SetState, + +threadID?: string, +}; +function useJoinCommunity(params: UseJoinCommunityParams): () => Promise { + const { + communityID, + keyserverOverride, + calendarQuery, + ongoingJoinData, + setOngoingJoinData, + step, + setStep, + inviteSecret, + setLinkStatus, + threadID, + } = params; + + const dispatch = useDispatch(); + const dispatchActionPromise = useDispatchActionPromise(); + const callJoinThread = useJoinThread(); + + let keyserverID = keyserverOverride?.keyserverID; + if (!keyserverID && communityID) { + keyserverID = extractKeyserverIDFromID(communityID); + } + + const isKeyserverKnown = useSelector(state => + keyserverID ? !!state.keyserverStore.keyserverInfos[keyserverID] : false, + ); + const isAuthenticated = useSelector(isLoggedInToKeyserver(keyserverID)); + + const keyserverURL = keyserverOverride?.keyserverURL; + const isKeyserverURLValid = useIsKeyserverURLValid(keyserverURL); + + const timeoutRef = React.useRef(); + + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const createJoinPromise = React.useCallback(() => { + return new Promise((resolve, reject) => { + if ( + !keyserverID || + !communityID || + keyserverID !== extractKeyserverIDFromID(communityID) + ) { + reject(); + setLinkStatus('invalid'); + setOngoingJoinData(null); + return; + } + const timeoutID = setTimeout(() => { + reject(); + setOngoingJoinData(oldData => { + if (oldData) { + setLinkStatus('timed_out'); + } + return null; + }); + }, permissionsAndAuthRelatedRequestTimeout); + timeoutRef.current = timeoutID; + const resolveAndClearTimeout = () => { + clearTimeout(timeoutID); + resolve(); + }; + const rejectAndClearTimeout = () => { + clearTimeout(timeoutID); + reject(); + }; + setOngoingJoinData({ + resolve: resolveAndClearTimeout, + reject: rejectAndClearTimeout, + communityID, + threadID, + }); + setStep('add_keyserver'); + }); + }, [ + communityID, + keyserverID, + setLinkStatus, + setOngoingJoinData, + setStep, + threadID, + ]); + + React.useEffect(() => { + void (async () => { + if (!ongoingJoinData || step !== 'add_keyserver') { + return; + } + if (isKeyserverKnown) { + setStep('auth_to_keyserver'); + return; + } + + const isValid = await isKeyserverURLValid(); + if (!isValid || !keyserverURL) { + setLinkStatus('invalid'); + ongoingJoinData.reject(); + setOngoingJoinData(null); + return; + } + dispatch({ + type: addKeyserverActionType, + payload: { + keyserverAdminUserID: keyserverID, + newKeyserverInfo: defaultKeyserverInfo(keyserverURL), + }, + }); + })(); + }, [ + dispatch, + isKeyserverKnown, + isKeyserverURLValid, + keyserverID, + keyserverURL, + ongoingJoinData, + setLinkStatus, + setOngoingJoinData, + setStep, + step, + ]); + + React.useEffect(() => { + if (step === 'auth_to_keyserver' && ongoingJoinData && isAuthenticated) { + setStep('join_community'); + } + }, [isAuthenticated, ongoingJoinData, setStep, step]); + + React.useEffect(() => { + void (async () => { + if (!ongoingJoinData || step !== 'join_community') { + return; + } + const communityThreadID = ongoingJoinData.communityID; + const query = calendarQuery(); + const joinThreadPromise = callJoinThread({ + threadID: communityThreadID, + calendarQuery: { + startDate: query.startDate, + endDate: query.endDate, + filters: [ + ...query.filters, + { type: 'threads', threadIDs: [communityThreadID] }, + ], + }, + inviteLinkSecret: inviteSecret, + }); + void dispatchActionPromise(joinThreadActionTypes, joinThreadPromise); + + try { + await joinThreadPromise; + setStep('join_thread'); + } catch (e) { + setLinkStatus(status => (status === 'valid' ? 'invalid' : status)); + ongoingJoinData.reject(); + setOngoingJoinData(null); + } + })(); + }, [ + calendarQuery, + callJoinThread, + dispatchActionPromise, + inviteSecret, + ongoingJoinData, + setLinkStatus, + setOngoingJoinData, + setStep, + step, + ]); + + return createJoinPromise; +} + export { tagFarcasterChannelCopy, tagFarcasterChannelErrorMessages, @@ -167,4 +373,5 @@ useCreateFarcasterChannelTag, useRemoveFarcasterChannelTag, useCanManageFarcasterChannelTag, + useJoinCommunity, }; diff --git a/lib/types/community-types.js b/lib/types/community-types.js --- a/lib/types/community-types.js +++ b/lib/types/community-types.js @@ -52,3 +52,18 @@ export type DeleteFarcasterChannelTagPayload = { +commCommunityID: string, }; + +export type OngoingJoinCommunityData = { + +resolve: () => mixed, + +reject: () => mixed, + +communityID: string, + +threadID: ?string, +}; + +export type JoinCommunityStep = + | 'inactive' + | 'add_keyserver' + | 'auth_to_keyserver' + | 'join_community' + | 'join_thread' + | 'finished';