diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js index 3483861c5..c7f1b6847 100644 --- a/lib/hooks/invite-links.js +++ b/lib/hooks/invite-links.js @@ -1,102 +1,195 @@ // @flow -import React from 'react'; +import invariant from 'invariant'; +import * as React from 'react'; import { useCreateOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, useDisableInviteLink, disableInviteLinkLinkActionTypes, } from '../actions/link-actions.js'; +import { + joinThreadActionTypes, + useJoinThread, +} from '../actions/thread-actions.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; +import type { CalendarQuery } from '../types/entry-types.js'; import type { SetState } from '../types/hook-types.js'; -import type { InviteLink } from '../types/link-types.js'; +import type { + InviteLink, + InviteLinkVerificationResponse, +} from '../types/link-types.js'; +import type { LoadingStatus } from '../types/loading-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-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, +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, name, setName, createOrUpdateInviteLink, disableInviteLink, }), [createOrUpdateInviteLink, disableInviteLink, error, isLoading, name], ); } -export { useInviteLinksActions }; +const joinThreadLoadingStatusSelector = createLoadingStatusSelector( + joinThreadActionTypes, +); + +type AcceptInviteLinkParams = { + +verificationResponse: InviteLinkVerificationResponse, + +inviteSecret: string, + +calendarQuery: () => CalendarQuery, + +onFinish: () => mixed, + +onInvalidLinkDetected: () => mixed, +}; +function useAcceptInviteLink(params: AcceptInviteLinkParams): { + +joinCommunity: () => mixed, + +joinThreadLoadingStatus: LoadingStatus, +} { + const { + verificationResponse, + inviteSecret, + calendarQuery, + onFinish, + onInvalidLinkDetected, + } = params; + + React.useEffect(() => { + if (verificationResponse.status === 'already_joined') { + onFinish(); + } + }, [onFinish, verificationResponse.status]); + + 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] }, + ], + }, + inviteLinkSecret: inviteSecret, + }); + onFinish(); + return result; + } catch (e) { + onInvalidLinkDetected(); + throw e; + } + }, [ + calendarQuery, + callJoinThread, + communityID, + inviteSecret, + onFinish, + onInvalidLinkDetected, + ]); + 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 }; diff --git a/native/navigation/invite-link-modal.react.js b/native/navigation/invite-link-modal.react.js index 96e5a4d9a..4ff821a3d 100644 --- a/native/navigation/invite-link-modal.react.js +++ b/native/navigation/invite-link-modal.react.js @@ -1,254 +1,214 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; -import { - useJoinThread, - joinThreadActionTypes, -} from 'lib/actions/thread-actions.js'; -import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { useAcceptInviteLink } from 'lib/hooks/invite-links.js'; import type { InviteLinkVerificationResponse } from 'lib/types/link-types.js'; -import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; 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 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, }; type Props = { +navigation: RootNavigationProp<'InviteLinkModal'>, +route: NavigationRoute<'InviteLinkModal'>, }; function InviteLinkModal(props: Props): React.Node { const styles = useStyles(unboundStyles); const { invitationDetails, secret } = props.route.params; - React.useEffect(() => { - if (invitationDetails.status === 'already_joined') { - props.navigation.goBack(); - } - }, [invitationDetails.status, props.navigation]); + const navContext = React.useContext(NavContext); + const calendarQuery = useSelector(state => + nonThreadCalendarQuery({ + redux: state, + navContext, + }), + ); + const onInvalidLinkDetected = React.useCallback( + () => + props.navigation.setParams({ + invitationDetails: { + status: 'invalid', + }, + secret, + }), + [props.navigation, secret], + ); + const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ + verificationResponse: invitationDetails, + inviteSecret: secret, + calendarQuery, + onFinish: props.navigation.goBack, + onInvalidLinkDetected, + }); const header = React.useMemo(() => { if (invitationDetails.status === 'valid') { return ( <> You have been invited to join {invitationDetails.community.name} ); } return ( <> Invite invalid This invite link may be expired. Please try again with another invite link. ); }, [ invitationDetails, styles.communityName, styles.invalidInviteExplanation, styles.invalidInviteTitle, styles.invitation, ]); - const callJoinThread = useJoinThread(); - const navContext = React.useContext(NavContext); - const calendarQuery = useSelector(state => - nonThreadCalendarQuery({ - redux: state, - navContext, - }), - ); - const communityID = invitationDetails.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] }, - ], - }, - inviteLinkSecret: secret, - }); - props.navigation.goBack(); - return result; - } catch (e) { - props.navigation.setParams({ - invitationDetails: { - status: 'invalid', - }, - secret, - }); - throw e; - } - }, [calendarQuery, callJoinThread, communityID, props.navigation, secret]); - const dispatchActionPromise = useDispatchActionPromise(); - const joinCommunity = React.useCallback(() => { - void dispatchActionPromise( - joinThreadActionTypes, - createJoinCommunityAction(), - ); - }, [createJoinCommunityAction, dispatchActionPromise]); - const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector); - const buttons = React.useMemo(() => { if (invitationDetails.status === 'valid') { const joinButtonContent = joinThreadLoadingStatus === 'loading' ? ( ) : ( Accept invite ); return ( <> ); } return ( ); }, [ invitationDetails.status, joinCommunity, joinThreadLoadingStatus, props.navigation.goBack, styles.activityIndicatorStyle, styles.button, styles.buttonPrimary, styles.buttonSecondary, styles.buttonText, styles.gap, ]); return ( {header} {buttons} ); } -const joinThreadLoadingStatusSelector = createLoadingStatusSelector( - joinThreadActionTypes, -); - 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 25f99924f..b438d1d79 100644 --- a/web/invite-links/accept-invite-modal.react.js +++ b/web/invite-links/accept-invite-modal.react.js @@ -1,133 +1,92 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; -import { - useJoinThread, - joinThreadActionTypes, -} from 'lib/actions/thread-actions.js'; import ModalOverlay from 'lib/components/modal-overlay.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { useAcceptInviteLink } from 'lib/hooks/invite-links.js'; import { type InviteLinkVerificationResponse } from 'lib/types/link-types.js'; -import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './accept-invite-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors.js'; type Props = { +verificationResponse: InviteLinkVerificationResponse, +inviteSecret: string, }; function AcceptInviteModal(props: Props): React.Node { const { verificationResponse, inviteSecret } = props; const [isLinkValid, setIsLinkValid] = React.useState( verificationResponse.status === 'valid', ); + const onInvalidLinkDetected = React.useCallback( + () => setIsLinkValid(false), + [], + ); const { popModal } = useModalContext(); - - React.useEffect(() => { - if (verificationResponse.status === 'already_joined') { - popModal(); - } - }, [popModal, verificationResponse.status]); - - const callJoinThread = useJoinThread(); const calendarQuery = useSelector(nonThreadCalendarQuery); - 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] }, - ], - }, - inviteLinkSecret: inviteSecret, - }); - popModal(); - return result; - } catch (e) { - setIsLinkValid(false); - throw e; - } - }, [calendarQuery, callJoinThread, communityID, inviteSecret, popModal]); - const dispatchActionPromise = useDispatchActionPromise(); - const joinCommunity = React.useCallback(() => { - void dispatchActionPromise( - joinThreadActionTypes, - createJoinCommunityAction(), - ); - }, [createJoinCommunityAction, dispatchActionPromise]); - const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector); + + const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({ + verificationResponse, + inviteSecret, + calendarQuery, + onFinish: popModal, + onInvalidLinkDetected, + }); let content; if (verificationResponse.status === 'valid' && isLinkValid) { const { community } = verificationResponse; content = ( <>
You have been invited to join
{community.name}

); } else { content = ( <>
Invite invalid
This invite link may be expired. Please try again with another invite link.

); } return (
{content}
); } -const joinThreadLoadingStatusSelector = createLoadingStatusSelector( - joinThreadActionTypes, -); - export default AcceptInviteModal;