// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useEffect, useState } from 'react'; import type { ReactNode } from 'react'; import type { ConversationType, ShowConversationType, } from '../../state/ducks/conversations'; import type { BadgeType } from '../../badges/types'; import type { HasStories } from '../../types/Stories'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import { StoryViewModeType } from '../../types/Stories'; import * as log from '../../logging/log'; import { Avatar, AvatarSize } from '../Avatar'; import { AvatarLightbox } from '../AvatarLightbox'; import { BadgeDialog } from '../BadgeDialog'; import { ConfirmationDialog } from '../ConfirmationDialog'; import { Modal } from '../Modal'; import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog'; import { missingCaseError } from '../../util/missingCaseError'; import { UserText } from '../UserText'; import { Button, ButtonIconType, ButtonVariant } from '../Button'; import { isInSystemContacts } from '../../util/isInSystemContacts'; import { InContactsIcon } from '../InContactsIcon'; import { canHaveNicknameAndNote } from '../../util/nicknames'; import { getThemeByThemeType } from '../../util/theme'; import { InAnotherCallTooltip, getTooltipContent, } from './InAnotherCallTooltip'; export type PropsDataType = { areWeASubscriber: boolean; areWeAdmin: boolean; badges: ReadonlyArray; contact?: ConversationType; conversation?: ConversationType; hasStories?: HasStories; readonly i18n: LocalizerType; isAdmin: boolean; isMember: boolean; theme: ThemeType; hasActiveCall: boolean; isInFullScreenCall: boolean; }; type PropsActionType = { blockConversation: (id: string) => void; hideContactModal: () => void; onOpenEditNicknameAndNoteModal: () => void; onOutgoingAudioCallInConversation: (conversationId: string) => unknown; onOutgoingVideoCallInConversation: (conversationId: string) => unknown; removeMemberFromGroup: (conversationId: string, contactId: string) => void; showConversation: ShowConversationType; toggleAdmin: (conversationId: string, contactId: string) => void; toggleAboutContactModal: (conversationId: string) => unknown; togglePip: () => void; toggleSafetyNumberModal: (conversationId: string) => unknown; toggleAddUserToAnotherGroupModal: (conversationId: string) => void; updateConversationModelSharedGroups: (conversationId: string) => void; viewUserStories: ViewUserStoriesActionCreatorType; }; export type PropsType = PropsDataType & PropsActionType; enum ContactModalView { Default, ShowingAvatar, ShowingBadges, } enum SubModalState { None = 'None', ToggleAdmin = 'ToggleAdmin', MemberRemove = 'MemberRemove', ConfirmingBlock = 'ConfirmingBlock', } export function ContactModal({ areWeAdmin, areWeASubscriber, badges, blockConversation, contact, conversation, hasActiveCall, hasStories, hideContactModal, isInFullScreenCall, i18n, isAdmin, isMember, onOpenEditNicknameAndNoteModal, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, removeMemberFromGroup, showConversation, theme, toggleAboutContactModal, toggleAddUserToAnotherGroupModal, toggleAdmin, togglePip, toggleSafetyNumberModal, updateConversationModelSharedGroups, viewUserStories, }: PropsType): JSX.Element { if (!contact) { throw new Error('Contact modal opened without a matching contact'); } const [view, setView] = useState(ContactModalView.Default); const [subModalState, setSubModalState] = useState( SubModalState.None ); const modalTheme = getThemeByThemeType(theme); useEffect(() => { if (contact?.id) { // Kick off the expensive hydration of the current sharedGroupNames updateConversationModelSharedGroups(contact.id); } }, [contact?.id, updateConversationModelSharedGroups]); const renderQuickActions = React.useCallback( (conversationId: string) => { const inAnotherCallTooltipContent = hasActiveCall ? getTooltipContent(i18n) : undefined; const discouraged = hasActiveCall; const videoCallButton = ( ); const audioCallButton = ( ); return (
{hasActiveCall ? ( {videoCallButton} ) : ( videoCallButton )} {hasActiveCall ? ( {audioCallButton} ) : ( audioCallButton )}
); }, [ hasActiveCall, hideContactModal, i18n, isInFullScreenCall, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, showConversation, togglePip, ] ); let modalNode: ReactNode; switch (subModalState) { case SubModalState.None: modalNode = undefined; break; case SubModalState.ToggleAdmin: if (!conversation?.id) { log.warn('ContactModal: ToggleAdmin state - missing conversationId'); modalNode = undefined; break; } modalNode = ( toggleAdmin(conversation.id, contact.id), text: isAdmin ? i18n('icu:ContactModal--rm-admin') : i18n('icu:ContactModal--make-admin'), }, ]} i18n={i18n} onClose={() => setSubModalState(SubModalState.None)} > {isAdmin ? i18n('icu:ContactModal--rm-admin-info', { contact: contact.title, }) : i18n('icu:ContactModal--make-admin-info', { contact: contact.title, })} ); break; case SubModalState.MemberRemove: if (!contact || !conversation?.id) { log.warn( 'ContactModal: MemberRemove state - missing contact or conversationId' ); modalNode = undefined; break; } modalNode = ( { setSubModalState(SubModalState.None); }} onRemove={() => { removeMemberFromGroup(conversation?.id, contact.id); }} /> ); break; case SubModalState.ConfirmingBlock: modalNode = ( blockConversation(contact.id), style: 'affirmative', }, ]} i18n={i18n} onClose={() => setSubModalState(SubModalState.None)} title={i18n('icu:MessageRequests--block-direct-confirm-title', { title: contact.title, })} > {i18n('icu:MessageRequests--block-direct-confirm-body')} ); break; default: { const state: never = subModalState; log.warn(`ContactModal: unexpected ${state}!`); modalNode = undefined; break; } } switch (view) { case ContactModalView.Default: { const preferredBadge: undefined | BadgeType = badges[0]; return (
{ if (conversation && hasStories) { viewUserStories({ conversationId: contact.id, storyViewMode: StoryViewModeType.User, }); hideContactModal(); } else { setView(ContactModalView.ShowingAvatar); } }} onClickBadge={() => setView(ContactModalView.ShowingBadges)} profileName={contact.profileName} sharedGroupNames={contact.sharedGroupNames} size={AvatarSize.EIGHTY} storyRing={hasStories} theme={theme} title={contact.title} unblurredAvatarUrl={contact.unblurredAvatarUrl} /> {!contact.isMe && renderQuickActions(contact.id)}
{canHaveNicknameAndNote(contact) && ( )} {!contact.isMe && (contact.isBlocked ? (
{i18n('icu:AboutContactModal__blocked', { name: contact.title, })}
) : ( ))} {!contact.isMe && ( )} {!contact.isMe && isMember && conversation?.id && ( )} {!contact.isMe && areWeAdmin && isMember && conversation?.id && ( <> )}
{modalNode}
); } case ContactModalView.ShowingAvatar: return ( setView(ContactModalView.Default)} /> ); case ContactModalView.ShowingBadges: return ( setView(ContactModalView.Default)} title={contact.title} /> ); default: throw missingCaseError(view); } }