diff --git a/native/navigation/invite-link-modal.react.js b/native/navigation/invite-link-modal.react.js index c40f018d3..6102d5cf3 100644 --- a/native/navigation/invite-link-modal.react.js +++ b/native/navigation/invite-link-modal.react.js @@ -1,250 +1,272 @@ // @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 { join, joinLoadingStatus } = useAcceptInviteLink({ verificationResponse: invitationDetails, inviteSecret: secret, keyserverOverride, calendarQuery, closeModal: props.navigation.goBack, setLinkStatus, navigateToThread, }); const header = React.useMemo(() => { if (invitationDetails.status === 'valid' && linkStatus === 'valid') { + let additionalCommunityDescription = null; + if (invitationDetails.thread) { + additionalCommunityDescription = ( + <> + within + + {invitationDetails.community.name} + + + ); + } + const targetName = + invitationDetails.thread?.name ?? invitationDetails.community.name; return ( <> You have been invited to join - - {invitationDetails.community.name} - + {targetName} + {additionalCommunityDescription} ); } return ( <> {inviteLinkTexts[linkStatus].header} {inviteLinkTexts[linkStatus].message(!!invitationDetails.thread)} ); }, [ invitationDetails, - styles.communityName, + styles.threadName, + styles.communityIntro, styles.invalidInviteExplanation, styles.invalidInviteTitle, styles.invitation, linkStatus, ]); const threadInfos = useSelector(threadInfoSelector); const closeModal = React.useCallback(() => { 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 = joinLoadingStatus === 'loading' ? ( ) : ( Accept invite ); return ( <> ); } return ( ); }, [ closeModal, join, joinLoadingStatus, 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: { + communityIntro: { + color: 'whiteText', + textAlign: 'center', + fontSize: 14, + fontWeight: '400', + lineHeight: 22, + marginBottom: 24, + marginTop: 16, + }, + threadName: { 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.css b/web/invite-links/accept-invite-modal.css index b56e6907f..9d12c046a 100644 --- a/web/invite-links/accept-invite-modal.css +++ b/web/invite-links/accept-invite-modal.css @@ -1,39 +1,40 @@ .container { border-radius: 16px; padding: 32px 24px; display: flex; flex-direction: column; justify-content: center; text-align: center; background: var(--modal-bg); width: 398px; gap: 32px; } .group { display: flex; flex-direction: column; align-items: center; gap: 16px; } .heading { font-size: var(--xl-font-20); line-height: var(--line-height-display); font-weight: var(--semi-bold); color: var(--fg); + word-break: break-word; } .text { font-size: var(--s-font-14); color: var(--label-default-color); } .container hr { width: 100%; background: var(--border); } .container button { width: 100%; } diff --git a/web/invite-links/accept-invite-modal.react.js b/web/invite-links/accept-invite-modal.react.js index 8a5824ff4..42435104f 100644 --- a/web/invite-links/accept-invite-modal.react.js +++ b/web/invite-links/accept-invite-modal.react.js @@ -1,118 +1,132 @@ // @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 { join, joinLoadingStatus } = useAcceptInviteLink({ verificationResponse, inviteSecret, keyserverOverride, calendarQuery, closeModal: popModal, setLinkStatus, navigateToThread, }); let content; if (verificationResponse.status === 'valid' && linkStatus === 'valid') { const { community } = verificationResponse; + let additionalCommunityDescription = null; + if (verificationResponse.thread) { + additionalCommunityDescription = ( +
+
within
+
{community.name}
+
+ ); + } + const targetName = + verificationResponse.thread?.name ?? verificationResponse.community.name; content = ( <> -
You have been invited to join
-
{community.name}
+
+
You have been invited to join
+
{targetName}
+
+ {additionalCommunityDescription}
); } else { content = ( <>
{inviteLinkTexts[linkStatus].header}
{inviteLinkTexts[linkStatus].message(!!verificationResponse.thread)}

); } return (
{content}
); } export default AcceptInviteModal;