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 @@ -1,8 +1,8 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; +import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { useCreateOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, @@ -13,16 +13,23 @@ joinThreadActionTypes, useJoinThread, } from '../actions/thread-actions.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 { callSingleKeyserverEndpointTimeout } 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 { ThreadJoinPayload } from '../types/thread-types.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 createOrUpdatePublicLinkStatusSelector = createLoadingStatusSelector( createOrUpdatePublicLinkActionTypes, @@ -116,6 +123,7 @@ type AcceptInviteLinkParams = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, + +keyserverOverride: ?KeyserverOverride, +calendarQuery: () => CalendarQuery, +onFinish: () => mixed, +onInvalidLinkDetected: () => mixed, @@ -127,6 +135,7 @@ const { verificationResponse, inviteSecret, + keyserverOverride, calendarQuery, onFinish, onInvalidLinkDetected, @@ -138,41 +147,147 @@ } }, [onFinish, verificationResponse.status]); + const dispatch = useDispatch(); const callJoinThread = useJoinThread(); + const communityID = verificationResponse.community?.id; - const createJoinCommunityAction = React.useCallback(async () => { - invariant( - communityID, - 'CommunityID should be present while calling this function', - ); - const query = calendarQuery(); - try { - const result = await callJoinThread({ - threadID: communityID, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [communityID] }, - ], + 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(); + onInvalidLinkDetected(); + setOngoingJoinData(null); + return; + } + const timeoutID = setTimeout(() => { + reject(); + setOngoingJoinData(oldData => { + if (oldData) { + onInvalidLinkDetected(); + } + return null; + }); + }, callSingleKeyserverEndpointTimeout); + timeoutRef.current = timeoutID; + const resolveAndClearTimeout = (result: ThreadJoinPayload) => { + clearTimeout(timeoutID); + resolve(result); + }; + const rejectAndClearTimeout = () => { + clearTimeout(timeoutID); + reject(); + }; + setOngoingJoinData({ + resolve: resolveAndClearTimeout, + reject: rejectAndClearTimeout, + communityID, + }); + }); + }, [communityID, keyserverID, onInvalidLinkDetected]); + + React.useEffect(() => { + void (async () => { + if (!ongoingJoinData || isKeyserverKnown) { + return; + } + const isValid = await isKeyserverURLValid(); + if (!isValid || !keyserverURL) { + onInvalidLinkDetected(); + ongoingJoinData.reject(); + setOngoingJoinData(null); + return; + } + dispatch({ + type: addKeyserverActionType, + payload: { + keyserverAdminUserID: keyserverID, + newKeyserverInfo: defaultKeyserverInfo(keyserverURL), }, - inviteLinkSecret: inviteSecret, }); - onFinish(); - return result; - } catch (e) { - onInvalidLinkDetected(); - throw e; - } + })(); + }, [ + dispatch, + isKeyserverKnown, + isKeyserverURLValid, + ongoingJoinData, + keyserverID, + keyserverURL, + onInvalidLinkDetected, + ]); + + 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, + }); + onFinish(); + ongoingJoinData.resolve(result); + } catch (e) { + onInvalidLinkDetected(); + ongoingJoinData.reject(); + } finally { + setOngoingJoinData(null); + } + })(); }, [ calendarQuery, callJoinThread, communityID, inviteSecret, + isAuthenticated, + ongoingJoinData, onFinish, onInvalidLinkDetected, ]); + const dispatchActionPromise = useDispatchActionPromise(); const joinCommunity = React.useCallback(() => { void dispatchActionPromise( diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -143,9 +143,12 @@ ); const isLoggedInToKeyserver: ( - keyserverID: string, + keyserverID: ?string, ) => (state: BaseAppState<>) => boolean = _memoize( - (keyserverID: string) => (state: BaseAppState<>) => { + (keyserverID: ?string) => (state: BaseAppState<>) => { + if (!keyserverID) { + return false; + } const cookie = state.keyserverStore.keyserverInfos[keyserverID]?.cookie; return !!cookie && cookie.startsWith('user='); }, diff --git a/native/navigation/deep-links-context-provider.react.js b/native/navigation/deep-links-context-provider.react.js --- a/native/navigation/deep-links-context-provider.react.js +++ b/native/navigation/deep-links-context-provider.react.js @@ -161,6 +161,7 @@ params: { invitationDetails: result, secret, + keyserverOverride, }, }); })(); diff --git a/native/navigation/invite-link-modal.react.js b/native/navigation/invite-link-modal.react.js --- a/native/navigation/invite-link-modal.react.js +++ b/native/navigation/invite-link-modal.react.js @@ -4,6 +4,7 @@ import { View, Text, ActivityIndicator } from 'react-native'; import { useAcceptInviteLink } from 'lib/hooks/invite-links.js'; +import type { KeyserverOverride } from 'lib/shared/invite-links'; import type { InviteLinkVerificationResponse } from 'lib/types/link-types.js'; import { nonThreadCalendarQuery } from './nav-selectors.js'; @@ -18,6 +19,7 @@ export type InviteLinkModalParams = { +invitationDetails: InviteLinkVerificationResponse, +secret: string, + +keyserverOverride?: ?KeyserverOverride, }; type Props = { @@ -27,7 +29,7 @@ function InviteLinkModal(props: Props): React.Node { const styles = useStyles(unboundStyles); - const { invitationDetails, secret } = props.route.params; + const { invitationDetails, secret, keyserverOverride } = props.route.params; const navContext = React.useContext(NavContext); const calendarQuery = useSelector(state => @@ -49,6 +51,7 @@ const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ verificationResponse: invitationDetails, inviteSecret: secret, + keyserverOverride, calendarQuery, onFinish: props.navigation.goBack, onInvalidLinkDetected, diff --git a/web/invite-links/accept-invite-modal.react.js b/web/invite-links/accept-invite-modal.react.js --- a/web/invite-links/accept-invite-modal.react.js +++ b/web/invite-links/accept-invite-modal.react.js @@ -5,6 +5,7 @@ import ModalOverlay from 'lib/components/modal-overlay.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { useAcceptInviteLink } 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 css from './accept-invite-modal.css'; @@ -15,10 +16,11 @@ type Props = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, + +keyserverOverride?: ?KeyserverOverride, }; function AcceptInviteModal(props: Props): React.Node { - const { verificationResponse, inviteSecret } = props; + const { verificationResponse, inviteSecret, keyserverOverride } = props; const [isLinkValid, setIsLinkValid] = React.useState( verificationResponse.status === 'valid', ); @@ -32,6 +34,7 @@ const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ verificationResponse, inviteSecret, + keyserverOverride, calendarQuery, onFinish: popModal, onInvalidLinkDetected, diff --git a/web/invite-links/invite-link-handler.react.js b/web/invite-links/invite-link-handler.react.js --- a/web/invite-links/invite-link-handler.react.js +++ b/web/invite-links/invite-link-handler.react.js @@ -89,6 +89,7 @@ , ); })();