// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; import { noop } from 'lodash'; import classNames from 'classnames'; import { Avatar, AvatarSize } from './Avatar'; import type { ActionSpec } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog'; import { InContactsIcon } from './InContactsIcon'; import { Modal } from './Modal'; import type { ConversationType } from '../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { LocalizerType } from '../types/Util'; import { ThemeType } from '../types/Util'; import { isInSystemContacts } from '../util/isInSystemContacts'; import { missingCaseError } from '../util/missingCaseError'; import { ContextMenu } from './ContextMenu'; import { Theme } from '../util/theme'; import { isNotNil } from '../util/isNotNil'; import { MY_STORY_ID } from '../types/Stories'; import type { UUIDStringType } from '../types/UUID'; import { UserText } from './UserText'; export enum SafetyNumberChangeSource { Calling = 'Calling', MessageSend = 'MessageSend', Story = 'Story', } enum DialogState { StartingInReview = 'StartingInReview', ExplicitReviewNeeded = 'ExplicitReviewNeeded', ExplicitReviewStep = 'ExplicitReviewStep', ExplicitReviewComplete = 'ExplicitReviewComplete', } export type SafetyNumberProps = { contactID: string; onClose: () => void; }; type StoryContacts = { story?: { name: string; // For My Story or custom distribution lists, conversationId will be our own conversationId: string; // For Group stories, distributionId will not be provided distributionId?: string; }; contacts: Array; }; export type ContactsByStory = Array; export type Props = Readonly<{ confirmText?: string; contacts: ContactsByStory; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; onCancel: () => void; onConfirm: () => void; removeFromStory?: ( distributionId: string, uuids: Array ) => unknown; renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element; theme: ThemeType; }>; function doesRequireExplicitReviewMode(count: number) { return count > 5; } function getStartingDialogState(count: number): DialogState { if (count === 0) { return DialogState.ExplicitReviewComplete; } if (doesRequireExplicitReviewMode(count)) { return DialogState.ExplicitReviewNeeded; } return DialogState.StartingInReview; } export function SafetyNumberChangeDialog({ confirmText, contacts, getPreferredBadge, i18n, onCancel, onConfirm, removeFromStory, renderSafetyNumber, theme, }: Props): JSX.Element { const totalCount = contacts.reduce( (count, item) => count + item.contacts.length, 0 ); const allVerified = contacts.every(item => item.contacts.every(contact => contact.isVerified) ); const [dialogState, setDialogState] = React.useState( getStartingDialogState(totalCount) ); const [selectedContact, setSelectedContact] = React.useState< ConversationType | undefined >(undefined); const cancelButtonRef = React.createRef(); React.useEffect(() => { if (cancelButtonRef && cancelButtonRef.current) { cancelButtonRef.current.focus(); } }, [cancelButtonRef, contacts]); React.useEffect(() => { if ( dialogState === DialogState.ExplicitReviewStep && (totalCount === 0 || allVerified) ) { setDialogState(DialogState.ExplicitReviewComplete); } }, [allVerified, dialogState, setDialogState, totalCount]); const onClose = selectedContact ? () => { setSelectedContact(undefined); } : onCancel; if (selectedContact) { return ( {renderSafetyNumber({ contactID: selectedContact.id, onClose })} ); } if ( dialogState === DialogState.StartingInReview || dialogState === DialogState.ExplicitReviewStep ) { let text: string; if (dialogState === DialogState.ExplicitReviewStep) { text = i18n('icu:safetyNumberChangeDialog_done'); } else if (allVerified || totalCount === 0) { text = confirmText || i18n('icu:safetyNumberChangeDialog_send'); } else { text = confirmText || i18n('icu:sendAnyway'); } return ( { if (dialogState === DialogState.ExplicitReviewStep) { setDialogState(DialogState.ExplicitReviewComplete); } else { onConfirm(); } }, text, style: 'affirmative', }, ]} hasXButton i18n={i18n} moduleClassName="module-SafetyNumberChangeDialog__confirm-dialog" noMouseClose onCancel={onClose} onClose={noop} >
{i18n('icu:safetyNumberChanges')}
{i18n('icu:safetyNumberChangeDialog__message')}
{contacts.map((section: StoryContacts) => ( ))} ); } let text: string; if (dialogState === DialogState.ExplicitReviewNeeded) { text = confirmText || i18n('icu:sendAnyway'); } else if (dialogState === DialogState.ExplicitReviewComplete) { text = confirmText || i18n('icu:safetyNumberChangeDialog_send'); } else { throw missingCaseError(dialogState); } const actions: Array = [ { action: onConfirm, text, style: 'affirmative', }, ]; if (dialogState === DialogState.ExplicitReviewNeeded) { actions.unshift({ action: () => setDialogState(DialogState.ExplicitReviewStep), text: i18n('icu:safetyNumberChangeDialog__review'), }); } return (
{i18n('icu:safetyNumberChanges')}
{dialogState === DialogState.ExplicitReviewNeeded ? i18n('icu:safetyNumberChangeDialog__many-contacts', { count: totalCount, }) : i18n('icu:safetyNumberChangeDialog__post-review')}
); } function ContactSection({ section, getPreferredBadge, i18n, removeFromStory, setSelectedContact, theme, }: Readonly<{ section: StoryContacts; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; removeFromStory?: ( distributionId: string, uuids: Array ) => unknown; setSelectedContact: (contact: ConversationType) => void; theme: ThemeType; }>) { if (section.contacts.length === 0) { return null; } if (!section.story) { return (
    {section.contacts.map((contact: ConversationType) => { const shouldShowNumber = Boolean(contact.name || contact.profileName); return ( ); })}
); } const { distributionId } = section.story; const uuids = section.contacts.map(contact => contact.uuid).filter(isNotNil); const sectionName = distributionId === MY_STORY_ID ? i18n('icu:Stories__mine') : section.story.name; return (
{sectionName}
{distributionId && removeFromStory && uuids.length > 1 && ( { removeFromStory(distributionId, uuids); }} /> )}
    {section.contacts.map((contact: ConversationType) => { const shouldShowNumber = Boolean(contact.name || contact.profileName); return ( ); })}
); } function SectionButtonWithMenu({ ariaLabel, i18n, removeFromStory, storyName, memberCount, theme, }: Readonly<{ ariaLabel: string; i18n: LocalizerType; removeFromStory: () => unknown; storyName: string; memberCount: number; theme: ThemeType; }>) { const [isConfirming, setIsConfirming] = React.useState(false); return ( <> setIsConfirming(true), }, ]} moduleClassName="module-SafetyNumberChangeDialog__row__chevron" theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light} /> {isConfirming && ( { removeFromStory(); setIsConfirming(false); }, text: i18n('icu:safetyNumberChangeDialog__remove-all'), style: 'affirmative', }, ]} i18n={i18n} noMouseClose onCancel={() => setIsConfirming(false)} onClose={noop} > {i18n('icu:safetyNumberChangeDialog__confirm-remove-all', { story: storyName, count: memberCount, })} )} ); } function ContactRow({ contact, distributionId, getPreferredBadge, i18n, removeFromStory, setSelectedContact, shouldShowNumber, theme, }: Readonly<{ contact: ConversationType; distributionId?: string; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; removeFromStory?: ( distributionId: string, uuids: Array ) => unknown; setSelectedContact: (contact: ConversationType) => void; shouldShowNumber: boolean; theme: ThemeType; }>) { const { uuid } = contact; return (
  • {isInSystemContacts(contact) && ( {' '} )}
    {shouldShowNumber || contact.isVerified ? (
    {shouldShowNumber && ( {contact.phoneNumber} )} {shouldShowNumber && contact.isVerified && (  ·  )} {contact.isVerified && ( {i18n('icu:verified')} )}
    ) : null}
    {distributionId && removeFromStory && uuid ? ( removeFromStory(distributionId, [uuid])} verifyContact={() => setSelectedContact(contact)} /> ) : ( )}
  • ); } function RowButtonWithMenu({ ariaLabel, i18n, removeFromStory, verifyContact, theme, }: Readonly<{ ariaLabel: string; i18n: LocalizerType; removeFromStory: () => unknown; verifyContact: () => unknown; theme: ThemeType; }>) { return ( ); }