diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js index 04de0aa45..fd4a109ac 100644 --- a/lib/hooks/invite-links.js +++ b/lib/hooks/invite-links.js @@ -1,440 +1,242 @@ // @flow import * as React from 'react'; -import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { useCreateOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, useDisableInviteLink, disableInviteLinkLinkActionTypes, } from '../actions/link-actions.js'; -import { - joinThreadActionTypes, - useJoinThread, -} from '../actions/thread-actions.js'; 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, } from '../types/link-types.js'; 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, ); const disableInviteLinkStatusSelector = createLoadingStatusSelector( disableInviteLinkLinkActionTypes, ); function useInviteLinksActions( communityID: string, inviteLink: ?InviteLink, ): { +error: ?string, +isLoading: boolean, +isChanged: boolean, +name: string, +setName: SetState, +createOrUpdateInviteLink: () => mixed, +disableInviteLink: () => mixed, } { const [name, setName] = React.useState( inviteLink?.name ?? Math.random().toString(36).slice(-9), ); const [error, setError] = React.useState(null); const dispatchActionPromise = useDispatchActionPromise(); const callCreateOrUpdatePublicLink = useCreateOrUpdatePublicLink(); const createCreateOrUpdateActionPromise = React.useCallback(async () => { setError(null); try { return await callCreateOrUpdatePublicLink({ name, communityID, }); } catch (e) { setError(e.message); throw e; } }, [callCreateOrUpdatePublicLink, communityID, name]); const createOrUpdateInviteLink = React.useCallback(() => { void dispatchActionPromise( createOrUpdatePublicLinkActionTypes, createCreateOrUpdateActionPromise(), ); }, [createCreateOrUpdateActionPromise, dispatchActionPromise]); const disableInviteLinkServerCall = useDisableInviteLink(); const createDisableLinkActionPromise = React.useCallback(async () => { setError(null); try { return await disableInviteLinkServerCall({ name, communityID, }); } catch (e) { setError(e.message); throw e; } }, [disableInviteLinkServerCall, communityID, name]); const disableInviteLink = React.useCallback(() => { void dispatchActionPromise( disableInviteLinkLinkActionTypes, createDisableLinkActionPromise(), ); }, [createDisableLinkActionPromise, dispatchActionPromise]); const disableInviteLinkStatus = useSelector(disableInviteLinkStatusSelector); const createOrUpdatePublicLinkStatus = useSelector( createOrUpdatePublicLinkStatusSelector, ); const isLoading = createOrUpdatePublicLinkStatus === 'loading' || disableInviteLinkStatus === 'loading'; return React.useMemo( () => ({ error, isLoading, isChanged: name !== inviteLink?.name, name, setName, createOrUpdateInviteLink, disableInviteLink, }), [ createOrUpdateInviteLink, disableInviteLink, error, inviteLink?.name, isLoading, name, ], ); } export type LinkStatus = 'invalid' | 'valid' | 'timed_out' | 'already_joined'; const inviteLinkTexts: { +[LinkStatus]: { +header: string, +message: (isThreadLink: boolean) => string, }, } = { invalid: { header: 'Invite invalid', message: () => 'This invite link may be expired. Please try again with another invite' + ' link.', }, ['timed_out']: { header: 'Timeout', message: () => 'The request has timed out.', }, ['already_joined']: { header: 'Already a member', message: isThreadLink => `You are already a member of this ${ isThreadLink ? 'thread' : 'community' }.`, }, }; type AcceptInviteLinkParams = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, +keyserverOverride: ?KeyserverOverride, +calendarQuery: () => CalendarQuery, +closeModal: () => mixed, +linkStatus: LinkStatus, +setLinkStatus: SetState, +navigateToThread: ThreadInfo => mixed, }; function useAcceptInviteLink(params: AcceptInviteLinkParams): { +join: () => mixed, +joinLoadingStatus: LoadingStatus, } { const { verificationResponse, inviteSecret, keyserverOverride, calendarQuery, closeModal, linkStatus, setLinkStatus, navigateToThread, } = params; - const dispatch = useDispatch(); - const callJoinThread = useJoinThread(); - const communityID = verificationResponse.community?.id; 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 [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, - ongoingJoinData, - setLinkStatus, - 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); - } - })(); - }, [ + keyserverOverride, calendarQuery, - callJoinThread, - dispatchActionPromise, - inviteSecret, ongoingJoinData, - setLinkStatus, + setOngoingJoinData, step, - ]); - - React.useEffect(() => { - void (async () => { - if (!ongoingJoinData || step !== 'join_thread') { - return; - } - const threadID = ongoingJoinData.threadID; - if (!threadID) { - setStep('finished'); - ongoingJoinData.resolve(); - setOngoingJoinData(null); - return; - } - - 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('finished'); - ongoingJoinData.resolve(); - } catch (e) { - setLinkStatus(status => (status === 'valid' ? 'invalid' : status)); - ongoingJoinData.reject(); - } finally { - setOngoingJoinData(null); - } - })(); - }, [ - calendarQuery, - callJoinThread, - dispatchActionPromise, + setStep, inviteSecret, - ongoingJoinData, setLinkStatus, - step, - ]); + threadID: verificationResponse.thread?.id, + }); const threadInfos = useSelector(threadInfoSelector); React.useEffect(() => { if (step !== 'finished') { return; } const joinedThreadID = verificationResponse.thread?.id; if (joinedThreadID && threadInfos[joinedThreadID]) { navigateToThread(threadInfos[joinedThreadID]); return; } const joinedCommunityID = verificationResponse.community?.id; if (!joinedCommunityID || !threadInfos[joinedCommunityID]) { closeModal(); return; } navigateToThread(threadInfos[joinedCommunityID]); }, [ closeModal, navigateToThread, step, threadInfos, verificationResponse.community?.id, verificationResponse.thread?.id, ]); let joinLoadingStatus: LoadingStatus = 'inactive'; if (linkStatus === 'invalid' || linkStatus === 'timed_out') { joinLoadingStatus = 'error'; } else if (step !== 'inactive' && step !== 'finished') { joinLoadingStatus = 'loading'; } return React.useMemo( () => ({ - join: createJoinPromise, + join: joinCommunity, joinLoadingStatus, }), - [createJoinPromise, joinLoadingStatus], + [joinCommunity, joinLoadingStatus], ); } export { useInviteLinksActions, useAcceptInviteLink, inviteLinkTexts }; diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js index bfc72f2da..fc5fea89f 100644 --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -1,170 +1,428 @@ // @flow import * as React from 'react'; import { useThreadHasPermission } from './thread-utils.js'; import { createOrUpdateFarcasterChannelTagActionTypes, useCreateOrUpdateFarcasterChannelTag, 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: 'Tag a Farcaster channel so followers can find your Comm community!', CHANNEL_NAME_HEADER: 'Selected channel:', NO_CHANNEL_TAGGED: 'No Farcaster channel tagged', REMOVE_TAG_BUTTON: 'Remove tag', }; const tagFarcasterChannelErrorMessages: { +[string]: string } = { already_in_use: 'This Farcaster channel is already tagged to a community.', channel_not_found: 'Could not find a channel with the provided name.', }; function farcasterChannelTagBlobHash(farcasterChannelID: string): string { return `farcaster_channel_tag_${farcasterChannelID}`; } const createOrUpdateFarcasterChannelTagStatusSelector = createLoadingStatusSelector(createOrUpdateFarcasterChannelTagActionTypes); function useCreateFarcasterChannelTag( commCommunityID: string, setError: SetState, onSuccessCallback?: () => mixed, ): { +createTag: (farcasterChannelID: string) => mixed, +isLoading: boolean, } { const dispatchActionPromise = useDispatchActionPromise(); const createOrUpdateFarcasterChannelTag = useCreateOrUpdateFarcasterChannelTag(); const createCreateOrUpdateActionPromise = React.useCallback( async (farcasterChannelID: string) => { try { const res = await createOrUpdateFarcasterChannelTag({ commCommunityID, farcasterChannelID, }); onSuccessCallback?.(); return res; } catch (e) { setError(e.message); throw e; } }, [ commCommunityID, createOrUpdateFarcasterChannelTag, onSuccessCallback, setError, ], ); const createTag = React.useCallback( (farcasterChannelID: string) => { void dispatchActionPromise( createOrUpdateFarcasterChannelTagActionTypes, createCreateOrUpdateActionPromise(farcasterChannelID), ); }, [createCreateOrUpdateActionPromise, dispatchActionPromise], ); const createOrUpdateFarcasterChannelTagStatus = useSelector( createOrUpdateFarcasterChannelTagStatusSelector, ); const isLoading = createOrUpdateFarcasterChannelTagStatus === 'loading'; return { createTag, isLoading, }; } const deleteFarcasterChannelTagStatusSelector = createLoadingStatusSelector( deleteFarcasterChannelTagActionTypes, ); function useRemoveFarcasterChannelTag( commCommunityID: string, farcasterChannelID: string, setError: SetState, ): { +removeTag: () => mixed, +isLoading: boolean, } { const dispatchActionPromise = useDispatchActionPromise(); const deleteFarcasterChannelTag = useDeleteFarcasterChannelTag(); const createDeleteActionPromise = React.useCallback(async () => { try { return await deleteFarcasterChannelTag({ commCommunityID, farcasterChannelID, }); } catch (e) { setError(e.message); throw e; } }, [ commCommunityID, deleteFarcasterChannelTag, farcasterChannelID, setError, ]); const removeTag = React.useCallback(() => { void dispatchActionPromise( deleteFarcasterChannelTagActionTypes, createDeleteActionPromise(), ); }, [createDeleteActionPromise, dispatchActionPromise]); const deleteFarcasterChannelTagStatus = useSelector( deleteFarcasterChannelTagStatusSelector, ); const isLoading = deleteFarcasterChannelTagStatus === 'loading'; return { removeTag, isLoading, }; } function useCanManageFarcasterChannelTag(community: ThreadInfo): boolean { const fid = useCurrentUserFID(); const canManageFarcasterChannelTag = useThreadHasPermission( community, threadPermissions.MANAGE_FARCASTER_CHANNEL_TAGS, ); return ( canManageFarcasterChannelTag && !!fid && community.type !== threadTypes.GENESIS ); } +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, + ]); + + React.useEffect(() => { + void (async () => { + if (!ongoingJoinData || step !== 'join_thread') { + return; + } + if (!threadID) { + setStep('finished'); + ongoingJoinData.resolve(); + setOngoingJoinData(null); + return; + } + + 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('finished'); + ongoingJoinData.resolve(); + } catch (e) { + setLinkStatus(status => (status === 'valid' ? 'invalid' : status)); + ongoingJoinData.reject(); + } finally { + setOngoingJoinData(null); + } + })(); + }, [ + calendarQuery, + callJoinThread, + dispatchActionPromise, + inviteSecret, + ongoingJoinData, + setLinkStatus, + setOngoingJoinData, + setStep, + step, + threadID, + ]); + + return createJoinPromise; +} + export { tagFarcasterChannelCopy, tagFarcasterChannelErrorMessages, farcasterChannelTagBlobHash, useCreateFarcasterChannelTag, useRemoveFarcasterChannelTag, useCanManageFarcasterChannelTag, + useJoinCommunity, }; diff --git a/lib/types/community-types.js b/lib/types/community-types.js index 449ff596d..0a55aa1d0 100644 --- a/lib/types/community-types.js +++ b/lib/types/community-types.js @@ -1,54 +1,69 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tID, tShape } from '../utils/validation-utils.js'; export type CommunityInfo = { +farcasterChannelID: ?string, }; export type CommunityInfos = { +[threadID: string]: CommunityInfo }; export type CommunityStore = { +communityInfos: CommunityInfos, }; export type AddCommunityPayload = { +id: string, +newCommunityInfo: CommunityInfo, }; export type ServerCommunityInfo = { +id: string, +farcasterChannelID: ?string, }; export const serverCommunityInfoValidator: TInterface = tShape({ id: tID, farcasterChannelID: t.maybe(t.String), }); export type FetchCommunityInfosResponse = { +communityInfos: $ReadOnlyArray, }; export type CreateOrUpdateFarcasterChannelTagRequest = { +commCommunityID: string, +farcasterChannelID: string, }; export type CreateOrUpdateFarcasterChannelTagResponse = { +commCommunityID: string, +farcasterChannelID: string, }; export type DeleteFarcasterChannelTagRequest = { +commCommunityID: string, +farcasterChannelID: string, }; 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';