// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactChild, ReactNode } from 'react'; import React, { useState } from 'react'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; import { MessageRequestActionsConfirmation, MessageRequestState, } from './MessageRequestActionsConfirmation'; import { ContactSpoofingType } from '../../util/contactSpoofing'; import { Modal } from '../Modal'; import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog'; import { ContactSpoofingReviewDialogPerson } from './ContactSpoofingReviewDialogPerson'; import { Button, ButtonVariant } from '../Button'; import { Intl } from '../Intl'; import { assertDev } from '../../util/assert'; import { missingCaseError } from '../../util/missingCaseError'; import { isInSystemContacts } from '../../util/isInSystemContacts'; import { UserText } from '../UserText'; export type PropsType = { conversationId: string; acceptConversation: (conversationId: string) => unknown; blockAndReportSpam: (conversationId: string) => unknown; blockConversation: (conversationId: string) => unknown; deleteConversation: (conversationId: string) => unknown; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; onClose: () => void; showContactModal: (contactId: string, conversationId?: string) => unknown; removeMember: ( conversationId: string, memberConversationId: string ) => unknown; theme: ThemeType; } & ( | { type: ContactSpoofingType.DirectConversationWithSameTitle; possiblyUnsafeConversation: ConversationType; safeConversation: ConversationType; } | { type: ContactSpoofingType.MultipleGroupMembersWithSameTitle; group: ConversationType; collisionInfoByTitle: Record< string, Array<{ oldName?: string; conversation: ConversationType; }> >; } ); enum ConfirmationStateType { ConfirmingDelete, ConfirmingBlock, ConfirmingGroupRemoval, } export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element { const { acceptConversation, blockAndReportSpam, blockConversation, conversationId, deleteConversation, getPreferredBadge, i18n, onClose, showContactModal, removeMember, theme, } = props; const [confirmationState, setConfirmationState] = useState< | undefined | { type: ConfirmationStateType.ConfirmingGroupRemoval; affectedConversation: ConversationType; group: ConversationType; } | { type: | ConfirmationStateType.ConfirmingDelete | ConfirmationStateType.ConfirmingBlock; affectedConversation: ConversationType; } >(); if (confirmationState) { const { type, affectedConversation } = confirmationState; switch (type) { case ConfirmationStateType.ConfirmingDelete: case ConfirmationStateType.ConfirmingBlock: return ( { switch (messageRequestState) { case MessageRequestState.blocking: setConfirmationState({ type: ConfirmationStateType.ConfirmingBlock, affectedConversation, }); break; case MessageRequestState.deleting: setConfirmationState({ type: ConfirmationStateType.ConfirmingDelete, affectedConversation, }); break; case MessageRequestState.unblocking: assertDev( false, 'Got unexpected MessageRequestState.unblocking state. Clearing confiration state' ); setConfirmationState(undefined); break; case MessageRequestState.default: setConfirmationState(undefined); break; default: throw missingCaseError(messageRequestState); } }} /> ); case ConfirmationStateType.ConfirmingGroupRemoval: { const { group } = confirmationState; return ( { setConfirmationState(undefined); }} onRemove={() => { removeMember(conversationId, affectedConversation.id); }} /> ); } default: throw missingCaseError(type); } } let title: string; let contents: ReactChild; switch (props.type) { case ContactSpoofingType.DirectConversationWithSameTitle: { const { possiblyUnsafeConversation, safeConversation } = props; assertDev( possiblyUnsafeConversation.type === 'direct', ' expected a direct conversation for the "possibly unsafe" conversation' ); assertDev( safeConversation.type === 'direct', ' expected a direct conversation for the "safe" conversation' ); title = i18n('icu:ContactSpoofingReviewDialog__title'); contents = ( <>

{i18n('icu:ContactSpoofingReviewDialog__description')}

{i18n('icu:ContactSpoofingReviewDialog__possibly-unsafe-title')}


{i18n('icu:ContactSpoofingReviewDialog__safe-title')}

{ showContactModal(safeConversation.id); }} theme={theme} /> ); break; } case ContactSpoofingType.MultipleGroupMembersWithSameTitle: { const { group, collisionInfoByTitle } = props; const sharedTitles = Object.keys(collisionInfoByTitle); const numSharedTitles = sharedTitles.length; const totalConversations = Object.values(collisionInfoByTitle).reduce( (sum, conversationInfos) => sum + conversationInfos.length, 0 ); title = i18n('icu:ContactSpoofingReviewDialog__group__title'); contents = ( <>

{numSharedTitles > 1 ? i18n( 'icu:ContactSpoofingReviewDialog__group__multiple-conflicts__description', { count: numSharedTitles, } ) : i18n('icu:ContactSpoofingReviewDialog__group__description', { count: totalConversations, })}

{Object.values(collisionInfoByTitle).map( (conversationInfos, titleIdx) => { return ( <>

{i18n( 'icu:ContactSpoofingReviewDialog__group__members-header' )}

{conversationInfos.map( (conversationInfo, conversationIdx) => { let button: ReactNode; if (group.areWeAdmin) { button = ( ); } else if (conversationInfo.conversation.isBlocked) { button = ( ); } else if ( !isInSystemContacts(conversationInfo.conversation) ) { button = ( ); } const { oldName } = conversationInfo; const newName = conversationInfo.conversation.profileName || conversationInfo.conversation.title; let callout: JSX.Element | undefined; if (oldName && oldName !== newName) { callout = (
, newName: , }} />
); } return ( <> {callout} {button && (
{button}
)}
{titleIdx < sharedTitles.length - 1 || conversationIdx < conversationInfos.length - 1 ? (
) : null} ); } )} ); } )} ); break; } default: throw missingCaseError(props); } return ( {contents} ); }