2022-07-22 00:44:35 +00:00
|
|
|
// Copyright 2018-2022 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { ReactNode } from 'react';
|
|
|
|
import React from 'react';
|
2021-03-01 20:08:37 +00:00
|
|
|
import Measure from 'react-measure';
|
2019-07-31 16:16:29 +00:00
|
|
|
import classNames from 'classnames';
|
2018-07-09 21:29:13 +00:00
|
|
|
import {
|
|
|
|
ContextMenu,
|
|
|
|
ContextMenuTrigger,
|
|
|
|
MenuItem,
|
|
|
|
SubMenu,
|
|
|
|
} from 'react-contextmenu';
|
|
|
|
|
2020-07-24 01:35:32 +00:00
|
|
|
import { Emojify } from './Emojify';
|
2021-06-25 23:52:56 +00:00
|
|
|
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
2020-12-12 00:45:14 +00:00
|
|
|
import { Avatar, AvatarSize } from '../Avatar';
|
2020-07-24 01:35:32 +00:00
|
|
|
import { InContactsIcon } from '../InContactsIcon';
|
|
|
|
|
2021-11-02 23:01:13 +00:00
|
|
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
2022-12-15 01:10:09 +00:00
|
|
|
import type {
|
|
|
|
ConversationType,
|
2022-12-21 03:25:10 +00:00
|
|
|
PopPanelForConversationActionType,
|
2022-12-15 01:10:09 +00:00
|
|
|
PushPanelForConversationActionType,
|
|
|
|
} from '../../state/ducks/conversations';
|
2021-11-02 23:01:13 +00:00
|
|
|
import type { BadgeType } from '../../badges/types';
|
2022-07-22 00:44:35 +00:00
|
|
|
import type { HasStories } from '../../types/Stories';
|
2022-08-22 17:44:23 +00:00
|
|
|
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
|
|
|
|
import { StoryViewModeType } from '../../types/Stories';
|
2021-08-05 12:35:33 +00:00
|
|
|
import { getMuteOptions } from '../../util/getMuteOptions';
|
2021-05-03 23:24:40 +00:00
|
|
|
import * as expirationTimer from '../../util/expirationTimer';
|
2020-11-19 16:37:56 +00:00
|
|
|
import { missingCaseError } from '../../util/missingCaseError';
|
2021-06-02 17:24:22 +00:00
|
|
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
2022-11-09 02:38:19 +00:00
|
|
|
import { isConversationMuted } from '../../util/isConversationMuted';
|
2022-12-05 22:56:23 +00:00
|
|
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
2022-11-16 20:18:02 +00:00
|
|
|
import { DurationInSeconds } from '../../util/durations';
|
2022-05-10 18:14:08 +00:00
|
|
|
import {
|
|
|
|
useStartCallShortcuts,
|
|
|
|
useKeyboardShortcuts,
|
|
|
|
} from '../../hooks/useKeyboardShortcuts';
|
2022-12-15 01:10:09 +00:00
|
|
|
import { PanelType } from '../../types/Panels';
|
2020-11-19 16:37:56 +00:00
|
|
|
|
|
|
|
export enum OutgoingCallButtonStyle {
|
|
|
|
None,
|
|
|
|
JustVideo,
|
|
|
|
Both,
|
2020-11-20 17:19:28 +00:00
|
|
|
Join,
|
2020-11-19 16:37:56 +00:00
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type PropsDataType = {
|
2021-11-02 23:01:13 +00:00
|
|
|
badge?: BadgeType;
|
2021-01-29 21:19:24 +00:00
|
|
|
conversationTitle?: string;
|
2022-07-22 00:44:35 +00:00
|
|
|
hasStories?: HasStories;
|
2020-10-30 17:52:21 +00:00
|
|
|
isMissingMandatoryProfileSharing?: boolean;
|
2020-11-19 16:37:56 +00:00
|
|
|
outgoingCallButtonStyle: OutgoingCallButtonStyle;
|
2021-04-30 19:40:25 +00:00
|
|
|
showBackButton?: boolean;
|
2021-05-25 22:30:57 +00:00
|
|
|
isSMSOnly?: boolean;
|
2022-11-09 02:38:19 +00:00
|
|
|
isSignalConversation?: boolean;
|
2021-11-02 23:01:13 +00:00
|
|
|
theme: ThemeType;
|
2021-04-30 19:40:25 +00:00
|
|
|
} & Pick<
|
|
|
|
ConversationType,
|
|
|
|
| 'acceptedMessageRequest'
|
2021-07-20 20:18:35 +00:00
|
|
|
| 'announcementsOnly'
|
|
|
|
| 'areWeAdmin'
|
2021-04-30 19:40:25 +00:00
|
|
|
| 'avatarPath'
|
|
|
|
| 'canChangeTimer'
|
|
|
|
| 'color'
|
|
|
|
| 'expireTimer'
|
|
|
|
| 'groupVersion'
|
|
|
|
| 'id'
|
|
|
|
| 'isArchived'
|
|
|
|
| 'isMe'
|
|
|
|
| 'isPinned'
|
|
|
|
| 'isVerified'
|
|
|
|
| 'left'
|
|
|
|
| 'markedUnread'
|
|
|
|
| 'muteExpiresAt'
|
|
|
|
| 'name'
|
|
|
|
| 'phoneNumber'
|
|
|
|
| 'profileName'
|
|
|
|
| 'sharedGroupNames'
|
|
|
|
| 'title'
|
|
|
|
| 'type'
|
|
|
|
| 'unblurredAvatarPath'
|
|
|
|
>;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type PropsActionsType = {
|
2022-12-05 22:56:23 +00:00
|
|
|
destroyMessages: (conversationId: string) => void;
|
2022-12-21 03:25:10 +00:00
|
|
|
onArchive: (conversationId: string) => void;
|
|
|
|
onMarkUnread: (conversationId: string) => void;
|
|
|
|
onMoveToInbox: (conversationId: string) => void;
|
2022-12-06 17:31:44 +00:00
|
|
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
|
|
|
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
2022-12-15 01:10:09 +00:00
|
|
|
pushPanelForConversation: PushPanelForConversationActionType;
|
2022-12-21 03:25:10 +00:00
|
|
|
popPanelForConversation: PopPanelForConversationActionType;
|
|
|
|
searchInConversation: (conversationId: string) => void;
|
2022-12-05 22:56:23 +00:00
|
|
|
setDisappearingMessages: (
|
|
|
|
conversationId: string,
|
|
|
|
seconds: DurationInSeconds
|
|
|
|
) => void;
|
2022-12-14 19:05:32 +00:00
|
|
|
setMuteExpiration: (conversationId: string, seconds: number) => void;
|
2022-12-07 01:00:02 +00:00
|
|
|
setPinned: (conversationId: string, value: boolean) => void;
|
2022-08-22 17:44:23 +00:00
|
|
|
viewUserStories: ViewUserStoriesActionCreatorType;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2019-03-12 00:20:16 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type PropsHousekeepingType = {
|
2019-03-12 00:20:16 +00:00
|
|
|
i18n: LocalizerType;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2020-07-24 01:35:32 +00:00
|
|
|
export type PropsType = PropsDataType &
|
|
|
|
PropsActionsType &
|
|
|
|
PropsHousekeepingType;
|
2020-03-26 21:47:35 +00:00
|
|
|
|
2021-06-01 20:45:43 +00:00
|
|
|
enum ModalState {
|
|
|
|
NothingOpen,
|
|
|
|
CustomDisappearingTimeout,
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
type StateType = {
|
2022-12-05 22:56:23 +00:00
|
|
|
hasDeleteMessagesConfirmation: boolean;
|
2021-03-01 20:08:37 +00:00
|
|
|
isNarrow: boolean;
|
2021-06-01 20:45:43 +00:00
|
|
|
modalState: ModalState;
|
2021-03-01 20:08:37 +00:00
|
|
|
};
|
|
|
|
|
2021-06-01 20:45:43 +00:00
|
|
|
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|
|
|
private showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
2020-09-14 19:51:27 +00:00
|
|
|
|
|
|
|
// Comes from a third-party dependency
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-03-01 20:08:37 +00:00
|
|
|
private menuTriggerRef: React.RefObject<any>;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-10-18 22:10:22 +00:00
|
|
|
public headerRef: React.RefObject<HTMLDivElement>;
|
|
|
|
|
2020-07-24 01:35:32 +00:00
|
|
|
public constructor(props: PropsType) {
|
2018-07-09 21:29:13 +00:00
|
|
|
super(props);
|
|
|
|
|
2022-12-05 22:56:23 +00:00
|
|
|
this.state = {
|
|
|
|
hasDeleteMessagesConfirmation: false,
|
|
|
|
isNarrow: false,
|
|
|
|
modalState: ModalState.NothingOpen,
|
|
|
|
};
|
2021-03-01 20:08:37 +00:00
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
this.menuTriggerRef = React.createRef();
|
2021-10-18 22:10:22 +00:00
|
|
|
this.headerRef = React.createRef();
|
2018-07-09 21:29:13 +00:00
|
|
|
this.showMenuBound = this.showMenu.bind(this);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
2019-01-14 21:49:58 +00:00
|
|
|
if (this.menuTriggerRef.current) {
|
|
|
|
this.menuTriggerRef.current.handleContextClick(event);
|
2018-07-09 21:29:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderBackButton(): ReactNode {
|
2022-12-21 03:25:10 +00:00
|
|
|
const { i18n, id, popPanelForConversation, showBackButton } = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
return (
|
2019-07-25 16:24:03 +00:00
|
|
|
<button
|
2020-09-14 19:51:27 +00:00
|
|
|
type="button"
|
2022-12-21 03:25:10 +00:00
|
|
|
onClick={() => popPanelForConversation(id)}
|
2019-07-31 16:16:29 +00:00
|
|
|
className={classNames(
|
2021-03-01 20:08:37 +00:00
|
|
|
'module-ConversationHeader__back-icon',
|
|
|
|
showBackButton ? 'module-ConversationHeader__back-icon--show' : null
|
2019-07-25 16:24:03 +00:00
|
|
|
)}
|
|
|
|
disabled={!showBackButton}
|
2020-09-14 19:51:27 +00:00
|
|
|
aria-label={i18n('goBack')}
|
2018-07-09 21:29:13 +00:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderHeaderInfoTitle(): ReactNode {
|
|
|
|
const { name, title, type, i18n, isMe } = this.props;
|
2019-01-31 01:45:58 +00:00
|
|
|
|
|
|
|
if (isMe) {
|
|
|
|
return (
|
2021-03-01 20:08:37 +00:00
|
|
|
<div className="module-ConversationHeader__header__info__title">
|
2019-01-31 01:45:58 +00:00
|
|
|
{i18n('noteToSelf')}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
return (
|
2021-03-01 20:08:37 +00:00
|
|
|
<div className="module-ConversationHeader__header__info__title">
|
2020-07-24 01:35:32 +00:00
|
|
|
<Emojify text={title} />
|
2021-06-02 17:24:22 +00:00
|
|
|
{isInSystemContacts({ name, type }) ? (
|
2021-03-01 20:08:37 +00:00
|
|
|
<InContactsIcon
|
|
|
|
className="module-ConversationHeader__header__info__title__in-contacts-icon"
|
|
|
|
i18n={i18n}
|
2021-10-18 22:10:22 +00:00
|
|
|
tooltipContainerRef={this.headerRef}
|
2021-03-01 20:08:37 +00:00
|
|
|
/>
|
2018-07-09 21:29:13 +00:00
|
|
|
) : null}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderHeaderInfoSubtitle(): ReactNode {
|
|
|
|
const expirationNode = this.renderExpirationLength();
|
|
|
|
const verifiedNode = this.renderVerifiedIcon();
|
|
|
|
|
|
|
|
if (expirationNode || verifiedNode) {
|
|
|
|
return (
|
|
|
|
<div className="module-ConversationHeader__header__info__subtitle">
|
|
|
|
{expirationNode}
|
|
|
|
{verifiedNode}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderAvatar(): ReactNode {
|
2018-07-09 21:29:13 +00:00
|
|
|
const {
|
2021-04-30 19:40:25 +00:00
|
|
|
acceptedMessageRequest,
|
2018-07-09 21:29:13 +00:00
|
|
|
avatarPath,
|
2021-11-02 23:01:13 +00:00
|
|
|
badge,
|
2018-07-09 21:29:13 +00:00
|
|
|
color,
|
2022-07-22 00:44:35 +00:00
|
|
|
hasStories,
|
|
|
|
id,
|
2018-07-09 21:29:13 +00:00
|
|
|
i18n,
|
2020-07-24 01:35:32 +00:00
|
|
|
type,
|
2019-01-31 01:45:58 +00:00
|
|
|
isMe,
|
2018-07-09 21:29:13 +00:00
|
|
|
phoneNumber,
|
|
|
|
profileName,
|
2021-04-30 19:40:25 +00:00
|
|
|
sharedGroupNames,
|
2021-11-02 23:01:13 +00:00
|
|
|
theme,
|
2020-07-24 01:35:32 +00:00
|
|
|
title,
|
2021-04-30 19:40:25 +00:00
|
|
|
unblurredAvatarPath,
|
2022-07-22 00:44:35 +00:00
|
|
|
viewUserStories,
|
2018-07-09 21:29:13 +00:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
2021-03-01 20:08:37 +00:00
|
|
|
<span className="module-ConversationHeader__header__avatar">
|
2018-09-27 00:23:17 +00:00
|
|
|
<Avatar
|
2021-04-30 19:40:25 +00:00
|
|
|
acceptedMessageRequest={acceptedMessageRequest}
|
2018-09-27 00:23:17 +00:00
|
|
|
avatarPath={avatarPath}
|
2021-11-02 23:01:13 +00:00
|
|
|
badge={badge}
|
2018-09-27 00:23:17 +00:00
|
|
|
color={color}
|
2020-07-24 01:35:32 +00:00
|
|
|
conversationType={type}
|
2018-09-27 00:23:17 +00:00
|
|
|
i18n={i18n}
|
2021-04-30 19:40:25 +00:00
|
|
|
isMe={isMe}
|
2022-07-22 00:44:35 +00:00
|
|
|
noteToSelf={isMe}
|
|
|
|
onClick={
|
|
|
|
hasStories
|
|
|
|
? () => {
|
2022-08-22 17:44:23 +00:00
|
|
|
viewUserStories({
|
|
|
|
conversationId: id,
|
|
|
|
storyViewMode: StoryViewModeType.User,
|
|
|
|
});
|
2022-07-22 00:44:35 +00:00
|
|
|
}
|
|
|
|
: undefined
|
|
|
|
}
|
2018-09-27 00:23:17 +00:00
|
|
|
phoneNumber={phoneNumber}
|
|
|
|
profileName={profileName}
|
2021-04-30 19:40:25 +00:00
|
|
|
sharedGroupNames={sharedGroupNames}
|
2020-12-12 00:45:14 +00:00
|
|
|
size={AvatarSize.THIRTY_TWO}
|
2022-10-04 21:39:29 +00:00
|
|
|
// user may have stories, but we don't show that on Note to Self conversation
|
|
|
|
storyRing={isMe ? undefined : hasStories}
|
2021-11-02 23:01:13 +00:00
|
|
|
theme={theme}
|
2022-07-22 00:44:35 +00:00
|
|
|
title={title}
|
2021-04-30 19:40:25 +00:00
|
|
|
unblurredAvatarPath={unblurredAvatarPath}
|
2018-09-27 00:23:17 +00:00
|
|
|
/>
|
|
|
|
</span>
|
2018-07-09 21:29:13 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderExpirationLength(): ReactNode {
|
|
|
|
const { i18n, expireTimer } = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-05-03 23:24:40 +00:00
|
|
|
if (!expireTimer) {
|
2018-07-09 21:29:13 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2021-03-01 20:08:37 +00:00
|
|
|
<div className="module-ConversationHeader__header__info__subtitle__expiration">
|
2021-05-03 23:24:40 +00:00
|
|
|
{expirationTimer.format(i18n, expireTimer)}
|
2018-07-09 21:29:13 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderVerifiedIcon(): ReactNode {
|
|
|
|
const { i18n, isVerified } = this.props;
|
|
|
|
|
|
|
|
if (!isVerified) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="module-ConversationHeader__header__info__subtitle__verified">
|
|
|
|
{i18n('verified')}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderMoreButton(triggerId: string): ReactNode {
|
2020-09-14 19:51:27 +00:00
|
|
|
const { i18n, showBackButton } = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
return (
|
2019-01-14 21:49:58 +00:00
|
|
|
<ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}>
|
2019-07-25 16:24:03 +00:00
|
|
|
<button
|
2020-09-14 19:51:27 +00:00
|
|
|
type="button"
|
2018-07-09 21:29:13 +00:00
|
|
|
onClick={this.showMenuBound}
|
2019-07-31 16:16:29 +00:00
|
|
|
className={classNames(
|
2021-03-03 01:16:04 +00:00
|
|
|
'module-ConversationHeader__button',
|
|
|
|
'module-ConversationHeader__button--more',
|
|
|
|
showBackButton ? null : 'module-ConversationHeader__button--show'
|
2019-07-25 16:24:03 +00:00
|
|
|
)}
|
|
|
|
disabled={showBackButton}
|
2020-09-14 19:51:27 +00:00
|
|
|
aria-label={i18n('moreInfo')}
|
2018-07-09 21:29:13 +00:00
|
|
|
/>
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderSearchButton(): ReactNode {
|
2022-12-21 03:25:10 +00:00
|
|
|
const { i18n, id, searchInConversation, showBackButton } = this.props;
|
2019-08-09 23:12:29 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<button
|
2020-09-14 19:51:27 +00:00
|
|
|
type="button"
|
2022-12-21 03:25:10 +00:00
|
|
|
onClick={() => searchInConversation(id)}
|
2019-08-09 23:12:29 +00:00
|
|
|
className={classNames(
|
2021-03-03 01:16:04 +00:00
|
|
|
'module-ConversationHeader__button',
|
|
|
|
'module-ConversationHeader__button--search',
|
|
|
|
showBackButton ? null : 'module-ConversationHeader__button--show'
|
2019-08-09 23:12:29 +00:00
|
|
|
)}
|
|
|
|
disabled={showBackButton}
|
2020-09-14 19:51:27 +00:00
|
|
|
aria-label={i18n('search')}
|
2019-08-09 23:12:29 +00:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderMenu(triggerId: string): ReactNode {
|
2018-07-09 21:29:13 +00:00
|
|
|
const {
|
2020-10-28 21:54:33 +00:00
|
|
|
acceptedMessageRequest,
|
2020-10-30 17:52:21 +00:00
|
|
|
canChangeTimer,
|
2021-06-01 20:45:43 +00:00
|
|
|
expireTimer,
|
2021-10-20 23:46:41 +00:00
|
|
|
groupVersion,
|
|
|
|
i18n,
|
2022-12-05 22:56:23 +00:00
|
|
|
id,
|
2020-10-30 17:52:21 +00:00
|
|
|
isArchived,
|
2021-10-20 23:46:41 +00:00
|
|
|
isMissingMandatoryProfileSharing,
|
2020-10-02 18:30:43 +00:00
|
|
|
isPinned,
|
2022-11-09 02:38:19 +00:00
|
|
|
isSignalConversation,
|
2021-10-20 23:46:41 +00:00
|
|
|
left,
|
2020-10-28 22:54:32 +00:00
|
|
|
markedUnread,
|
2020-10-30 17:52:21 +00:00
|
|
|
muteExpiresAt,
|
2021-10-20 23:46:41 +00:00
|
|
|
onArchive,
|
|
|
|
onMarkUnread,
|
|
|
|
onMoveToInbox,
|
2022-12-15 01:10:09 +00:00
|
|
|
pushPanelForConversation,
|
2022-12-05 22:56:23 +00:00
|
|
|
setDisappearingMessages,
|
2022-12-06 17:31:44 +00:00
|
|
|
setMuteExpiration,
|
2022-12-07 01:00:02 +00:00
|
|
|
setPinned,
|
2021-10-20 23:46:41 +00:00
|
|
|
type,
|
2018-07-09 21:29:13 +00:00
|
|
|
} = this.props;
|
|
|
|
|
2021-08-05 12:35:33 +00:00
|
|
|
const muteOptions = getMuteOptions(muteExpiresAt, i18n);
|
2020-08-27 19:45:08 +00:00
|
|
|
|
2022-11-09 02:38:19 +00:00
|
|
|
const muteTitle = <span>{i18n('muteNotificationsTitle')}</span>;
|
|
|
|
|
|
|
|
if (isSignalConversation) {
|
|
|
|
const isMuted = muteExpiresAt && isConversationMuted({ muteExpiresAt });
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ContextMenu id={triggerId}>
|
|
|
|
<SubMenu hoverDelay={1} title={muteTitle} rtl>
|
|
|
|
{isMuted ? (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
2022-12-06 17:31:44 +00:00
|
|
|
setMuteExpiration(id, 0);
|
2022-11-09 02:38:19 +00:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('unmute')}
|
|
|
|
</MenuItem>
|
|
|
|
) : (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
2022-12-06 17:31:44 +00:00
|
|
|
setMuteExpiration(id, Number.MAX_SAFE_INTEGER);
|
2022-11-09 02:38:19 +00:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('muteAlways')}
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
|
|
|
</SubMenu>
|
|
|
|
</ContextMenu>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const disappearingTitle = <span>{i18n('icu:disappearingMessages')}</span>;
|
2020-07-24 01:35:32 +00:00
|
|
|
const isGroup = type === 'group';
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
const disableTimerChanges = Boolean(
|
|
|
|
!canChangeTimer ||
|
|
|
|
!acceptedMessageRequest ||
|
|
|
|
left ||
|
|
|
|
isMissingMandatoryProfileSharing
|
|
|
|
);
|
|
|
|
|
2021-03-11 22:59:10 +00:00
|
|
|
const hasGV2AdminEnabled = isGroup && groupVersion === 2;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
2021-06-01 20:45:43 +00:00
|
|
|
const isActiveExpireTimer = (value: number): boolean => {
|
|
|
|
if (!expireTimer) {
|
|
|
|
return value === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Custom time...
|
|
|
|
if (value === -1) {
|
|
|
|
return !expirationTimer.DEFAULT_DURATIONS_SET.has(expireTimer);
|
|
|
|
}
|
|
|
|
return value === expireTimer;
|
|
|
|
};
|
|
|
|
|
|
|
|
const expireDurations: ReadonlyArray<ReactNode> = [
|
|
|
|
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
|
2022-11-16 20:18:02 +00:00
|
|
|
DurationInSeconds.fromSeconds(-1),
|
|
|
|
].map(seconds => {
|
2021-06-01 20:45:43 +00:00
|
|
|
let text: string;
|
|
|
|
|
|
|
|
if (seconds === -1) {
|
|
|
|
text = i18n('customDisappearingTimeOption');
|
|
|
|
} else {
|
|
|
|
text = expirationTimer.format(i18n, seconds, {
|
|
|
|
capitalizeOff: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const onDurationClick = () => {
|
|
|
|
if (seconds === -1) {
|
|
|
|
this.setState({
|
|
|
|
modalState: ModalState.CustomDisappearingTimeout,
|
|
|
|
});
|
|
|
|
} else {
|
2022-12-05 22:56:23 +00:00
|
|
|
setDisappearingMessages(id, seconds);
|
2021-06-01 20:45:43 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<MenuItem key={seconds} onClick={onDurationClick}>
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
TIMER_ITEM_CLASS,
|
|
|
|
isActiveExpireTimer(seconds) && `${TIMER_ITEM_CLASS}--active`
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{text}
|
|
|
|
</div>
|
|
|
|
</MenuItem>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
return (
|
|
|
|
<ContextMenu id={triggerId}>
|
2020-09-09 02:25:05 +00:00
|
|
|
{disableTimerChanges ? null : (
|
2021-10-29 06:52:45 +00:00
|
|
|
<SubMenu hoverDelay={1} title={disappearingTitle} rtl>
|
2021-07-13 22:27:28 +00:00
|
|
|
{expireDurations}
|
|
|
|
</SubMenu>
|
2020-09-09 02:25:05 +00:00
|
|
|
)}
|
2021-10-29 06:52:45 +00:00
|
|
|
<SubMenu hoverDelay={1} title={muteTitle} rtl>
|
2020-10-28 22:54:32 +00:00
|
|
|
{muteOptions.map(item => (
|
|
|
|
<MenuItem
|
|
|
|
key={item.name}
|
|
|
|
disabled={item.disabled}
|
|
|
|
onClick={() => {
|
2022-12-06 17:31:44 +00:00
|
|
|
setMuteExpiration(id, item.value);
|
2020-10-28 22:54:32 +00:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{item.name}
|
|
|
|
</MenuItem>
|
|
|
|
))}
|
|
|
|
</SubMenu>
|
2021-10-20 23:46:41 +00:00
|
|
|
{!isGroup || hasGV2AdminEnabled ? (
|
2022-12-16 03:12:05 +00:00
|
|
|
<MenuItem
|
|
|
|
onClick={() =>
|
|
|
|
pushPanelForConversation(id, {
|
|
|
|
type: PanelType.ConversationDetails,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
>
|
2021-10-20 23:46:41 +00:00
|
|
|
{isGroup
|
|
|
|
? i18n('showConversationDetails')
|
|
|
|
: i18n('showConversationDetails--direct')}
|
2021-01-29 21:19:24 +00:00
|
|
|
</MenuItem>
|
|
|
|
) : null}
|
|
|
|
{isGroup && !hasGV2AdminEnabled ? (
|
2022-12-15 01:10:09 +00:00
|
|
|
<MenuItem
|
|
|
|
onClick={() =>
|
|
|
|
pushPanelForConversation(id, { type: PanelType.GroupV1Members })
|
|
|
|
}
|
|
|
|
>
|
2018-07-09 21:29:13 +00:00
|
|
|
{i18n('showMembers')}
|
|
|
|
</MenuItem>
|
|
|
|
) : null}
|
2022-12-20 17:50:23 +00:00
|
|
|
<MenuItem
|
|
|
|
onClick={() =>
|
|
|
|
pushPanelForConversation(id, { type: PanelType.AllMedia })
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{i18n('viewRecentMedia')}
|
|
|
|
</MenuItem>
|
2020-10-28 22:54:32 +00:00
|
|
|
<MenuItem divider />
|
|
|
|
{!markedUnread ? (
|
2022-12-21 03:25:10 +00:00
|
|
|
<MenuItem onClick={() => onMarkUnread(id)}>
|
|
|
|
{i18n('markUnread')}
|
|
|
|
</MenuItem>
|
2020-10-28 22:54:32 +00:00
|
|
|
) : null}
|
2019-03-12 00:20:16 +00:00
|
|
|
{isArchived ? (
|
2022-12-21 03:25:10 +00:00
|
|
|
<MenuItem onClick={() => onMoveToInbox(id)}>
|
2019-03-12 00:20:16 +00:00
|
|
|
{i18n('moveConversationToInbox')}
|
|
|
|
</MenuItem>
|
|
|
|
) : (
|
2022-12-21 03:25:10 +00:00
|
|
|
<MenuItem onClick={() => onArchive(id)}>
|
|
|
|
{i18n('archiveConversation')}
|
|
|
|
</MenuItem>
|
2019-03-12 00:20:16 +00:00
|
|
|
)}
|
2022-12-05 22:56:23 +00:00
|
|
|
<MenuItem
|
|
|
|
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}
|
|
|
|
>
|
|
|
|
{i18n('deleteMessages')}
|
|
|
|
</MenuItem>
|
2020-10-28 22:54:32 +00:00
|
|
|
{isPinned ? (
|
2022-12-07 01:00:02 +00:00
|
|
|
<MenuItem onClick={() => setPinned(id, false)}>
|
2020-10-28 22:54:32 +00:00
|
|
|
{i18n('unpinConversation')}
|
|
|
|
</MenuItem>
|
|
|
|
) : (
|
2022-12-07 01:00:02 +00:00
|
|
|
<MenuItem onClick={() => setPinned(id, true)}>
|
2020-10-28 22:54:32 +00:00
|
|
|
{i18n('pinConversation')}
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
2018-07-09 21:29:13 +00:00
|
|
|
</ContextMenu>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-05 22:56:23 +00:00
|
|
|
private renderConfirmationDialog(): ReactNode {
|
|
|
|
const { hasDeleteMessagesConfirmation } = this.state;
|
|
|
|
const { destroyMessages, i18n, id } = this.props;
|
|
|
|
|
|
|
|
if (!hasDeleteMessagesConfirmation) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ConfirmationDialog
|
|
|
|
dialogName="ConversationHeader.destroyMessages"
|
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
action: () => {
|
|
|
|
this.setState({ hasDeleteMessagesConfirmation: false });
|
|
|
|
destroyMessages(id);
|
|
|
|
},
|
|
|
|
style: 'negative',
|
|
|
|
text: i18n('delete'),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
this.setState({ hasDeleteMessagesConfirmation: false });
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('deleteConversationConfirmation')}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
private renderHeader(): ReactNode {
|
2022-12-16 03:12:05 +00:00
|
|
|
const {
|
|
|
|
conversationTitle,
|
|
|
|
id,
|
|
|
|
groupVersion,
|
|
|
|
pushPanelForConversation,
|
|
|
|
type,
|
|
|
|
} = this.props;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
2021-02-01 22:57:42 +00:00
|
|
|
if (conversationTitle !== undefined) {
|
2021-01-29 21:19:24 +00:00
|
|
|
return (
|
2021-03-01 20:08:37 +00:00
|
|
|
<div className="module-ConversationHeader__header">
|
|
|
|
<div className="module-ConversationHeader__header__info">
|
|
|
|
<div className="module-ConversationHeader__header__info__title">
|
|
|
|
{conversationTitle}
|
|
|
|
</div>
|
2021-01-29 21:19:24 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2021-01-28 00:18:50 +00:00
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
let onClick: undefined | (() => void);
|
|
|
|
switch (type) {
|
|
|
|
case 'direct':
|
2021-10-21 21:06:44 +00:00
|
|
|
onClick = () => {
|
2022-12-16 03:12:05 +00:00
|
|
|
pushPanelForConversation(id, { type: PanelType.ConversationDetails });
|
2021-10-21 21:06:44 +00:00
|
|
|
};
|
2021-03-01 20:08:37 +00:00
|
|
|
break;
|
|
|
|
case 'group': {
|
2021-03-11 22:59:10 +00:00
|
|
|
const hasGV2AdminEnabled = groupVersion === 2;
|
2021-03-01 20:08:37 +00:00
|
|
|
onClick = hasGV2AdminEnabled
|
|
|
|
? () => {
|
2022-12-16 03:12:05 +00:00
|
|
|
pushPanelForConversation(id, {
|
|
|
|
type: PanelType.ConversationDetails,
|
|
|
|
});
|
2021-03-01 20:08:37 +00:00
|
|
|
}
|
|
|
|
: undefined;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
throw missingCaseError(type);
|
|
|
|
}
|
2021-02-01 22:57:42 +00:00
|
|
|
|
2022-07-22 00:44:35 +00:00
|
|
|
const avatar = this.renderAvatar();
|
2021-03-01 20:08:37 +00:00
|
|
|
const contents = (
|
2022-07-22 00:44:35 +00:00
|
|
|
<div className="module-ConversationHeader__header__info">
|
|
|
|
{this.renderHeaderInfoTitle()}
|
|
|
|
{this.renderHeaderInfoSubtitle()}
|
|
|
|
</div>
|
2021-03-01 20:08:37 +00:00
|
|
|
);
|
2021-02-01 22:57:42 +00:00
|
|
|
|
2021-03-01 20:08:37 +00:00
|
|
|
if (onClick) {
|
2021-01-28 00:18:50 +00:00
|
|
|
return (
|
2022-07-22 00:44:35 +00:00
|
|
|
<div className="module-ConversationHeader__header">
|
|
|
|
{avatar}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
className="module-ConversationHeader__header--clickable"
|
|
|
|
onClick={onClick}
|
|
|
|
>
|
|
|
|
{contents}
|
|
|
|
</button>
|
|
|
|
</div>
|
2021-01-28 00:18:50 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-18 22:10:22 +00:00
|
|
|
return (
|
|
|
|
<div className="module-ConversationHeader__header" ref={this.headerRef}>
|
2022-07-22 00:44:35 +00:00
|
|
|
{avatar}
|
2021-10-18 22:10:22 +00:00
|
|
|
{contents}
|
|
|
|
</div>
|
|
|
|
);
|
2021-01-28 00:18:50 +00:00
|
|
|
}
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
public override render(): ReactNode {
|
2022-05-10 18:14:08 +00:00
|
|
|
const {
|
|
|
|
announcementsOnly,
|
|
|
|
areWeAdmin,
|
|
|
|
expireTimer,
|
|
|
|
i18n,
|
|
|
|
id,
|
|
|
|
isSMSOnly,
|
2022-11-09 02:38:19 +00:00
|
|
|
isSignalConversation,
|
2022-05-10 18:14:08 +00:00
|
|
|
onOutgoingAudioCallInConversation,
|
|
|
|
onOutgoingVideoCallInConversation,
|
|
|
|
outgoingCallButtonStyle,
|
2022-12-05 22:56:23 +00:00
|
|
|
setDisappearingMessages,
|
2022-05-10 18:14:08 +00:00
|
|
|
showBackButton,
|
|
|
|
} = this.props;
|
2021-06-01 20:45:43 +00:00
|
|
|
const { isNarrow, modalState } = this.state;
|
2019-01-14 21:49:58 +00:00
|
|
|
const triggerId = `conversation-${id}`;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-06-01 20:45:43 +00:00
|
|
|
let modalNode: ReactNode;
|
|
|
|
if (modalState === ModalState.NothingOpen) {
|
|
|
|
modalNode = undefined;
|
|
|
|
} else if (modalState === ModalState.CustomDisappearingTimeout) {
|
|
|
|
modalNode = (
|
|
|
|
<DisappearingTimeDialog
|
|
|
|
i18n={i18n}
|
|
|
|
initialValue={expireTimer}
|
|
|
|
onSubmit={value => {
|
|
|
|
this.setState({ modalState: ModalState.NothingOpen });
|
2022-12-05 22:56:23 +00:00
|
|
|
setDisappearingMessages(id, value);
|
2021-06-01 20:45:43 +00:00
|
|
|
}}
|
|
|
|
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
throw missingCaseError(modalState);
|
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
return (
|
2021-06-01 20:45:43 +00:00
|
|
|
<>
|
|
|
|
{modalNode}
|
2022-12-05 22:56:23 +00:00
|
|
|
{this.renderConfirmationDialog()}
|
2021-06-01 20:45:43 +00:00
|
|
|
<Measure
|
|
|
|
bounds
|
|
|
|
onResize={({ bounds }) => {
|
|
|
|
if (!bounds || !bounds.width) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setState({ isNarrow: bounds.width < 500 });
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{({ measureRef }) => (
|
|
|
|
<div
|
|
|
|
className={classNames('module-ConversationHeader', {
|
|
|
|
'module-ConversationHeader--narrow': isNarrow,
|
|
|
|
})}
|
|
|
|
ref={measureRef}
|
|
|
|
>
|
|
|
|
{this.renderBackButton()}
|
|
|
|
{this.renderHeader()}
|
2022-11-09 02:38:19 +00:00
|
|
|
{!isSMSOnly && !isSignalConversation && (
|
2022-05-10 18:14:08 +00:00
|
|
|
<OutgoingCallButtons
|
|
|
|
announcementsOnly={announcementsOnly}
|
|
|
|
areWeAdmin={areWeAdmin}
|
|
|
|
i18n={i18n}
|
2022-12-06 17:31:44 +00:00
|
|
|
id={id}
|
2022-05-10 18:14:08 +00:00
|
|
|
isNarrow={isNarrow}
|
|
|
|
onOutgoingAudioCallInConversation={
|
|
|
|
onOutgoingAudioCallInConversation
|
|
|
|
}
|
|
|
|
onOutgoingVideoCallInConversation={
|
|
|
|
onOutgoingVideoCallInConversation
|
|
|
|
}
|
|
|
|
outgoingCallButtonStyle={outgoingCallButtonStyle}
|
|
|
|
showBackButton={showBackButton}
|
|
|
|
/>
|
|
|
|
)}
|
2021-06-01 20:45:43 +00:00
|
|
|
{this.renderSearchButton()}
|
|
|
|
{this.renderMoreButton(triggerId)}
|
|
|
|
{this.renderMenu(triggerId)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Measure>
|
|
|
|
</>
|
2018-07-09 21:29:13 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-05-10 18:14:08 +00:00
|
|
|
|
|
|
|
function OutgoingCallButtons({
|
|
|
|
announcementsOnly,
|
|
|
|
areWeAdmin,
|
|
|
|
i18n,
|
2022-12-06 17:31:44 +00:00
|
|
|
id,
|
2022-05-10 18:14:08 +00:00
|
|
|
isNarrow,
|
|
|
|
onOutgoingAudioCallInConversation,
|
|
|
|
onOutgoingVideoCallInConversation,
|
|
|
|
outgoingCallButtonStyle,
|
|
|
|
showBackButton,
|
|
|
|
}: { isNarrow: boolean } & Pick<
|
|
|
|
PropsType,
|
|
|
|
| 'announcementsOnly'
|
|
|
|
| 'areWeAdmin'
|
|
|
|
| 'i18n'
|
2022-12-06 17:31:44 +00:00
|
|
|
| 'id'
|
2022-05-10 18:14:08 +00:00
|
|
|
| 'onOutgoingAudioCallInConversation'
|
|
|
|
| 'onOutgoingVideoCallInConversation'
|
|
|
|
| 'outgoingCallButtonStyle'
|
|
|
|
| 'showBackButton'
|
|
|
|
>): JSX.Element | null {
|
|
|
|
const videoButton = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('makeOutgoingVideoCall')}
|
|
|
|
className={classNames(
|
|
|
|
'module-ConversationHeader__button',
|
|
|
|
'module-ConversationHeader__button--video',
|
|
|
|
showBackButton ? null : 'module-ConversationHeader__button--show',
|
|
|
|
!showBackButton && announcementsOnly && !areWeAdmin
|
|
|
|
? 'module-ConversationHeader__button--show-disabled'
|
|
|
|
: undefined
|
|
|
|
)}
|
|
|
|
disabled={showBackButton}
|
2022-12-06 17:31:44 +00:00
|
|
|
onClick={() => onOutgoingVideoCallInConversation(id)}
|
2022-05-10 18:14:08 +00:00
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
const startCallShortcuts = useStartCallShortcuts(
|
2022-12-06 17:31:44 +00:00
|
|
|
() => onOutgoingAudioCallInConversation(id),
|
|
|
|
() => onOutgoingVideoCallInConversation(id)
|
2022-05-10 18:14:08 +00:00
|
|
|
);
|
|
|
|
useKeyboardShortcuts(startCallShortcuts);
|
|
|
|
|
|
|
|
switch (outgoingCallButtonStyle) {
|
|
|
|
case OutgoingCallButtonStyle.None:
|
|
|
|
return null;
|
|
|
|
case OutgoingCallButtonStyle.JustVideo:
|
|
|
|
return videoButton;
|
|
|
|
case OutgoingCallButtonStyle.Both:
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{videoButton}
|
|
|
|
<button
|
|
|
|
type="button"
|
2022-12-06 17:31:44 +00:00
|
|
|
onClick={() => onOutgoingAudioCallInConversation(id)}
|
2022-05-10 18:14:08 +00:00
|
|
|
className={classNames(
|
|
|
|
'module-ConversationHeader__button',
|
|
|
|
'module-ConversationHeader__button--audio',
|
|
|
|
showBackButton ? null : 'module-ConversationHeader__button--show'
|
|
|
|
)}
|
|
|
|
disabled={showBackButton}
|
|
|
|
aria-label={i18n('makeOutgoingCall')}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
case OutgoingCallButtonStyle.Join:
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('joinOngoingCall')}
|
|
|
|
className={classNames(
|
|
|
|
'module-ConversationHeader__button',
|
|
|
|
'module-ConversationHeader__button--join-call',
|
|
|
|
showBackButton ? null : 'module-ConversationHeader__button--show'
|
|
|
|
)}
|
|
|
|
disabled={showBackButton}
|
2022-12-06 17:31:44 +00:00
|
|
|
onClick={() => onOutgoingVideoCallInConversation(id)}
|
2022-05-10 18:14:08 +00:00
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
{isNarrow ? null : i18n('joinOngoingCall')}
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
default:
|
|
|
|
throw missingCaseError(outgoingCallButtonStyle);
|
|
|
|
}
|
|
|
|
}
|