Signal-Desktop/ts/components/conversation/ConversationHero.tsx

399 lines
11 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// 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';
import { Avatar, AvatarSize, AvatarBlur } from '../Avatar';
import { ContactName } from './ContactName';
import { About } from './About';
import { GroupDescription } from './GroupDescription';
import { SharedGroupNames } from '../SharedGroupNames';
import { GroupMembersNames } from '../GroupMembersNames';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { HasStories } from '../../types/Stories';
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import type { GroupV2Membership } from './conversation-details/ConversationDetailsMembershipList';
import { StoryViewModeType } from '../../types/Stories';
import { Button, ButtonVariant } from '../Button';
import { SafetyTipsModal } from '../SafetyTipsModal';
import { I18n } from '../I18n';
export type Props = {
about?: string;
acceptedMessageRequest?: boolean;
fromOrAddedByTrustedContact?: boolean;
groupDescription?: string;
hasAvatar?: boolean;
hasStories?: HasStories;
id: string;
i18n: LocalizerType;
isDirectConvoAndHasNickname?: boolean;
isMe: boolean;
invitesCount?: number;
isSignalConversation?: boolean;
membersCount?: number;
memberships: ReadonlyArray<GroupV2Membership>;
openConversationDetails?: () => unknown;
pendingAvatarDownload?: boolean;
phoneNumber?: string;
sharedGroupNames?: ReadonlyArray<string>;
startAvatarDownload: () => void;
updateSharedGroups: (conversationId: string) => unknown;
theme: ThemeType;
viewUserStories: ViewUserStoriesActionCreatorType;
toggleAboutContactModal: (conversationId: string) => unknown;
toggleProfileNameWarningModal: (conversationType?: string) => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
const renderExtraInformation = ({
acceptedMessageRequest,
conversationType,
fromOrAddedByTrustedContact,
i18n,
isDirectConvoAndHasNickname,
isMe,
invitesCount,
memberships,
onClickProfileNameWarning,
onToggleSafetyTips,
openConversationDetails,
phoneNumber,
sharedGroupNames,
}: Pick<
Props,
| 'avatarPlaceholderGradient'
| 'acceptedMessageRequest'
| 'conversationType'
| 'fromOrAddedByTrustedContact'
| 'i18n'
| 'isDirectConvoAndHasNickname'
| 'isMe'
| 'invitesCount'
| 'membersCount'
| 'memberships'
| 'openConversationDetails'
| 'phoneNumber'
> &
Required<Pick<Props, 'sharedGroupNames'>> & {
onClickProfileNameWarning: () => void;
onToggleSafetyTips: (showSafetyTips: boolean) => void;
}) => {
if (conversationType !== 'direct' && conversationType !== 'group') {
return null;
}
if (isMe) {
return (
<div className="module-conversation-hero__note-to-self">
{i18n('icu:noteToSelfHero')}
</div>
);
}
const safetyTipsButton = !acceptedMessageRequest ? (
<div>
<Button
className="module-conversation-hero__safety-tips-button"
variant={ButtonVariant.SecondaryAffirmative}
onClick={() => {
onToggleSafetyTips(true);
}}
>
{i18n('icu:MessageRequestWarning__safety-tips')}
</Button>
</div>
) : null;
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" />
<SharedGroupNames
i18n={i18n}
nameClassName="module-conversation-hero__membership__name"
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}
{safetyTipsButton}
</div>
);
};
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>
);
}
export function ConversationHero({
avatarPlaceholderGradient,
i18n,
about,
acceptedMessageRequest,
avatarUrl,
badge,
color,
conversationType,
fromOrAddedByTrustedContact,
groupDescription,
hasAvatar,
hasStories,
id,
isDirectConvoAndHasNickname,
isMe,
invitesCount,
openConversationDetails,
isSignalConversation,
membersCount,
memberships,
pendingAvatarDownload,
sharedGroupNames = [],
phoneNumber,
profileName,
startAvatarDownload,
theme,
title,
updateSharedGroups,
viewUserStories,
toggleAboutContactModal,
toggleProfileNameWarningModal,
}: Props): JSX.Element {
const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false);
useEffect(() => {
// Kick off the expensive hydration of the current sharedGroupNames
updateSharedGroups(id);
}, [id, updateSharedGroups]);
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
let avatarOnClick: undefined | (() => void);
if (!avatarUrl && !isMe && hasAvatar) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = () => {
if (!pendingAvatarDownload) {
startAvatarDownload();
}
};
} else if (hasStories) {
avatarOnClick = () => {
viewUserStories({
conversationId: id,
storyViewMode: StoryViewModeType.User,
});
};
}
let titleElem: JSX.Element | undefined;
if (isMe) {
titleElem = (
<ContactName
isMe={isMe}
title={i18n('icu:noteToSelf')}
largeVerifiedBadge={isMe}
/>
);
} else if (isSignalConversation || conversationType !== 'direct') {
titleElem = (
<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>
);
}
return (
<>
<div className="module-conversation-hero">
<Avatar
avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl}
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}
size={AvatarSize.EIGHTY}
// user may have stories, but we don't show that on Note to Self conversation
storyRing={isMe ? undefined : hasStories}
theme={theme}
title={title}
/>
<h1 className="module-conversation-hero__profile-name">{titleElem}</h1>
{about && !isMe && (
<div className="module-about__container">
<About text={about} />
</div>
)}
{!isMe && groupDescription ? (
<div className="module-conversation-hero__with">
<GroupDescription
i18n={i18n}
title={title}
text={groupDescription}
/>
</div>
) : null}
{!isSignalConversation &&
renderExtraInformation({
acceptedMessageRequest,
conversationType,
fromOrAddedByTrustedContact,
i18n,
isDirectConvoAndHasNickname,
isMe,
invitesCount,
membersCount,
memberships,
onClickProfileNameWarning() {
toggleProfileNameWarningModal(conversationType);
},
onToggleSafetyTips(showSafetyTips: boolean) {
setIsShowingSafetyTips(showSafetyTips);
},
openConversationDetails,
phoneNumber,
sharedGroupNames,
})}
{isSignalConversation && <ReleaseNotesExtraInformation i18n={i18n} />}
</div>
{isShowingSafetyTips && (
<SafetyTipsModal
i18n={i18n}
onClose={() => {
setIsShowingSafetyTips(false);
}}
/>
)}
</>
);
}