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

399 lines
11 KiB
TypeScript
Raw Normal View History

2023-01-03 11:55:46 -08:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 15:34:04 -05:00
// SPDX-License-Identifier: AGPL-3.0-only
import React, { type ReactNode, useEffect, useState } from 'react';
import classNames from 'classnames';
import type { Props as AvatarProps } from '../Avatar';
2022-12-09 13:37:45 -07:00
import { Avatar, AvatarSize, AvatarBlur } from '../Avatar';
2020-05-27 17:37:06 -04:00
import { ContactName } from './ContactName';
2021-01-25 20:01:19 -05:00
import { About } from './About';
2021-06-01 17:24:28 -07:00
import { GroupDescription } from './GroupDescription';
2021-04-16 10:51:26 -05:00
import { SharedGroupNames } from '../SharedGroupNames';
import { GroupMembersNames } from '../GroupMembersNames';
2021-11-02 18:01:13 -05:00
import type { LocalizerType, ThemeType } from '../../types/Util';
2022-07-21 20:44:35 -04:00
import type { HasStories } from '../../types/Stories';
2022-08-22 13:44:23 -04:00
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import type { GroupV2Membership } from './conversation-details/ConversationDetailsMembershipList';
2022-08-22 13:44:23 -04:00
import { StoryViewModeType } from '../../types/Stories';
2024-03-12 09:29:31 -07:00
import { Button, ButtonVariant } from '../Button';
import { SafetyTipsModal } from '../SafetyTipsModal';
import { I18n } from '../I18n';
2020-05-27 17:37:06 -04:00
export type Props = {
2021-01-25 20:01:19 -05:00
about?: string;
acceptedMessageRequest?: boolean;
fromOrAddedByTrustedContact?: boolean;
2021-06-01 17:24:28 -07:00
groupDescription?: string;
hasAvatar?: boolean;
2022-07-21 20:44:35 -04:00
hasStories?: HasStories;
id: string;
2020-05-27 17:37:06 -04:00
i18n: LocalizerType;
isDirectConvoAndHasNickname?: boolean;
2021-05-07 17:21:10 -05:00
isMe: boolean;
invitesCount?: number;
2022-11-08 21:38:19 -05:00
isSignalConversation?: boolean;
2020-05-27 17:37:06 -04:00
membersCount?: number;
memberships: ReadonlyArray<GroupV2Membership>;
openConversationDetails?: () => unknown;
pendingAvatarDownload?: boolean;
phoneNumber?: string;
sharedGroupNames?: ReadonlyArray<string>;
startAvatarDownload: () => void;
updateSharedGroups: (conversationId: string) => unknown;
2021-11-02 18:01:13 -05:00
theme: ThemeType;
2022-08-22 13:44:23 -04:00
viewUserStories: ViewUserStoriesActionCreatorType;
toggleAboutContactModal: (conversationId: string) => unknown;
toggleProfileNameWarningModal: (conversationType?: string) => unknown;
2020-05-27 17:37:06 -04:00
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
const renderExtraInformation = ({
acceptedMessageRequest,
2020-05-27 17:37:06 -04:00
conversationType,
fromOrAddedByTrustedContact,
i18n,
isDirectConvoAndHasNickname,
2020-05-27 17:37:06 -04:00
isMe,
invitesCount,
memberships,
onClickProfileNameWarning,
2024-03-12 09:29:31 -07:00
onToggleSafetyTips,
openConversationDetails,
phoneNumber,
sharedGroupNames,
}: Pick<
Props,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest'
| 'conversationType'
| 'fromOrAddedByTrustedContact'
| 'i18n'
| 'isDirectConvoAndHasNickname'
| 'isMe'
| 'invitesCount'
| 'membersCount'
| 'memberships'
| 'openConversationDetails'
| 'phoneNumber'
> &
Required<Pick<Props, 'sharedGroupNames'>> & {
onClickProfileNameWarning: () => void;
2024-03-12 09:29:31 -07:00
onToggleSafetyTips: (showSafetyTips: boolean) => void;
}) => {
if (conversationType !== 'direct' && conversationType !== 'group') {
return null;
}
2020-05-27 17:37:06 -04:00
if (isMe) {
return (
<div className="module-conversation-hero__note-to-self">
{i18n('icu:noteToSelfHero')}
</div>
);
2020-05-27 17:37:06 -04:00
}
const safetyTipsButton = !acceptedMessageRequest ? (
2024-03-12 09:29:31 -07:00
<div>
<Button
className="module-conversation-hero__safety-tips-button"
variant={ButtonVariant.SecondaryAffirmative}
onClick={() => {
onToggleSafetyTips(true);
}}
>
{i18n('icu:MessageRequestWarning__safety-tips')}
</Button>
</div>
) : null;
2024-03-12 09:29:31 -07:00
const shouldShowReviewCarefully =
!acceptedMessageRequest &&
(conversationType === 'group' || sharedGroupNames.length <= 1);
const reviewCarefullyLabel = shouldShowReviewCarefully ? (
<div className="module-conversation-hero__review-carefully">
<i className="module-conversation-hero__membership__review-carefully-icon" />
{i18n('icu:ConversationHero--review-carefully')}
</div>
) : null;
const sharedGroupsLabel =
conversationType === 'direct' ? (
<div>
<i className="module-conversation-hero__membership__chevron" />
2021-04-16 10:51:26 -05:00
<SharedGroupNames
i18n={i18n}
nameClassName="module-conversation-hero__membership__name"
2021-04-16 10:51:26 -05:00
sharedGroupNames={sharedGroupNames}
/>
</div>
) : null;
const nameNotVerifiedLabel =
!fromOrAddedByTrustedContact && !isDirectConvoAndHasNickname ? (
<div className="module-conversation-hero__name-not-verified">
<i
className={classNames({
'module-conversation-hero__group-question-icon':
conversationType === 'group',
'module-conversation-hero__direct-question-icon':
conversationType === 'direct',
})}
/>
<I18n
components={{
clickable: (parts: ReactNode) => (
<button
className="module-conversation-hero__name-not-verified__button"
type="button"
onClick={ev => {
ev.preventDefault();
onClickProfileNameWarning();
}}
>
{parts}
</button>
),
}}
i18n={i18n}
id={
conversationType === 'group'
? 'icu:ConversationHero--group-names'
: 'icu:ConversationHero--profile-names'
}
/>
</div>
) : null;
const membersCountLabel =
conversationType === 'group' ? (
<div className="module-conversation-hero__membership__members-count">
<i className="module-conversation-hero__members-count-icon" />
<GroupMembersNames
i18n={i18n}
nameClassName="module-conversation-hero__membership__name"
memberships={memberships}
invitesCount={invitesCount}
onOtherMembersClick={openConversationDetails}
/>
</div>
) : null;
if (
conversationType === 'direct' &&
sharedGroupNames.length === 0 &&
acceptedMessageRequest &&
phoneNumber
) {
return null;
}
// Check if we should show anything at all
const shouldShowAnything =
Boolean(reviewCarefullyLabel) ||
Boolean(nameNotVerifiedLabel) ||
Boolean(sharedGroupsLabel) ||
Boolean(safetyTipsButton) ||
Boolean(membersCountLabel);
if (!shouldShowAnything) {
return null;
}
return (
<div className="module-conversation-hero__membership">
{reviewCarefullyLabel}
{nameNotVerifiedLabel}
{sharedGroupsLabel}
{membersCountLabel}
2024-03-12 09:29:31 -07:00
{safetyTipsButton}
</div>
);
2020-05-27 17:37:06 -04:00
};
function ReleaseNotesExtraInformation({
i18n,
}: {
i18n: LocalizerType;
}): JSX.Element {
return (
<div className="module-conversation-hero--release-notes-notice">
<div className="module-conversation-hero__release-notes-notice-content">
<i className="module-conversation-hero__release-notes-notice-check-icon" />
{i18n('icu:ConversationHero--signal-official-chat')}
</div>
<div className="module-conversation-hero__release-notes-notice-content">
<i className="module-conversation-hero__release-notes-notice-bell-icon" />
{i18n('icu:ConversationHero--release-notes')}
</div>
</div>
);
}
2022-11-17 16:45:19 -08:00
export function ConversationHero({
avatarPlaceholderGradient,
2020-05-27 17:37:06 -04:00
i18n,
2021-01-25 20:01:19 -05:00
about,
acceptedMessageRequest,
2024-07-11 12:44:09 -07:00
avatarUrl,
2021-11-02 18:01:13 -05:00
badge,
2020-05-27 17:37:06 -04:00
color,
conversationType,
fromOrAddedByTrustedContact,
2021-06-01 17:24:28 -07:00
groupDescription,
hasAvatar,
2022-07-21 20:44:35 -04:00
hasStories,
id,
isDirectConvoAndHasNickname,
2020-05-27 17:37:06 -04:00
isMe,
invitesCount,
openConversationDetails,
2022-11-08 21:38:19 -05:00
isSignalConversation,
2020-05-27 17:37:06 -04:00
membersCount,
memberships,
pendingAvatarDownload,
2020-08-04 18:13:19 -07:00
sharedGroupNames = [],
2020-05-27 17:37:06 -04:00
phoneNumber,
profileName,
startAvatarDownload,
2021-11-02 18:01:13 -05:00
theme,
2020-07-23 18:35:32 -07:00
title,
2020-08-06 17:50:54 -07:00
updateSharedGroups,
2022-07-21 20:44:35 -04:00
viewUserStories,
toggleAboutContactModal,
toggleProfileNameWarningModal,
2022-11-17 16:45:19 -08:00
}: Props): JSX.Element {
2024-03-12 09:29:31 -07:00
const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false);
2020-08-06 17:50:54 -07:00
useEffect(() => {
// Kick off the expensive hydration of the current sharedGroupNames
updateSharedGroups(id);
}, [id, updateSharedGroups]);
2022-07-21 20:44:35 -04:00
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
let avatarOnClick: undefined | (() => void);
if (!avatarUrl && !isMe && hasAvatar) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = () => {
if (!pendingAvatarDownload) {
startAvatarDownload();
}
};
2022-07-21 20:44:35 -04:00
} else if (hasStories) {
avatarOnClick = () => {
2022-08-22 13:44:23 -04:00
viewUserStories({
conversationId: id,
storyViewMode: StoryViewModeType.User,
});
2022-07-21 20:44:35 -04:00
};
}
let titleElem: JSX.Element | undefined;
if (isMe) {
2025-04-22 12:33:51 -05:00
titleElem = (
<ContactName
isMe={isMe}
title={i18n('icu:noteToSelf')}
largeVerifiedBadge={isMe}
/>
);
} else if (isSignalConversation || conversationType !== 'direct') {
titleElem = (
2025-04-22 12:33:51 -05:00
<ContactName
isSignalConversation={isSignalConversation}
title={title}
largeVerifiedBadge={isSignalConversation}
/>
);
} 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>
);
}
2020-07-23 18:35:32 -07:00
2020-05-27 17:37:06 -04:00
return (
<>
<div className="module-conversation-hero">
<Avatar
avatarPlaceholderGradient={avatarPlaceholderGradient}
2024-07-11 12:44:09 -07:00
avatarUrl={avatarUrl}
2021-11-02 18:01:13 -05:00
badge={badge}
blur={avatarBlur}
className="module-conversation-hero__avatar"
color={color}
conversationType={conversationType}
i18n={i18n}
hasAvatar={hasAvatar}
loading={pendingAvatarDownload && !avatarUrl}
noteToSelf={isMe}
onClick={avatarOnClick}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
2022-12-09 13:37:45 -07:00
size={AvatarSize.EIGHTY}
2022-10-04 15:39:29 -06:00
// user may have stories, but we don't show that on Note to Self conversation
storyRing={isMe ? undefined : hasStories}
2021-11-02 18:01:13 -05:00
theme={theme}
title={title}
/>
2025-04-22 12:33:51 -05:00
<h1 className="module-conversation-hero__profile-name">{titleElem}</h1>
{about && !isMe && (
<div className="module-about__container">
<About text={about} />
</div>
2020-05-27 17:37:06 -04:00
)}
{!isMe && groupDescription ? (
<div className="module-conversation-hero__with">
<GroupDescription
i18n={i18n}
title={title}
text={groupDescription}
/>
</div>
) : null}
2022-11-08 21:38:19 -05:00
{!isSignalConversation &&
renderExtraInformation({
2022-11-08 21:38:19 -05:00
acceptedMessageRequest,
conversationType,
fromOrAddedByTrustedContact,
2022-11-08 21:38:19 -05:00
i18n,
isDirectConvoAndHasNickname,
2022-11-08 21:38:19 -05:00
isMe,
invitesCount,
membersCount,
memberships,
onClickProfileNameWarning() {
toggleProfileNameWarningModal(conversationType);
2022-11-08 21:38:19 -05:00
},
2024-03-12 09:29:31 -07:00
onToggleSafetyTips(showSafetyTips: boolean) {
setIsShowingSafetyTips(showSafetyTips);
},
openConversationDetails,
2022-11-08 21:38:19 -05:00
phoneNumber,
sharedGroupNames,
})}
{isSignalConversation && <ReleaseNotesExtraInformation i18n={i18n} />}
</div>
2024-03-12 09:29:31 -07:00
{isShowingSafetyTips && (
<SafetyTipsModal
i18n={i18n}
onClose={() => {
setIsShowingSafetyTips(false);
}}
/>
)}
</>
2020-05-27 17:37:06 -04:00
);
2022-11-17 16:45:19 -08:00
}