Conversation details changes for PNP
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
1a74da0c26
commit
eb82ace2de
49 changed files with 1660 additions and 699 deletions
|
@ -36,6 +36,7 @@ export enum AvatarBlur {
|
|||
|
||||
export enum AvatarSize {
|
||||
TWENTY = 20,
|
||||
TWENTY_FOUR = 24,
|
||||
TWENTY_EIGHT = 28,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
|
@ -44,6 +45,7 @@ export enum AvatarSize {
|
|||
FIFTY_TWO = 52,
|
||||
EIGHTY = 80,
|
||||
NINETY_SIX = 96,
|
||||
TWO_HUNDRED_SIXTEEN = 216,
|
||||
}
|
||||
|
||||
type BadgePlacementType = { bottom: number; right: number };
|
||||
|
|
29
ts/components/CollidingAvatars.stories.tsx
Normal file
29
ts/components/CollidingAvatars.stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { PropsType } from './CollidingAvatars';
|
||||
import { CollidingAvatars } from './CollidingAvatars';
|
||||
import { type ComponentMeta } from '../storybook/types';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
export default {
|
||||
title: 'Components/CollidingAvatars',
|
||||
component: CollidingAvatars,
|
||||
argTypes: {},
|
||||
args: {
|
||||
i18n,
|
||||
conversations: [alice, bob],
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function Defaults(args: PropsType): JSX.Element {
|
||||
return <CollidingAvatars {...args} />;
|
||||
}
|
80
ts/components/CollidingAvatars.tsx
Normal file
80
ts/components/CollidingAvatars.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
conversations: ReadonlyArray<ConversationType>;
|
||||
}>;
|
||||
|
||||
const MAX_AVATARS = 2;
|
||||
|
||||
export function CollidingAvatars({
|
||||
i18n,
|
||||
conversations,
|
||||
}: PropsType): JSX.Element {
|
||||
const clipId = useMemo(() => uuid(), []);
|
||||
const onRef = useCallback(
|
||||
(elem: HTMLDivElement | null): void => {
|
||||
if (elem) {
|
||||
// Note that these cannot be set through html attributes
|
||||
elem.style.setProperty('--clip-path', `url(#${clipId})`);
|
||||
}
|
||||
},
|
||||
[clipId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="CollidingAvatars" ref={onRef}>
|
||||
{conversations.slice(0, MAX_AVATARS).map(({ id, type, ...convo }) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={id}
|
||||
className="CollidingAvatars__avatar"
|
||||
i18n={i18n}
|
||||
size={AvatarSize.TWENTY_FOUR}
|
||||
conversationType={type}
|
||||
badge={undefined}
|
||||
{...convo}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/*
|
||||
This clip path is a rectangle with the right-bottom corner cut off
|
||||
by a circle:
|
||||
|
||||
AAAAAAA
|
||||
AAAAAAA
|
||||
AAAAA
|
||||
AAA
|
||||
AAA
|
||||
AA
|
||||
AA
|
||||
|
||||
The idea is that we cut a circle away from the top avatar so that there
|
||||
is a bit of transparent area between two avatars:
|
||||
|
||||
AAAAAAA
|
||||
AAAAAAA
|
||||
AAAAA
|
||||
AAA B
|
||||
AAA BB
|
||||
AA BBB
|
||||
AA BBB
|
||||
|
||||
See CollidingAvatars.scss for how this clipPath is applied.
|
||||
*/}
|
||||
<svg width={0} height={0} className="CollidingAvatars__clip_svg">
|
||||
<clipPath id={clipId} clipPathUnits="objectBoundingBox">
|
||||
<path d="M0 0 h1 v0.4166 A0.54166 0.54166 0 0 0.4166 1 H0 Z" />
|
||||
</clipPath>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -23,6 +23,8 @@ import { ConfirmationDialog } from './ConfirmationDialog';
|
|||
import { FormattingWarningModal } from './FormattingWarningModal';
|
||||
import { SendEditWarningModal } from './SendEditWarningModal';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { AboutContactModal } from './conversation/AboutContactModal';
|
||||
import type { ExternalPropsType as AboutContactModalPropsType } from './conversation/AboutContactModal';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
// NOTE: All types should be required for this component so that the smart
|
||||
|
@ -73,6 +75,9 @@ export type PropsType = {
|
|||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible: boolean;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
// AboutContactModal
|
||||
aboutContactModalProps: AboutContactModalPropsType | undefined;
|
||||
toggleAboutContactModal: () => unknown;
|
||||
// StickerPackPreviewModal
|
||||
stickerPackPreviewId: string | undefined;
|
||||
renderStickerPreviewModal: () => JSX.Element | null;
|
||||
|
@ -139,6 +144,9 @@ export function GlobalModalContainer({
|
|||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible,
|
||||
toggleSignalConnectionsModal,
|
||||
// AboutContactModal
|
||||
aboutContactModalProps,
|
||||
toggleAboutContactModal,
|
||||
// StickerPackPreviewModal
|
||||
stickerPackPreviewId,
|
||||
renderStickerPreviewModal,
|
||||
|
@ -185,10 +193,6 @@ export function GlobalModalContainer({
|
|||
return renderAddUserToAnotherGroup();
|
||||
}
|
||||
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (editHistoryMessages) {
|
||||
return renderEditHistoryMessagesModal();
|
||||
}
|
||||
|
@ -252,6 +256,20 @@ export function GlobalModalContainer({
|
|||
);
|
||||
}
|
||||
|
||||
if (aboutContactModalProps) {
|
||||
return (
|
||||
<AboutContactModal
|
||||
i18n={i18n}
|
||||
onClose={toggleAboutContactModal}
|
||||
{...aboutContactModalProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (isStoriesSettingsVisible) {
|
||||
return renderStoriesSettings();
|
||||
}
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
}>;
|
||||
|
||||
export function SignalConnectionsModal({
|
||||
i18n,
|
||||
|
@ -48,12 +47,6 @@ export function SignalConnectionsModal({
|
|||
<div className="SignalConnectionsModal__description">
|
||||
{i18n('icu:SignalConnectionsModal__footer')}
|
||||
</div>
|
||||
|
||||
<div className="SignalConnectionsModal__button">
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('icu:Confirmation--confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
60
ts/components/conversation/AboutContactModal.stories.tsx
Normal file
60
ts/components/conversation/AboutContactModal.stories.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { PropsType } from './AboutContactModal';
|
||||
import { AboutContactModal } from './AboutContactModal';
|
||||
import { type ComponentMeta } from '../../storybook/types';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const conversation = getDefaultConversation();
|
||||
const conversationWithAbout = getDefaultConversation({
|
||||
aboutText: '😀 About Me',
|
||||
});
|
||||
const systemContact = getDefaultConversation({
|
||||
systemGivenName: 'Alice',
|
||||
phoneNumber: '+1 555 123-4567',
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/AboutContactModal',
|
||||
component: AboutContactModal,
|
||||
argTypes: {
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
conversation,
|
||||
isSignalConnection: false,
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function Defaults(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} />;
|
||||
}
|
||||
|
||||
export function WithAbout(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={conversationWithAbout} />;
|
||||
}
|
||||
|
||||
export function SignalConnection(args: PropsType): JSX.Element {
|
||||
return <AboutContactModal {...args} isSignalConnection />;
|
||||
}
|
||||
|
||||
export function SystemContact(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={systemContact}
|
||||
isSignalConnection
|
||||
/>
|
||||
);
|
||||
}
|
135
ts/components/conversation/AboutContactModal.tsx
Normal file
135
ts/components/conversation/AboutContactModal.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { Modal } from '../Modal';
|
||||
import { UserText } from '../UserText';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { About } from './About';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
}> &
|
||||
ExternalPropsType;
|
||||
|
||||
export type ExternalPropsType = Readonly<{
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
updateSharedGroups: (id: string) => void;
|
||||
}>;
|
||||
|
||||
export function AboutContactModal({
|
||||
i18n,
|
||||
conversation,
|
||||
isSignalConnection,
|
||||
toggleSignalConnectionsModal,
|
||||
updateSharedGroups,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups(conversation.id);
|
||||
}, [conversation.id, updateSharedGroups]);
|
||||
|
||||
const onSignalConnectionClick = useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
toggleSignalConnectionsModal();
|
||||
},
|
||||
[toggleSignalConnectionsModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
key="main"
|
||||
modalName="AboutContactModal"
|
||||
moduleClassName="AboutContactModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="AboutContactModal__row AboutContactModal__row--centered">
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarPath={conversation.avatarPath}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWO_HUNDRED_SIXTEEN}
|
||||
title={conversation.title}
|
||||
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<h3 className="AboutContactModal__title">
|
||||
{i18n('icu:AboutContactModal__title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
|
||||
{conversation.about ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--about" />
|
||||
<About
|
||||
className="AboutContactModal__about"
|
||||
text={conversation.about}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isSignalConnection ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--connections" />
|
||||
<button
|
||||
type="button"
|
||||
className="AboutContactModal__signal-connection"
|
||||
onClick={onSignalConnectionClick}
|
||||
>
|
||||
{i18n('icu:AboutContactModal__signal-connection')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isInSystemContacts(conversation) ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--person" />
|
||||
{i18n('icu:AboutContactModal__system-contact', {
|
||||
name: conversation.firstName || conversation.title,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{conversation.phoneNumber ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--phone" />
|
||||
<UserText text={conversation.phoneNumber} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--group" />
|
||||
<div>
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -45,6 +45,7 @@ export default {
|
|||
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||
showConversation: action('showConversation'),
|
||||
theme: ThemeType.light,
|
||||
toggleAboutContactModal: action('AboutContactModal'),
|
||||
toggleAdmin: action('toggleAdmin'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
updateConversationModelSharedGroups: action(
|
||||
|
|
|
@ -43,6 +43,7 @@ type PropsActionType = {
|
|||
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
|
||||
updateConversationModelSharedGroups: (conversationId: string) => void;
|
||||
|
@ -77,6 +78,7 @@ export function ContactModal({
|
|||
removeMemberFromGroup,
|
||||
showConversation,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
toggleAdmin,
|
||||
toggleSafetyNumberModal,
|
||||
|
@ -208,9 +210,17 @@ export function ContactModal({
|
|||
title={contact.title}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
/>
|
||||
<div className="ContactModal__name">
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__name"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(contact.id);
|
||||
}}
|
||||
>
|
||||
<UserText text={contact.title} />
|
||||
</div>
|
||||
<i className="ContactModal__name__chevron" />
|
||||
</button>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
|
|
|
@ -30,6 +30,7 @@ const getCommonProps = () => ({
|
|||
i18n,
|
||||
onClose: action('onClose'),
|
||||
showContactModal: action('showContactModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnectionsModal'),
|
||||
removeMember: action('removeMember'),
|
||||
theme: ThemeType.light,
|
||||
});
|
||||
|
@ -39,13 +40,19 @@ export function DirectConversationsWithSameTitle(): JSX.Element {
|
|||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.DirectConversationWithSameTitle}
|
||||
possiblyUnsafeConversation={getDefaultConversation()}
|
||||
safeConversation={getDefaultConversation()}
|
||||
possiblyUnsafe={{
|
||||
conversation: getDefaultConversation(),
|
||||
isSignalConnection: false,
|
||||
}}
|
||||
safe={{
|
||||
conversation: getDefaultConversation(),
|
||||
isSignalConnection: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotAdmin(): JSX.Element {
|
||||
export function NotAdminMany(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
|
@ -57,12 +64,15 @@ export function NotAdmin(): JSX.Element {
|
|||
collisionInfoByTitle={{
|
||||
Alice: times(2, () => ({
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
})),
|
||||
Bob: times(3, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Bob' }),
|
||||
})),
|
||||
Charlie: times(5, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Charlie' }),
|
||||
})),
|
||||
}}
|
||||
|
@ -70,7 +80,34 @@ export function NotAdmin(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function Admin(): JSX.Element {
|
||||
export function NotAdminOne(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: false,
|
||||
}}
|
||||
collisionInfoByTitle={{
|
||||
Alice: [
|
||||
{
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
{
|
||||
oldName: 'Alice',
|
||||
isSignalConnection: true,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminMany(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
|
@ -82,15 +119,44 @@ export function Admin(): JSX.Element {
|
|||
collisionInfoByTitle={{
|
||||
Alice: times(2, () => ({
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
})),
|
||||
Bob: times(3, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Bob' }),
|
||||
})),
|
||||
Charlie: times(5, () => ({
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Charlie' }),
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminOne(): JSX.Element {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...getCommonProps()}
|
||||
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: true,
|
||||
}}
|
||||
collisionInfoByTitle={{
|
||||
Alice: [
|
||||
{
|
||||
oldName: 'Alicia',
|
||||
isSignalConnection: false,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
{
|
||||
isSignalConnection: true,
|
||||
conversation: getDefaultConversation({ title: 'Alice' }),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,11 +17,35 @@ 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 ReviewPropsType = Readonly<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafe: {
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
};
|
||||
safe: {
|
||||
conversation: ConversationType;
|
||||
isSignalConnection: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
group: ConversationType;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
isSignalConnection: boolean;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
|
@ -29,6 +53,7 @@ export type PropsType = {
|
|||
blockAndReportSpam: (conversationId: string) => unknown;
|
||||
blockConversation: (conversationId: string) => unknown;
|
||||
deleteConversation: (conversationId: string) => unknown;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
|
@ -38,24 +63,7 @@ export type PropsType = {
|
|||
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;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
);
|
||||
} & ReviewPropsType;
|
||||
|
||||
enum ConfirmationStateType {
|
||||
ConfirmingDelete,
|
||||
|
@ -70,6 +78,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
blockConversation,
|
||||
conversationId,
|
||||
deleteConversation,
|
||||
toggleSignalConnectionsModal,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
onClose,
|
||||
|
@ -169,13 +178,13 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
|
||||
switch (props.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle: {
|
||||
const { possiblyUnsafeConversation, safeConversation } = props;
|
||||
const { possiblyUnsafe, safe } = props;
|
||||
assertDev(
|
||||
possiblyUnsafeConversation.type === 'direct',
|
||||
possiblyUnsafe.conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "possibly unsafe" conversation'
|
||||
);
|
||||
assertDev(
|
||||
safeConversation.type === 'direct',
|
||||
safe.conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "safe" conversation'
|
||||
);
|
||||
|
||||
|
@ -187,10 +196,13 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
{i18n('icu:ContactSpoofingReviewDialog__possibly-unsafe-title')}
|
||||
</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={possiblyUnsafeConversation}
|
||||
conversation={possiblyUnsafe.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
isSignalConnection={possiblyUnsafe.isSignalConnection}
|
||||
oldName={undefined}
|
||||
>
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
<Button
|
||||
|
@ -198,7 +210,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingDelete,
|
||||
affectedConversation: possiblyUnsafeConversation,
|
||||
affectedConversation: possiblyUnsafe.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -209,7 +221,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation: possiblyUnsafeConversation,
|
||||
affectedConversation: possiblyUnsafe.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -220,13 +232,16 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
<hr />
|
||||
<h2>{i18n('icu:ContactSpoofingReviewDialog__safe-title')}</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={safeConversation}
|
||||
conversation={safe.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
showContactModal(safeConversation.id);
|
||||
showContactModal(safe.conversation.id);
|
||||
}}
|
||||
theme={theme}
|
||||
isSignalConnection={safe.isSignalConnection}
|
||||
oldName={undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -257,117 +272,83 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
})}
|
||||
</p>
|
||||
|
||||
{Object.values(collisionInfoByTitle).map(
|
||||
(conversationInfos, titleIdx) => {
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
{i18n(
|
||||
'icu:ContactSpoofingReviewDialog__group__members-header'
|
||||
)}
|
||||
</h2>
|
||||
{conversationInfos.map(
|
||||
(conversationInfo, conversationIdx) => {
|
||||
let button: ReactNode;
|
||||
if (group.areWeAdmin) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
||||
affectedConversation:
|
||||
conversationInfo.conversation,
|
||||
group,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:RemoveGroupMemberConfirmation__remove-button'
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
} else if (conversationInfo.conversation.isBlocked) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
acceptConversation(
|
||||
conversationInfo.conversation.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
);
|
||||
} else if (
|
||||
!isInSystemContacts(conversationInfo.conversation)
|
||||
) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation:
|
||||
conversationInfo.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
);
|
||||
{Object.values(collisionInfoByTitle)
|
||||
.map((conversationInfos, titleIdx) =>
|
||||
conversationInfos.map((conversationInfo, conversationIdx) => {
|
||||
let button: ReactNode;
|
||||
if (group.areWeAdmin) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingGroupRemoval,
|
||||
affectedConversation: conversationInfo.conversation,
|
||||
group,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:RemoveGroupMemberConfirmation__remove-button')}
|
||||
</Button>
|
||||
);
|
||||
} else if (conversationInfo.conversation.isBlocked) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
acceptConversation(conversationInfo.conversation.id);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
);
|
||||
} else if (!isInSystemContacts(conversationInfo.conversation)) {
|
||||
button = (
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setConfirmationState({
|
||||
type: ConfirmationStateType.ConfirmingBlock,
|
||||
affectedConversation: conversationInfo.conversation,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const { oldName, isSignalConnection } = conversationInfo;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
key={conversationInfo.conversation.id}
|
||||
conversation={conversationInfo.conversation}
|
||||
toggleSignalConnectionsModal={
|
||||
toggleSignalConnectionsModal
|
||||
}
|
||||
|
||||
const { oldName } = conversationInfo;
|
||||
const newName =
|
||||
conversationInfo.conversation.profileName ||
|
||||
conversationInfo.conversation.title;
|
||||
|
||||
let callout: JSX.Element | undefined;
|
||||
if (oldName && oldName !== newName) {
|
||||
callout = (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property module-ContactSpoofingReviewDialogPerson__info__property--callout">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofingReviewDialog__group__name-change-info"
|
||||
components={{
|
||||
oldName: <UserText text={oldName} />,
|
||||
newName: <UserText text={newName} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
key={conversationInfo.conversation.id}
|
||||
conversation={conversationInfo.conversation}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
>
|
||||
{callout}
|
||||
{button && (
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
{button}
|
||||
</div>
|
||||
)}
|
||||
</ContactSpoofingReviewDialogPerson>
|
||||
{titleIdx < sharedTitles.length - 1 ||
|
||||
conversationIdx < conversationInfos.length - 1 ? (
|
||||
<hr />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
)}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
oldName={oldName}
|
||||
isSignalConnection={isSignalConnection}
|
||||
>
|
||||
{button && (
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
{button}
|
||||
</div>
|
||||
)}
|
||||
</ContactSpoofingReviewDialogPerson>
|
||||
{titleIdx < sharedTitles.length - 1 ||
|
||||
conversationIdx < conversationInfos.length - 1 ? (
|
||||
<hr />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
})
|
||||
)
|
||||
.flat()}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { ThemeType } from '../../types/Util';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import type { PropsType } from './ContactSpoofingReviewDialogPerson';
|
||||
import { ContactSpoofingReviewDialogPerson } from './ContactSpoofingReviewDialogPerson';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
component: ContactSpoofingReviewDialogPerson,
|
||||
title: 'Components/Conversation/ContactSpoofingReviewDialogPerson',
|
||||
argTypes: {
|
||||
oldName: { control: { type: 'text' } },
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
onClick: action('onClick'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnectionsModal'),
|
||||
getPreferredBadge: () => undefined,
|
||||
conversation: getDefaultConversation(),
|
||||
theme: ThemeType.light,
|
||||
oldName: undefined,
|
||||
isSignalConnection: false,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => {
|
||||
return <ContactSpoofingReviewDialogPerson {...args} />;
|
||||
};
|
||||
|
||||
export const Normal = Template.bind({});
|
||||
|
||||
export const SignalConnection = Template.bind({});
|
||||
SignalConnection.args = {
|
||||
isSignalConnection: true,
|
||||
};
|
||||
|
||||
export const ProfileNameChanged = Template.bind({});
|
||||
ProfileNameChanged.args = {
|
||||
oldName: 'Imposter',
|
||||
};
|
||||
|
||||
export const WithSharedGroups = Template.bind({});
|
||||
WithSharedGroups.args = {
|
||||
conversation: getDefaultConversation({
|
||||
sharedGroupNames: ['A', 'B', 'C'],
|
||||
}),
|
||||
};
|
|
@ -12,15 +12,20 @@ import { assertDev } from '../../util/assert';
|
|||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { UserText } from '../UserText';
|
||||
import { Intl } from '../Intl';
|
||||
|
||||
type PropsType = {
|
||||
export type PropsType = Readonly<{
|
||||
children?: ReactNode;
|
||||
conversation: ConversationType;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
onClick?: () => void;
|
||||
toggleSignalConnectionsModal: () => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
oldName: string | undefined;
|
||||
isSignalConnection: boolean;
|
||||
}>;
|
||||
|
||||
export function ContactSpoofingReviewDialogPerson({
|
||||
children,
|
||||
|
@ -28,13 +33,44 @@ export function ContactSpoofingReviewDialogPerson({
|
|||
getPreferredBadge,
|
||||
i18n,
|
||||
onClick,
|
||||
toggleSignalConnectionsModal,
|
||||
theme,
|
||||
oldName,
|
||||
isSignalConnection,
|
||||
}: PropsType): JSX.Element {
|
||||
assertDev(
|
||||
conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialogPerson> expected a direct conversation'
|
||||
);
|
||||
|
||||
const newName = conversation.profileName || conversation.title;
|
||||
|
||||
let callout: JSX.Element | undefined;
|
||||
if (oldName && oldName !== newName) {
|
||||
callout = (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--person" />
|
||||
<div>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofingReviewDialog__group__name-change-info"
|
||||
components={{
|
||||
oldName: <UserText text={oldName} />,
|
||||
newName: <UserText text={newName} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const name = (
|
||||
<ContactName
|
||||
module="module-ContactSpoofingReviewDialogPerson__info__contact-name"
|
||||
title={conversation.title}
|
||||
/>
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<Avatar
|
||||
|
@ -45,40 +81,59 @@ export function ContactSpoofingReviewDialogPerson({
|
|||
className="module-ContactSpoofingReviewDialogPerson__avatar"
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info">
|
||||
<ContactName
|
||||
module="module-ContactSpoofingReviewDialogPerson__info__contact-name"
|
||||
title={conversation.title}
|
||||
/>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson"
|
||||
onClick={onClick}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
{callout}
|
||||
{conversation.phoneNumber ? (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
{conversation.phoneNumber}
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--phone" />
|
||||
<div>{conversation.phoneNumber}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isSignalConnection ? (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--connections" />
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson__info__property__signal-connection"
|
||||
onClick={toggleSignalConnectionsModal}
|
||||
>
|
||||
{i18n('icu:ContactSpoofingReviewDialog__signal-connection')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
<i className="module-ContactSpoofingReviewDialogPerson__info__property__icon module-ContactSpoofingReviewDialogPerson__info__property__icon--group" />
|
||||
<div>
|
||||
{conversation.sharedGroupNames?.length ? (
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
) : (
|
||||
i18n(
|
||||
'icu:ContactSpoofingReviewDialog__group__members__no-shared-groups'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson"
|
||||
onClick={onClick}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson">{contents}</div>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ export default {
|
|||
unblurAvatar: action('unblurAvatar'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
viewUserStories: action('viewUserStories'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
|
@ -78,7 +79,7 @@ export const DirectNoGroupsJustPhoneNumber = Template.bind({});
|
|||
DirectNoGroupsJustPhoneNumber.args = {
|
||||
phoneNumber: casual.phone,
|
||||
profileName: '',
|
||||
title: '',
|
||||
title: casual.phone,
|
||||
};
|
||||
|
||||
export const DirectNoGroupsNoData = Template.bind({});
|
||||
|
@ -86,7 +87,7 @@ DirectNoGroupsNoData.args = {
|
|||
avatarPath: undefined,
|
||||
phoneNumber: '',
|
||||
profileName: '',
|
||||
title: '',
|
||||
title: casual.phone,
|
||||
};
|
||||
|
||||
export const DirectNoGroupsNoDataNotAccepted = Template.bind({});
|
||||
|
|
|
@ -13,7 +13,6 @@ import type { HasStories } from '../../types/Stories';
|
|||
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
|
||||
|
@ -27,7 +26,6 @@ export type Props = {
|
|||
isMe: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
membersCount?: number;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
sharedGroupNames?: ReadonlyArray<string>;
|
||||
unblurAvatar: (conversationId: string) => void;
|
||||
|
@ -35,6 +33,7 @@ export type Props = {
|
|||
updateSharedGroups: (conversationId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
const renderMembershipRow = ({
|
||||
|
@ -56,22 +55,25 @@ const renderMembershipRow = ({
|
|||
Required<Pick<Props, 'sharedGroupNames'>> & {
|
||||
onClickMessageRequestWarning: () => void;
|
||||
}) => {
|
||||
const className = 'module-conversation-hero__membership';
|
||||
|
||||
if (conversationType !== 'direct') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
return <div className={className}>{i18n('icu:noteToSelfHero')}</div>;
|
||||
return (
|
||||
<div className="module-conversation-hero__note-to-self">
|
||||
{i18n('icu:noteToSelfHero')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sharedGroupNames.length > 0) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="module-conversation-hero__membership">
|
||||
<i className="module-conversation-hero__membership__chevron" />
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
nameClassName={`${className}__name`}
|
||||
nameClassName="module-conversation-hero__membership__name"
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
</div>
|
||||
|
@ -81,21 +83,30 @@ const renderMembershipRow = ({
|
|||
if (phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return <div className={className}>{i18n('icu:no-groups-in-common')}</div>;
|
||||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
{i18n('icu:no-groups-in-common')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-conversation-hero__message-request-warning">
|
||||
<div className="module-conversation-hero__message-request-warning__message">
|
||||
{i18n('icu:no-groups-in-common-warning')}
|
||||
<div className="module-conversation-hero__membership">
|
||||
<div className="module-conversation-hero__membership__warning">
|
||||
<i className="module-conversation-hero__membership__warning__icon" />
|
||||
<span>{i18n('icu:no-groups-in-common-warning')}</span>
|
||||
|
||||
<button
|
||||
className="module-conversation-hero__membership__warning__learn-more"
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
onClickMessageRequestWarning();
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onClickMessageRequestWarning}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -115,7 +126,6 @@ export function ConversationHero({
|
|||
isSignalConversation,
|
||||
membersCount,
|
||||
sharedGroupNames = [],
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
theme,
|
||||
|
@ -124,6 +134,7 @@ export function ConversationHero({
|
|||
unblurredAvatarPath,
|
||||
updateSharedGroups,
|
||||
viewUserStories,
|
||||
toggleAboutContactModal,
|
||||
}: Props): JSX.Element {
|
||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||
useState(false);
|
||||
|
@ -158,9 +169,29 @@ export function ConversationHero({
|
|||
};
|
||||
}
|
||||
|
||||
const phoneNumberOnly = Boolean(
|
||||
!name && !profileName && conversationType === 'direct'
|
||||
);
|
||||
let titleElem: JSX.Element | undefined;
|
||||
|
||||
if (isMe) {
|
||||
titleElem = <>{i18n('icu:noteToSelf')}</>;
|
||||
} else if (isSignalConversation || conversationType !== 'direct') {
|
||||
titleElem = (
|
||||
<ContactName isSignalConversation={isSignalConversation} title={title} />
|
||||
);
|
||||
} else if (title) {
|
||||
titleElem = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-conversation-hero__title"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(id);
|
||||
}}
|
||||
>
|
||||
<ContactName title={title} />
|
||||
<i className="module-conversation-hero__title__chevron" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
return (
|
||||
|
@ -187,14 +218,7 @@ export function ConversationHero({
|
|||
title={title}
|
||||
/>
|
||||
<h1 className="module-conversation-hero__profile-name">
|
||||
{isMe ? (
|
||||
i18n('icu:noteToSelf')
|
||||
) : (
|
||||
<ContactName
|
||||
isSignalConversation={isSignalConversation}
|
||||
title={title}
|
||||
/>
|
||||
)}
|
||||
{titleElem}
|
||||
{isMe && <span className="ContactModal__official-badge__large" />}
|
||||
</h1>
|
||||
{about && !isMe && (
|
||||
|
@ -212,9 +236,7 @@ export function ConversationHero({
|
|||
/>
|
||||
) : membersCount != null ? (
|
||||
i18n('icu:ConversationHero--members', { count: membersCount })
|
||||
) : phoneNumberOnly ? null : (
|
||||
phoneNumber
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!isSignalConversation &&
|
||||
|
|
|
@ -13,10 +13,8 @@ import type { PropsType } from './Timeline';
|
|||
import { Timeline } from './Timeline';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import { TimelineItem } from './TimelineItem';
|
||||
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from '../../state/smart/ContactSpoofingReviewDialog';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { TypingBubble } from './TypingBubble';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
|
@ -26,9 +24,13 @@ import { ThemeType } from '../../types/Util';
|
|||
import { TextDirection } from './Message';
|
||||
import { PaymentEventKind } from '../../types/Payment';
|
||||
import type { PropsData as TimelineMessageProps } from './TimelineMessage';
|
||||
import { CollidingAvatars } from '../CollidingAvatars';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/Timeline',
|
||||
argTypes: {},
|
||||
|
@ -323,10 +325,7 @@ const actions = () => ({
|
|||
returnToActiveCall: action('returnToActiveCall'),
|
||||
|
||||
closeContactSpoofingReview: action('closeContactSpoofingReview'),
|
||||
reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'),
|
||||
reviewMessageRequestNameCollision: action(
|
||||
'reviewMessageRequestNameCollision'
|
||||
),
|
||||
reviewConversationNameCollision: action('reviewConversationNameCollision'),
|
||||
|
||||
unblurAvatar: action('unblurAvatar'),
|
||||
|
||||
|
@ -375,35 +374,9 @@ const renderItem = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const renderContactSpoofingReviewDialog = (
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
) => {
|
||||
const sharedProps = {
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
removeMember: action('removeMember'),
|
||||
showContactModal: action('showContactModal'),
|
||||
theme: ThemeType.dark,
|
||||
};
|
||||
|
||||
if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
|
||||
return (
|
||||
<ContactSpoofingReviewDialog
|
||||
{...props}
|
||||
{...sharedProps}
|
||||
group={{
|
||||
...getDefaultConversation(),
|
||||
areWeAdmin: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
|
||||
const renderContactSpoofingReviewDialog = () => {
|
||||
// hasContactSpoofingReview is always false in stories
|
||||
return <div />;
|
||||
};
|
||||
|
||||
const getAbout = () => '👍 Free to chat';
|
||||
|
@ -433,6 +406,7 @@ const renderHeroRow = () => {
|
|||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={noop}
|
||||
viewUserStories={action('viewUserStories')}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -452,6 +426,9 @@ const renderTypingBubble = () => (
|
|||
theme={ThemeType.light}
|
||||
/>
|
||||
);
|
||||
const renderCollidingAvatars = () => (
|
||||
<CollidingAvatars i18n={i18n} conversations={[alice, bob]} />
|
||||
);
|
||||
const renderMiniPlayer = () => (
|
||||
<div>If active, this is where smart mini player would be</div>
|
||||
);
|
||||
|
@ -477,12 +454,14 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
invitedContactsForNewlyCreatedGroup:
|
||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||
warning: overrideProps.warning,
|
||||
hasContactSpoofingReview: false,
|
||||
|
||||
id: uuid(),
|
||||
renderItem,
|
||||
renderHeroRow,
|
||||
renderMiniPlayer,
|
||||
renderTypingBubble,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
|
||||
|
||||
|
@ -581,7 +560,9 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
|
|||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversation: getDefaultConversation(),
|
||||
|
||||
// Just to pacify type-script
|
||||
safeConversationId: '123',
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
@ -590,6 +571,21 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
|
|||
}
|
||||
|
||||
export function WithSameNameInGroupConversationWarning(): JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => uuid()),
|
||||
},
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithSameNamesInGroupConversationWarning(): JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
|
|
|
@ -58,7 +58,7 @@ const LOAD_NEWER_THRESHOLD = 5;
|
|||
export type WarningType = ReadonlyDeep<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
safeConversation: ConversationType;
|
||||
safeConversationId: string;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
|
@ -67,23 +67,6 @@ export type WarningType = ReadonlyDeep<
|
|||
}
|
||||
>;
|
||||
|
||||
export type ContactSpoofingReviewPropType =
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
collisionInfoByTitle: Record<
|
||||
string,
|
||||
Array<{
|
||||
oldName?: string;
|
||||
conversation: ConversationType;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
|
@ -112,7 +95,7 @@ type PropsHousekeepingType = {
|
|||
shouldShowMiniPlayer: boolean;
|
||||
|
||||
warning?: WarningType;
|
||||
contactSpoofingReview?: ContactSpoofingReviewPropType;
|
||||
hasContactSpoofingReview: boolean | undefined;
|
||||
|
||||
discardMessages: (
|
||||
_: Readonly<
|
||||
|
@ -128,6 +111,9 @@ type PropsHousekeepingType = {
|
|||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
|
||||
renderCollidingAvatars: (_: {
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
}) => JSX.Element;
|
||||
renderContactSpoofingReviewDialog: (
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
) => JSX.Element;
|
||||
|
@ -167,12 +153,7 @@ export type PropsActionsType = {
|
|||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||
peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
|
||||
peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
|
||||
reviewGroupMemberNameCollision: (groupConversationId: string) => void;
|
||||
reviewMessageRequestNameCollision: (
|
||||
_: Readonly<{
|
||||
safeConversationId: string;
|
||||
}>
|
||||
) => void;
|
||||
reviewConversationNameCollision: () => void;
|
||||
scrollToOldestUnreadMention: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
|
@ -798,7 +779,7 @@ export class Timeline extends React.Component<
|
|||
acknowledgeGroupMemberNameCollisions,
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup,
|
||||
closeContactSpoofingReview,
|
||||
contactSpoofingReview,
|
||||
hasContactSpoofingReview,
|
||||
getPreferredBadge,
|
||||
getTimestampForMessage,
|
||||
haveNewest,
|
||||
|
@ -811,13 +792,13 @@ export class Timeline extends React.Component<
|
|||
items,
|
||||
messageLoadingState,
|
||||
oldestUnseenIndex,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
renderHeroRow,
|
||||
renderItem,
|
||||
renderMiniPlayer,
|
||||
renderTypingBubble,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
reviewConversationNameCollision,
|
||||
scrollToOldestUnreadMention,
|
||||
shouldShowMiniPlayer,
|
||||
theme,
|
||||
|
@ -963,8 +944,14 @@ export class Timeline extends React.Component<
|
|||
let headerElements: ReactNode;
|
||||
if (warning || shouldShowMiniPlayer) {
|
||||
let text: ReactChild | undefined;
|
||||
let icon: ReactChild | undefined;
|
||||
let onClose: () => void;
|
||||
if (warning) {
|
||||
icon = (
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
);
|
||||
switch (warning.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
text = (
|
||||
|
@ -976,11 +963,7 @@ export class Timeline extends React.Component<
|
|||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
reviewRequestLink: parts => (
|
||||
<TimelineWarning.Link
|
||||
onClick={() => {
|
||||
reviewMessageRequestNameCollision({
|
||||
safeConversationId: warning.safeConversation.id,
|
||||
});
|
||||
}}
|
||||
onClick={reviewConversationNameCollision}
|
||||
>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
|
@ -998,24 +981,25 @@ export class Timeline extends React.Component<
|
|||
const { groupNameCollisions } = warning;
|
||||
const numberOfSharedNames = Object.keys(groupNameCollisions).length;
|
||||
const reviewRequestLink: FullJSXType = parts => (
|
||||
<TimelineWarning.Link
|
||||
onClick={() => {
|
||||
reviewGroupMemberNameCollision(id);
|
||||
}}
|
||||
>
|
||||
<TimelineWarning.Link onClick={reviewConversationNameCollision}>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
);
|
||||
if (numberOfSharedNames === 1) {
|
||||
const [conversationIds] = [...Object.values(groupNameCollisions)];
|
||||
if (conversationIds.length >= 2) {
|
||||
icon = (
|
||||
<TimelineWarning.CustomInfo>
|
||||
{renderCollidingAvatars({ conversationIds })}
|
||||
</TimelineWarning.CustomInfo>
|
||||
);
|
||||
}
|
||||
text = (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name-in-group--link"
|
||||
components={{
|
||||
count: Object.values(groupNameCollisions).reduce(
|
||||
(result, conversations) => result + conversations.length,
|
||||
0
|
||||
),
|
||||
count: conversationIds.length,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
|
@ -1053,9 +1037,7 @@ export class Timeline extends React.Component<
|
|||
{renderMiniPlayer({ shouldFlow: true })}
|
||||
{text && (
|
||||
<TimelineWarning i18n={i18n} onClose={onClose}>
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
{icon}
|
||||
<TimelineWarning.Text>{text}</TimelineWarning.Text>
|
||||
</TimelineWarning>
|
||||
)}
|
||||
|
@ -1066,33 +1048,11 @@ export class Timeline extends React.Component<
|
|||
}
|
||||
|
||||
let contactSpoofingReviewDialog: ReactNode;
|
||||
if (contactSpoofingReview) {
|
||||
const commonProps = {
|
||||
if (hasContactSpoofingReview) {
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
conversationId: id,
|
||||
onClose: closeContactSpoofingReview,
|
||||
};
|
||||
|
||||
switch (contactSpoofingReview.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
...commonProps,
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
possiblyUnsafeConversation:
|
||||
contactSpoofingReview.possiblyUnsafeConversation,
|
||||
safeConversation: contactSpoofingReview.safeConversation,
|
||||
});
|
||||
break;
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle:
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
...commonProps,
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
groupConversationId: id,
|
||||
collisionInfoByTitle: contactSpoofingReview.collisionInfoByTitle,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(contactSpoofingReview);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -71,3 +71,11 @@ function Link({ children, onClick }: Readonly<LinkProps>): JSX.Element {
|
|||
}
|
||||
|
||||
TimelineWarning.Link = Link;
|
||||
|
||||
function CustomInfo({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): JSX.Element {
|
||||
return <div className="module-TimelineWarning__custom_info">{children}</div>;
|
||||
}
|
||||
|
||||
TimelineWarning.CustomInfo = CustomInfo;
|
||||
|
|
|
@ -102,6 +102,7 @@ const createProps = (
|
|||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
|
|
|
@ -150,6 +150,7 @@ type ActionProps = {
|
|||
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
updateGroupAttributes: (
|
||||
|
@ -223,6 +224,7 @@ export function ConversationDetails({
|
|||
showConversation,
|
||||
showLightboxWithMedia,
|
||||
theme,
|
||||
toggleAboutContactModal,
|
||||
toggleSafetyNumberModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
updateGroupAttributes,
|
||||
|
@ -398,6 +400,7 @@ export function ConversationDetails({
|
|||
);
|
||||
}}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={toggleAboutContactModal}
|
||||
/>
|
||||
|
||||
<div className="ConversationDetails__header-buttons">
|
||||
|
|
|
@ -45,6 +45,7 @@ function Wrapper(overrideProps: Partial<Props>) {
|
|||
isGroup
|
||||
isMe={false}
|
||||
theme={theme}
|
||||
toggleAboutContactModal={action('toggleAboutContactModal')}
|
||||
{...overrideProps}
|
||||
/>
|
||||
);
|
||||
|
@ -80,7 +81,16 @@ export function EditableNoDescription(): JSX.Element {
|
|||
}
|
||||
|
||||
export function OneOnOne(): JSX.Element {
|
||||
return <Wrapper isGroup={false} badges={getFakeBadges(3)} />;
|
||||
return (
|
||||
<Wrapper
|
||||
isGroup={false}
|
||||
badges={getFakeBadges(3)}
|
||||
conversation={getDefaultConversation({
|
||||
title: 'Maya Johnson',
|
||||
type: 'direct',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteToSelf(): JSX.Element {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { GroupDescription } from '../GroupDescription';
|
|||
import { About } from '../About';
|
||||
import type { GroupV2Membership } from './ConversationDetailsMembershipList';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import { bemGenerator } from './util';
|
||||
import { assertDev } from '../../../util/assert';
|
||||
import { BadgeDialog } from '../../BadgeDialog';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
import { UserText } from '../../UserText';
|
||||
|
@ -26,6 +26,7 @@ export type Props = {
|
|||
isMe: boolean;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
|
@ -34,8 +35,6 @@ enum ConversationDetailsHeaderActiveModal {
|
|||
ShowingBadges,
|
||||
}
|
||||
|
||||
const bem = bemGenerator('ConversationDetails-header');
|
||||
|
||||
export function ConversationDetailsHeader({
|
||||
areWeASubscriber,
|
||||
badges,
|
||||
|
@ -46,6 +45,7 @@ export function ConversationDetailsHeader({
|
|||
isMe,
|
||||
memberships,
|
||||
startEditing,
|
||||
toggleAboutContactModal,
|
||||
theme,
|
||||
}: Props): JSX.Element {
|
||||
const [activeModal, setActiveModal] = useState<
|
||||
|
@ -75,10 +75,10 @@ export function ConversationDetailsHeader({
|
|||
} else if (!isMe) {
|
||||
subtitle = (
|
||||
<>
|
||||
<div className={bem('subtitle__about')}>
|
||||
<div className="ConversationDetailsHeader__subtitle__about">
|
||||
<About text={conversation.about} />
|
||||
</div>
|
||||
<div className={bem('subtitle__phone-number')}>
|
||||
<div className="ConversationDetailsHeader__subtitle__phone-number">
|
||||
{conversation.phoneNumber}
|
||||
</div>
|
||||
</>
|
||||
|
@ -105,15 +105,6 @@ export function ConversationDetailsHeader({
|
|||
/>
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<div>
|
||||
<div className={bem('title')}>
|
||||
{isMe ? i18n('icu:noteToSelf') : <UserText text={conversation.title} />}
|
||||
{isMe && <span className="ContactModal__official-badge__large" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let modal: ReactNode;
|
||||
switch (activeModal) {
|
||||
case ConversationDetailsHeaderActiveModal.ShowingAvatar:
|
||||
|
@ -150,8 +141,13 @@ export function ConversationDetailsHeader({
|
|||
}
|
||||
|
||||
if (canEdit) {
|
||||
assertDev(isGroup, 'Only groups support editable title');
|
||||
|
||||
return (
|
||||
<div className={bem('root')} data-testid="ConversationDetailsHeader">
|
||||
<div
|
||||
className="ConversationDetailsHeader"
|
||||
data-testid="ConversationDetailsHeader"
|
||||
>
|
||||
{modal}
|
||||
{avatar}
|
||||
<button
|
||||
|
@ -161,12 +157,14 @@ export function ConversationDetailsHeader({
|
|||
ev.stopPropagation();
|
||||
startEditing(true);
|
||||
}}
|
||||
className={bem('root', 'editable')}
|
||||
className="ConversationDetailsHeader__edit-button"
|
||||
>
|
||||
{contents}
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
</button>
|
||||
{hasNestedButton ? (
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
<div className="ConversationDetailsHeader__subtitle">{subtitle}</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -179,21 +177,61 @@ export function ConversationDetailsHeader({
|
|||
ev.stopPropagation();
|
||||
startEditing(false);
|
||||
}}
|
||||
className={bem('root', 'editable')}
|
||||
className="ConversationDetailsHeader__edit-button"
|
||||
>
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
<div className="ConversationDetailsHeader__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let title: JSX.Element;
|
||||
|
||||
if (isMe) {
|
||||
title = (
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
{i18n('icu:noteToSelf')}
|
||||
<span className="ContactModal__official-badge__large" />
|
||||
</div>
|
||||
);
|
||||
} else if (isGroup) {
|
||||
title = (
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
}}
|
||||
className="ConversationDetailsHeader__about-button"
|
||||
>
|
||||
<div className="ConversationDetailsHeader__title">
|
||||
<UserText text={conversation.title} />
|
||||
|
||||
<span className="ConversationDetailsHeader__about-icon" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={bem('root')} data-testid="ConversationDetailsHeader">
|
||||
<div
|
||||
className="ConversationDetailsHeader"
|
||||
data-testid="ConversationDetailsHeader"
|
||||
>
|
||||
{modal}
|
||||
{avatar}
|
||||
{contents}
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
{title}
|
||||
<div className="ConversationDetailsHeader__subtitle">{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue