// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FunctionComponent, 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 = { getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; onBlock: (conversationId: string) => unknown; onBlockAndReportSpam: (conversationId: string) => unknown; onClose: () => void; onDelete: (conversationId: string) => unknown; onShowContactModal: (contactId: string, conversationId?: string) => unknown; onUnblock: (conversationId: string) => unknown; removeMember: (conversationId: 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 const ContactSpoofingReviewDialog: FunctionComponent< PropsType > = props => { const { getPreferredBadge, i18n, onBlock, onBlockAndReportSpam, onClose, onDelete, onShowContactModal, onUnblock, 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 ( { onBlock(affectedConversation.id); }} onBlockAndReportSpam={() => { onBlockAndReportSpam(affectedConversation.id); }} onUnblock={() => { onUnblock(affectedConversation.id); }} onDelete={() => { onDelete(affectedConversation.id); }} title={affectedConversation.title} conversationType="direct" state={ type === ConfirmationStateType.ConfirmingDelete ? MessageRequestState.deleting : MessageRequestState.blocking } onChangeState={messageRequestState => { 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(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')}

{ onShowContactModal(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; return ( <> {index !== 0 &&
} {Boolean(oldName) && oldName !== newName && (
, newName: , }} />
)} {button && (
{button}
)}
); })} ); break; } default: throw missingCaseError(props); } return ( {contents} ); };