diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js index e9d89e774..8f372136d 100644 --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -1,194 +1,195 @@ // @flow import * as React from 'react'; import type { CallSingleKeyserverEndpoint } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { FetchInviteLinksResponse, InviteLinkVerificationRequest, InviteLinkVerificationResponse, CreateOrUpdatePublicLinkRequest, InviteLink, DisableInviteLinkRequest, DisableInviteLinkPayload, } from '../types/link-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { useSelector } from '../utils/redux-utils.js'; const verifyInviteLinkActionTypes = Object.freeze({ started: 'VERIFY_INVITE_LINK_STARTED', success: 'VERIFY_INVITE_LINK_SUCCESS', failed: 'VERIFY_INVITE_LINK_FAILED', }); const verifyInviteLink = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((input: { +request: InviteLinkVerificationRequest, +keyserverID: string, }) => Promise) => async input => { const { request, keyserverID } = input; const requests = { [keyserverID]: request, }; const responses = await callSingleKeyserverEndpoint( 'verify_invite_link', requests, ); const response = responses[keyserverID]; if (response.status === 'valid' || response.status === 'already_joined') { return { status: response.status, community: response.community, + thread: response.thread, }; } return { status: response.status, }; }; function useVerifyInviteLink( keyserverOverride?: ?{ +keyserverID: string, +keyserverURL: string, }, ): ( request: InviteLinkVerificationRequest, ) => Promise { const keyserverID = keyserverOverride?.keyserverID ?? authoritativeKeyserverID(); const isKeyserverKnown = useSelector( state => !!state.keyserverStore.keyserverInfos[keyserverID], ); let paramOverride = null; if (keyserverOverride && !isKeyserverKnown) { paramOverride = { keyserverInfos: { [keyserverOverride.keyserverID]: { urlPrefix: keyserverOverride.keyserverURL, }, }, }; } const callVerifyInviteLink = useKeyserverCall( verifyInviteLink, paramOverride, ); return React.useCallback( (request: InviteLinkVerificationRequest) => callVerifyInviteLink({ request, keyserverID }), [callVerifyInviteLink, keyserverID], ); } const fetchPrimaryInviteLinkActionTypes = Object.freeze({ started: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', success: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', failed: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', }); const fetchPrimaryInviteLinks = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { const requests: { [string]: void } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = undefined; } const responses = await callKeyserverEndpoint( 'fetch_primary_invite_links', requests, ); let links: $ReadOnlyArray = []; for (const keyserverID in responses) { links = links.concat(responses[keyserverID].links); } return { links, }; }; function useFetchPrimaryInviteLinks(): () => Promise { return useKeyserverCall(fetchPrimaryInviteLinks); } const createOrUpdatePublicLinkActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', success: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', failed: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', }); const createOrUpdatePublicLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateOrUpdatePublicLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: { name: input.name, communityID: input.communityID, }, }; const responses = await callKeyserverEndpoint( 'create_or_update_public_link', requests, ); const response = responses[keyserverID]; return { name: response.name, primary: response.primary, role: response.role, communityID: response.communityID, expirationTime: response.expirationTime, limitOfUses: response.limitOfUses, numberOfUses: response.numberOfUses, }; }; function useCreateOrUpdatePublicLink(): ( input: CreateOrUpdatePublicLinkRequest, ) => Promise { return useKeyserverCall(createOrUpdatePublicLink); } const disableInviteLinkLinkActionTypes = Object.freeze({ started: 'DISABLE_INVITE_LINK_STARTED', success: 'DISABLE_INVITE_LINK_SUCCESS', failed: 'DISABLE_INVITE_LINK_FAILED', }); const disableInviteLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DisableInviteLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: input }; await callKeyserverEndpoint('disable_invite_link', requests); return input; }; function useDisableInviteLink(): ( input: DisableInviteLinkRequest, ) => Promise { return useKeyserverCall(disableInviteLink); } export { verifyInviteLinkActionTypes, useVerifyInviteLink, fetchPrimaryInviteLinkActionTypes, useFetchPrimaryInviteLinks, createOrUpdatePublicLinkActionTypes, useCreateOrUpdatePublicLink, disableInviteLinkLinkActionTypes, useDisableInviteLink, }; diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js index 1dfec33f3..64d2f2c48 100644 --- a/lib/hooks/invite-links.js +++ b/lib/hooks/invite-links.js @@ -1,358 +1,364 @@ // @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 type { KeyserverOverride } from '../shared/invite-links.js'; import { useIsKeyserverURLValid } from '../shared/keyserver-utils.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.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 type { ThreadJoinPayload } from '../types/thread-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useDispatch, 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, ], ); } const joinThreadLoadingStatusSelector = createLoadingStatusSelector( joinThreadActionTypes, ); export type LinkStatus = 'invalid' | 'valid' | 'timed_out' | 'already_joined'; const inviteLinkTexts: { - +[LinkStatus]: { +header: string, +message: string }, + +[LinkStatus]: { + +header: string, + +message: (isThreadLink: boolean) => string, + }, } = { invalid: { header: 'Invite invalid', - message: + message: () => 'This invite link may be expired. Please try again with another invite' + ' link.', }, ['timed_out']: { header: 'Timeout', - message: 'The request has timed out.', + message: () => 'The request has timed out.', }, ['already_joined']: { header: 'Already a member', - message: 'You are already a member of this community.', + message: isThreadLink => + `You are already a member of this ${ + isThreadLink ? 'thread' : 'community' + }.`, }, }; type AcceptInviteLinkParams = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, +keyserverOverride: ?KeyserverOverride, +calendarQuery: () => CalendarQuery, +closeModal: () => mixed, +setLinkStatus: SetState, +navigateToThread: ThreadInfo => mixed, }; function useAcceptInviteLink(params: AcceptInviteLinkParams): { +joinCommunity: () => mixed, +joinThreadLoadingStatus: LoadingStatus, } { const { verificationResponse, inviteSecret, keyserverOverride, calendarQuery, closeModal, 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, }>(null); const timeoutRef = React.useRef(); React.useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); const createJoinCommunityAction = 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 = (result: ThreadJoinPayload) => { clearTimeout(timeoutID); resolve(result); }; const rejectAndClearTimeout = () => { clearTimeout(timeoutID); reject(); }; setOngoingJoinData({ resolve: resolveAndClearTimeout, reject: rejectAndClearTimeout, communityID, }); }); }, [communityID, keyserverID, setLinkStatus]); React.useEffect(() => { void (async () => { if (!ongoingJoinData || isKeyserverKnown) { 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, ongoingJoinData, keyserverID, keyserverURL, setLinkStatus, ]); const [joined, setJoined] = React.useState(false); React.useEffect(() => { void (async () => { if (!ongoingJoinData || !isAuthenticated) { return; } const threadID = ongoingJoinData.communityID; const query = calendarQuery(); try { const result = await callJoinThread({ threadID, calendarQuery: { startDate: query.startDate, endDate: query.endDate, filters: [ ...query.filters, { type: 'threads', threadIDs: [threadID] }, ], }, inviteLinkSecret: inviteSecret, }); setJoined(true); ongoingJoinData.resolve(result); } catch (e) { setLinkStatus(status => (status === 'valid' ? 'invalid' : status)); ongoingJoinData.reject(); } finally { setOngoingJoinData(null); } })(); }, [ calendarQuery, callJoinThread, communityID, inviteSecret, isAuthenticated, ongoingJoinData, closeModal, setLinkStatus, ]); const threadInfos = useSelector(threadInfoSelector); React.useEffect(() => { if (!joined) { return; } const threadID = verificationResponse.community?.id; if (!threadID || !threadInfos[threadID]) { closeModal(); return; } navigateToThread(threadInfos[threadID]); }, [ joined, navigateToThread, closeModal, threadInfos, verificationResponse.community?.id, ]); const dispatchActionPromise = useDispatchActionPromise(); const joinCommunity = React.useCallback(() => { void dispatchActionPromise( joinThreadActionTypes, createJoinCommunityAction(), ); }, [createJoinCommunityAction, dispatchActionPromise]); const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector); return React.useMemo( () => ({ joinCommunity, joinThreadLoadingStatus, }), [joinCommunity, joinThreadLoadingStatus], ); } export { useInviteLinksActions, useAcceptInviteLink, inviteLinkTexts }; diff --git a/native/navigation/invite-link-modal.react.js b/native/navigation/invite-link-modal.react.js index deb2b3207..7b06c31d4 100644 --- a/native/navigation/invite-link-modal.react.js +++ b/native/navigation/invite-link-modal.react.js @@ -1,252 +1,250 @@ // @flow import * as React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; import { inviteLinkTexts, useAcceptInviteLink, } from 'lib/hooks/invite-links.js'; import type { LinkStatus } from 'lib/hooks/invite-links.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import type { KeyserverOverride } from 'lib/shared/invite-links'; import type { InviteLinkVerificationResponse } from 'lib/types/link-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types'; import { nonThreadCalendarQuery } from './nav-selectors.js'; import { NavContext } from './navigation-context.js'; import type { RootNavigationProp } from './root-navigator.react.js'; import type { NavigationRoute } from './route-names.js'; import { useNavigateToThread } from '../chat/message-list-types.js'; import Button from '../components/button.react.js'; import Modal from '../components/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; export type InviteLinkModalParams = { +invitationDetails: InviteLinkVerificationResponse, +secret: string, +keyserverOverride?: ?KeyserverOverride, }; type Props = { +navigation: RootNavigationProp<'InviteLinkModal'>, +route: NavigationRoute<'InviteLinkModal'>, }; function InviteLinkModal(props: Props): React.Node { const styles = useStyles(unboundStyles); const { invitationDetails, secret, keyserverOverride } = props.route.params; const [linkStatus, setLinkStatus] = React.useState( invitationDetails.status === 'expired' ? 'invalid' : invitationDetails.status, ); const navContext = React.useContext(NavContext); const calendarQuery = useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); const navigateToThreadWithParams = useNavigateToThread(); const navigateToThread = React.useCallback( (threadInfo: ThreadInfo) => { navigateToThreadWithParams({ threadInfo }); }, [navigateToThreadWithParams], ); const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ verificationResponse: invitationDetails, inviteSecret: secret, keyserverOverride, calendarQuery, closeModal: props.navigation.goBack, setLinkStatus, navigateToThread, }); const header = React.useMemo(() => { if (invitationDetails.status === 'valid' && linkStatus === 'valid') { return ( <> You have been invited to join {invitationDetails.community.name} ); } return ( <> {inviteLinkTexts[linkStatus].header} - {inviteLinkTexts[linkStatus].message} + {inviteLinkTexts[linkStatus].message(!!invitationDetails.thread)} ); }, [ invitationDetails, styles.communityName, styles.invalidInviteExplanation, styles.invalidInviteTitle, styles.invitation, linkStatus, ]); const threadInfos = useSelector(threadInfoSelector); const closeModal = React.useCallback(() => { - const communityID = invitationDetails.community?.id; - if ( - linkStatus === 'already_joined' && - communityID && - threadInfos[communityID] - ) { - navigateToThread(threadInfos[communityID]); + const threadID = + invitationDetails.thread?.id ?? invitationDetails.community?.id; + if (linkStatus === 'already_joined' && threadID && threadInfos[threadID]) { + navigateToThread(threadInfos[threadID]); } else { props.navigation.goBack(); } }, [ invitationDetails.community?.id, + invitationDetails.thread?.id, linkStatus, navigateToThread, props.navigation, threadInfos, ]); const buttons = React.useMemo(() => { if (linkStatus === 'valid') { const joinButtonContent = joinThreadLoadingStatus === 'loading' ? ( ) : ( Accept invite ); return ( <> ); } return ( ); }, [ closeModal, joinCommunity, joinThreadLoadingStatus, linkStatus, props.navigation.goBack, styles.activityIndicatorStyle, styles.button, styles.buttonPrimary, styles.buttonSecondary, styles.buttonText, styles.gap, ]); return ( {header} {buttons} ); } const unboundStyles = { modal: { backgroundColor: 'modalForeground', paddingVertical: 24, paddingHorizontal: 16, flex: 0, }, invitation: { color: 'whiteText', textAlign: 'center', fontSize: 14, fontWeight: '400', lineHeight: 22, marginBottom: 24, }, communityName: { color: 'whiteText', textAlign: 'center', fontSize: 18, fontWeight: '500', lineHeight: 24, }, invalidInviteTitle: { color: 'whiteText', textAlign: 'center', fontSize: 22, fontWeight: '500', lineHeight: 28, marginBottom: 24, }, invalidInviteExplanation: { color: 'whiteText', textAlign: 'center', fontSize: 14, fontWeight: '400', lineHeight: 22, }, separator: { height: 1, backgroundColor: 'modalSeparator', marginVertical: 24, }, gap: { marginBottom: 16, }, button: { borderRadius: 8, paddingVertical: 12, paddingHorizontal: 24, }, buttonPrimary: { backgroundColor: 'purpleButton', }, buttonSecondary: { borderColor: 'secondaryButtonBorder', borderWidth: 1, }, buttonText: { color: 'whiteText', textAlign: 'center', fontSize: 16, fontWeight: '500', lineHeight: 24, }, activityIndicatorStyle: { paddingVertical: 2, }, }; export default InviteLinkModal; diff --git a/web/invite-links/accept-invite-modal.react.js b/web/invite-links/accept-invite-modal.react.js index 6fc5bf5eb..9c9f9483c 100644 --- a/web/invite-links/accept-invite-modal.react.js +++ b/web/invite-links/accept-invite-modal.react.js @@ -1,116 +1,118 @@ // @flow import * as React from 'react'; import ModalOverlay from 'lib/components/modal-overlay.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { inviteLinkTexts, useAcceptInviteLink, } from 'lib/hooks/invite-links.js'; import type { LinkStatus } from 'lib/hooks/invite-links.js'; import type { KeyserverOverride } from 'lib/shared/invite-links.js'; import { type InviteLinkVerificationResponse } from 'lib/types/link-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './accept-invite-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors.js'; type Props = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, +keyserverOverride?: ?KeyserverOverride, }; function AcceptInviteModal(props: Props): React.Node { const { verificationResponse, inviteSecret, keyserverOverride } = props; const [linkStatus, setLinkStatus] = React.useState( verificationResponse.status === 'expired' ? 'invalid' : verificationResponse.status, ); const { popModal } = useModalContext(); const calendarQuery = useSelector(nonThreadCalendarQuery); const dispatch = useDispatch(); const navigateToThread = React.useCallback( (threadInfo: ThreadInfo) => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadInfo.id, tab: 'chat', }, }); popModal(); }, [dispatch, popModal], ); const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ verificationResponse, inviteSecret, keyserverOverride, calendarQuery, closeModal: popModal, setLinkStatus, navigateToThread, }); let content; if (verificationResponse.status === 'valid' && linkStatus === 'valid') { const { community } = verificationResponse; content = ( <>
You have been invited to join
{community.name}

); } else { content = ( <>
{inviteLinkTexts[linkStatus].header}
-
{inviteLinkTexts[linkStatus].message}
+
+ {inviteLinkTexts[linkStatus].message(!!verificationResponse.thread)} +

); } return (
{content}
); } export default AcceptInviteModal; diff --git a/web/invite-links/invite-link-handler.react.js b/web/invite-links/invite-link-handler.react.js index bf3f56b8a..de7bb0ceb 100644 --- a/web/invite-links/invite-link-handler.react.js +++ b/web/invite-links/invite-link-handler.react.js @@ -1,125 +1,125 @@ // @flow import * as React from 'react'; import { useVerifyInviteLink, verifyInviteLinkActionTypes, } from 'lib/actions/link-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { getKeyserverOverrideForAnInviteLink, type KeyserverOverride, } from 'lib/shared/invite-links.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import AcceptInviteModal from './accept-invite-modal.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; function InviteLinkHandler(): null { const inviteSecret = useSelector(state => state.navInfo.inviteSecret); const inviteLinkSecret = React.useRef(null); const [keyserverOverride, setKeyserverOverride] = React.useState(undefined); const loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); const { pushModal } = useModalContext(); React.useEffect(() => { void (async () => { if (!inviteSecret || !loggedIn) { return; } dispatch({ type: updateNavInfoActionType, payload: { inviteSecret: null }, }); setKeyserverOverride(undefined); inviteLinkSecret.current = inviteSecret; try { const newKeyserverOverride = await getKeyserverOverrideForAnInviteLink(inviteSecret); setKeyserverOverride(newKeyserverOverride); } catch (e) { console.error('Error while downloading an invite link blob', e); pushModal( , ); } })(); }, [dispatch, inviteSecret, loggedIn, pushModal]); const validateLink = useVerifyInviteLink(keyserverOverride); const threadInfos = useSelector(threadInfoSelector); React.useEffect(() => { const secret = inviteLinkSecret.current; if (keyserverOverride === undefined || !secret) { return; } setKeyserverOverride(undefined); void (async () => { let result; try { const validateLinkPromise = validateLink({ secret }); void dispatchActionPromise( verifyInviteLinkActionTypes, validateLinkPromise, ); result = await validateLinkPromise; } catch (e) { console.error('Error while verifying an invite link', e); result = { status: 'invalid', }; } - const communityID = result.community?.id; + const threadID = result.thread?.id ?? result.community?.id; if ( - communityID && + threadID && result.status === 'already_joined' && - threadInfos[communityID] + threadInfos[threadID] ) { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', - activeChatThreadID: communityID, + activeChatThreadID: threadID, tab: 'chat', }, }); } pushModal( , ); })(); }, [ dispatch, dispatchActionPromise, keyserverOverride, pushModal, threadInfos, validateLink, ]); return null; } export default InviteLinkHandler;