signal-desktop/ts/components/conversation/ContactModal.tsx

493 lines
16 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2021-09-21 22:37:10 +00:00
import React, { useEffect, useState } from 'react';
import type { ReactNode } from 'react';
2022-06-16 19:12:50 +00:00
import type {
ConversationType,
ShowConversationType,
} from '../../state/ducks/conversations';
2022-07-22 00:44:35 +00:00
import type { BadgeType } from '../../badges/types';
import type { HasStories } from '../../types/Stories';
2021-11-11 16:23:00 +00:00
import type { LocalizerType, ThemeType } from '../../types/Util';
2022-08-22 17:44:23 +00:00
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import { StoryViewModeType } from '../../types/Stories';
2022-07-22 00:44:35 +00:00
import * as log from '../../logging/log';
2022-12-09 20:37:45 +00:00
import { Avatar, AvatarSize } from '../Avatar';
2022-07-22 00:44:35 +00:00
import { AvatarLightbox } from '../AvatarLightbox';
2021-11-02 23:01:13 +00:00
import { BadgeDialog } from '../BadgeDialog';
2021-09-21 22:37:10 +00:00
import { ConfirmationDialog } from '../ConfirmationDialog';
2022-07-22 00:44:35 +00:00
import { Modal } from '../Modal';
import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog';
2022-07-22 00:44:35 +00:00
import { missingCaseError } from '../../util/missingCaseError';
2023-04-20 17:03:43 +00:00
import { UserText } from '../UserText';
2024-02-14 20:25:27 +00:00
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';
2021-09-21 22:37:10 +00:00
export type PropsDataType = {
areWeASubscriber: boolean;
areWeAdmin: boolean;
2021-11-02 23:01:13 +00:00
badges: ReadonlyArray<BadgeType>;
contact?: ConversationType;
conversation?: ConversationType;
2022-07-22 00:44:35 +00:00
hasStories?: HasStories;
readonly i18n: LocalizerType;
isAdmin: boolean;
isMember: boolean;
2021-11-11 16:23:00 +00:00
theme: ThemeType;
2024-02-14 20:25:27 +00:00
hasActiveCall: boolean;
isInFullScreenCall: boolean;
};
2021-09-21 22:37:10 +00:00
type PropsActionType = {
2024-02-14 20:25:27 +00:00
blockConversation: (id: string) => void;
2021-09-21 22:37:10 +00:00
hideContactModal: () => void;
onOpenEditNicknameAndNoteModal: () => void;
2024-02-14 20:25:27 +00:00
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
2021-09-21 22:37:10 +00:00
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
2022-06-16 19:12:50 +00:00
showConversation: ShowConversationType;
2021-09-21 22:37:10 +00:00
toggleAdmin: (conversationId: string, contactId: string) => void;
toggleAboutContactModal: (conversationId: string) => unknown;
togglePip: () => void;
toggleSafetyNumberModal: (conversationId: string) => unknown;
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
2021-09-21 22:37:10 +00:00
updateConversationModelSharedGroups: (conversationId: string) => void;
2022-08-22 17:44:23 +00:00
viewUserStories: ViewUserStoriesActionCreatorType;
2021-09-21 22:37:10 +00:00
};
export type PropsType = PropsDataType & PropsActionType;
2021-11-02 23:01:13 +00:00
enum ContactModalView {
Default,
ShowingAvatar,
ShowingBadges,
}
enum SubModalState {
None = 'None',
ToggleAdmin = 'ToggleAdmin',
MemberRemove = 'MemberRemove',
2024-02-14 20:25:27 +00:00
ConfirmingBlock = 'ConfirmingBlock',
}
2022-11-18 00:45:19 +00:00
export function ContactModal({
areWeAdmin,
areWeASubscriber,
2021-11-02 23:01:13 +00:00
badges,
2024-02-14 20:25:27 +00:00
blockConversation,
contact,
conversation,
2024-02-14 20:25:27 +00:00
hasActiveCall,
2022-07-22 00:44:35 +00:00
hasStories,
2021-09-21 22:37:10 +00:00
hideContactModal,
isInFullScreenCall,
i18n,
isAdmin,
isMember,
onOpenEditNicknameAndNoteModal,
2024-02-14 20:25:27 +00:00
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
2021-09-21 22:37:10 +00:00
removeMemberFromGroup,
2022-06-16 19:12:50 +00:00
showConversation,
2021-11-11 16:23:00 +00:00
theme,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleAdmin,
togglePip,
toggleSafetyNumberModal,
2021-09-21 22:37:10 +00:00
updateConversationModelSharedGroups,
2022-07-22 00:44:35 +00:00
viewUserStories,
2022-11-18 00:45:19 +00:00
}: PropsType): JSX.Element {
if (!contact) {
throw new Error('Contact modal opened without a matching contact');
}
2021-11-02 23:01:13 +00:00
const [view, setView] = useState(ContactModalView.Default);
const [subModalState, setSubModalState] = useState<SubModalState>(
SubModalState.None
);
const modalTheme = getThemeByThemeType(theme);
2021-08-06 00:17:05 +00:00
useEffect(() => {
if (contact?.id) {
2021-09-21 22:37:10 +00:00
// 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 = (
<Button
icon={ButtonIconType.video}
variant={ButtonVariant.Details}
discouraged={discouraged}
aria-label={inAnotherCallTooltipContent}
onClick={() => {
hideContactModal();
onOutgoingVideoCallInConversation(conversationId);
}}
>
{i18n('icu:video')}
</Button>
);
const audioCallButton = (
<Button
icon={ButtonIconType.audio}
variant={ButtonVariant.Details}
discouraged={discouraged}
aria-label={inAnotherCallTooltipContent}
onClick={() => {
hideContactModal();
onOutgoingAudioCallInConversation(conversationId);
}}
>
{i18n('icu:audio')}
</Button>
);
return (
<div className="ContactModal__quick-actions">
<Button
icon={ButtonIconType.message}
variant={ButtonVariant.Details}
onClick={() => {
hideContactModal();
showConversation({
conversationId,
switchToAssociatedView: true,
});
if (isInFullScreenCall) {
togglePip();
}
}}
>
{i18n('icu:ConversationDetails__HeaderButton--Message')}
</Button>
{hasActiveCall ? (
<InAnotherCallTooltip i18n={i18n}>
{videoCallButton}
</InAnotherCallTooltip>
) : (
videoCallButton
)}
{hasActiveCall ? (
<InAnotherCallTooltip i18n={i18n}>
{audioCallButton}
</InAnotherCallTooltip>
) : (
audioCallButton
)}
</div>
);
},
[
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 = (
<ConfirmationDialog
2022-09-27 20:24:21 +00:00
dialogName="ContactModal.toggleAdmin"
actions={[
{
action: () => toggleAdmin(conversation.id, contact.id),
text: isAdmin
2023-03-30 00:03:25 +00:00
? i18n('icu:ContactModal--rm-admin')
: i18n('icu:ContactModal--make-admin'),
},
]}
i18n={i18n}
onClose={() => setSubModalState(SubModalState.None)}
>
{isAdmin
2023-03-30 00:03:25 +00:00
? i18n('icu:ContactModal--rm-admin-info', {
2023-03-27 23:37:39 +00:00
contact: contact.title,
})
2023-03-30 00:03:25 +00:00
: i18n('icu:ContactModal--make-admin-info', {
2023-03-27 23:37:39 +00:00
contact: contact.title,
})}
</ConfirmationDialog>
);
break;
case SubModalState.MemberRemove:
if (!contact || !conversation?.id) {
log.warn(
'ContactModal: MemberRemove state - missing contact or conversationId'
);
modalNode = undefined;
break;
}
modalNode = (
<RemoveGroupMemberConfirmationDialog
conversation={contact}
group={conversation}
i18n={i18n}
onClose={() => {
setSubModalState(SubModalState.None);
}}
onRemove={() => {
removeMemberFromGroup(conversation?.id, contact.id);
}}
/>
);
break;
2024-02-14 20:25:27 +00:00
case SubModalState.ConfirmingBlock:
modalNode = (
<ConfirmationDialog
dialogName="ContactModal.confirmBlock"
actions={[
{
text: i18n('icu:MessageRequests--block'),
action: () => 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')}
</ConfirmationDialog>
);
break;
default: {
const state: never = subModalState;
log.warn(`ContactModal: unexpected ${state}!`);
modalNode = undefined;
break;
}
}
2021-11-02 23:01:13 +00:00
switch (view) {
case ContactModalView.Default: {
const preferredBadge: undefined | BadgeType = badges[0];
return (
<Modal
2022-09-27 20:24:21 +00:00
modalName="ContactModal"
2021-11-02 23:01:13 +00:00
moduleClassName="ContactModal__modal"
hasXButton
2021-08-06 00:17:05 +00:00
i18n={i18n}
2021-11-02 23:01:13 +00:00
onClose={hideContactModal}
padded={false}
theme={modalTheme}
2021-11-02 23:01:13 +00:00
>
<div className="ContactModal">
<Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
2024-07-11 19:44:09 +00:00
avatarUrl={contact.avatarUrl}
2021-11-02 23:01:13 +00:00
badge={preferredBadge}
color={contact.color}
conversationType="direct"
2021-09-21 22:37:10 +00:00
i18n={i18n}
2021-11-02 23:01:13 +00:00
isMe={contact.isMe}
2022-07-22 00:44:35 +00:00
onClick={() => {
if (conversation && hasStories) {
2022-08-22 17:44:23 +00:00
viewUserStories({
conversationId: contact.id,
2022-08-22 17:44:23 +00:00
storyViewMode: StoryViewModeType.User,
});
hideContactModal();
2022-07-22 00:44:35 +00:00
} else {
setView(ContactModalView.ShowingAvatar);
}
}}
onClickBadge={() => setView(ContactModalView.ShowingBadges)}
2021-11-02 23:01:13 +00:00
profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames}
2022-12-09 20:37:45 +00:00
size={AvatarSize.EIGHTY}
2022-07-22 00:44:35 +00:00
storyRing={hasStories}
2021-11-11 16:23:00 +00:00
theme={theme}
2021-11-02 23:01:13 +00:00
title={contact.title}
2024-07-11 19:44:09 +00:00
unblurredAvatarUrl={contact.unblurredAvatarUrl}
2021-11-02 23:01:13 +00:00
/>
<button
type="button"
className="ContactModal__name"
onClick={ev => {
ev.preventDefault();
toggleAboutContactModal(contact.id);
}}
>
2024-02-27 00:58:07 +00:00
<div className="ContactModal__name__text">
<UserText text={contact.title} />
{isInSystemContacts(contact) && (
<span>
{' '}
<InContactsIcon
className="ContactModal__name__contact-icon"
i18n={i18n}
/>
</span>
)}
2024-02-27 00:58:07 +00:00
</div>
<i className="ContactModal__name__chevron" />
</button>
{!contact.isMe && renderQuickActions(contact.id)}
2024-02-14 20:25:27 +00:00
<div className="ContactModal__divider" />
2021-11-02 23:01:13 +00:00
<div className="ContactModal__button-container">
{canHaveNicknameAndNote(contact) && (
<button
type="button"
className="ContactModal__button ContactModal__block"
onClick={onOpenEditNicknameAndNoteModal}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__nickname__bubble-icon" />
</div>
<span>{i18n('icu:ContactModal--nickname')}</span>
</button>
)}
{!contact.isMe &&
(contact.isBlocked ? (
<div className="ContactModal__button ContactModal__block">
<div className="ContactModal__bubble-icon">
<div className="ContactModal__block__bubble-icon" />
</div>
<span>
{i18n('icu:AboutContactModal__blocked', {
name: contact.title,
})}
</span>
2024-02-14 20:25:27 +00:00
</div>
) : (
<button
type="button"
className="ContactModal__button ContactModal__block"
onClick={() =>
setSubModalState(SubModalState.ConfirmingBlock)
}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__block__bubble-icon" />
</div>
<span>{i18n('icu:MessageRequests--block')}</span>
</button>
))}
2021-11-02 23:01:13 +00:00
{!contact.isMe && (
<button
type="button"
className="ContactModal__button ContactModal__safety-number"
onClick={() => {
hideContactModal();
toggleSafetyNumberModal(contact.id);
}}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__safety-number__bubble-icon" />
</div>
2023-03-30 00:03:25 +00:00
<span>{i18n('icu:showSafetyNumber')}</span>
2021-11-02 23:01:13 +00:00
</button>
)}
{!contact.isMe && isMember && conversation?.id && (
<button
type="button"
className="ContactModal__button"
onClick={() => {
hideContactModal();
toggleAddUserToAnotherGroupModal(contact.id);
}}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__add-to-another-group__bubble-icon" />
</div>
{i18n('icu:ContactModal--add-to-group')}
</button>
)}
{!contact.isMe && areWeAdmin && isMember && conversation?.id && (
2021-11-02 23:01:13 +00:00
<>
<button
type="button"
className="ContactModal__button ContactModal__make-admin"
onClick={() => setSubModalState(SubModalState.ToggleAdmin)}
2021-11-02 23:01:13 +00:00
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__make-admin__bubble-icon" />
</div>
{isAdmin ? (
2023-03-30 00:03:25 +00:00
<span>{i18n('icu:ContactModal--rm-admin')}</span>
2021-11-02 23:01:13 +00:00
) : (
2023-03-30 00:03:25 +00:00
<span>{i18n('icu:ContactModal--make-admin')}</span>
2021-11-02 23:01:13 +00:00
)}
</button>
<button
type="button"
className="ContactModal__button ContactModal__remove-from-group"
onClick={() => setSubModalState(SubModalState.MemberRemove)}
2021-11-02 23:01:13 +00:00
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__remove-from-group__bubble-icon" />
</div>
2023-03-30 00:03:25 +00:00
<span>{i18n('icu:ContactModal--remove-from-group')}</span>
2021-11-02 23:01:13 +00:00
</button>
</>
)}
</div>
{modalNode}
2021-11-02 23:01:13 +00:00
</div>
</Modal>
);
}
case ContactModalView.ShowingAvatar:
return (
<AvatarLightbox
avatarColor={contact.color}
2024-07-11 19:44:09 +00:00
avatarUrl={contact.avatarUrl}
2021-11-02 23:01:13 +00:00
conversationTitle={contact.title}
i18n={i18n}
onClose={() => setView(ContactModalView.Default)}
/>
);
case ContactModalView.ShowingBadges:
return (
<BadgeDialog
areWeASubscriber={areWeASubscriber}
2021-11-02 23:01:13 +00:00
badges={badges}
firstName={contact.firstName}
i18n={i18n}
onClose={() => setView(ContactModalView.Default)}
title={contact.title}
/>
);
default:
throw missingCaseError(view);
}
2022-11-18 00:45:19 +00:00
}