// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactChild, ReactNode } from 'react'; import React, { useState } from 'react'; import { concat, orderBy } from 'lodash'; 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 { Emojify } from './Emojify'; import { assertDev } from '../../util/assert'; import { missingCaseError } from '../../util/missingCaseError'; import { isInSystemContacts } from '../../util/isInSystemContacts'; 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('ContactSpoofingReviewDialog__title'); contents = ( <>

{i18n('ContactSpoofingReviewDialog__description')}

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


{i18n('ContactSpoofingReviewDialog__safe-title')}

{ showContactModal(safeConversation.id); }} theme={theme} /> ); break; } case ContactSpoofingType.MultipleGroupMembersWithSameTitle: { const { group, collisionInfoByTitle } = props; const unsortedConversationInfos = concat( // This empty array exists to appease Lodash's type definitions. [], ...Object.values(collisionInfoByTitle) ); const conversationInfos = orderBy(unsortedConversationInfos, [ // We normally use an `Intl.Collator` to sort by title. We do this instead, as // we only really care about stability (not perfect ordering). 'title', 'id', ]); title = i18n('ContactSpoofingReviewDialog__group__title'); contents = ( <>

{i18n('ContactSpoofingReviewDialog__group__description', [ conversationInfos.length.toString(), ])}

{i18n('ContactSpoofingReviewDialog__group__members-header')}

{conversationInfos.map((conversationInfo, index) => { 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 ( <> {index !== 0 &&
} {callout} {button && (
{button}
)}
); })} ); break; } default: throw missingCaseError(props); } return ( {contents} ); }