981 lines
30 KiB
TypeScript
981 lines
30 KiB
TypeScript
// Copyright 2018 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import classNames from 'classnames';
|
|
import type { ReactNode, RefObject } from 'react';
|
|
import React, { memo, useRef, useState } from 'react';
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuTrigger,
|
|
MenuItem,
|
|
SubMenu,
|
|
} from 'react-contextmenu';
|
|
import { createPortal } from 'react-dom';
|
|
import type { BadgeType } from '../../badges/types';
|
|
import {
|
|
useKeyboardShortcuts,
|
|
useStartCallShortcuts,
|
|
} from '../../hooks/useKeyboardShortcuts';
|
|
import { SizeObserver } from '../../hooks/useSizeObserver';
|
|
import type { ConversationTypeType } from '../../state/ducks/conversations';
|
|
import type { HasStories } from '../../types/Stories';
|
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
|
import { DurationInSeconds } from '../../util/durations';
|
|
import * as expirationTimer from '../../util/expirationTimer';
|
|
import { getMuteOptions } from '../../util/getMuteOptions';
|
|
import { isConversationMuted } from '../../util/isConversationMuted';
|
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
|
import { missingCaseError } from '../../util/missingCaseError';
|
|
import { Alert } from '../Alert';
|
|
import { Avatar, AvatarSize } from '../Avatar';
|
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
|
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
|
import { InContactsIcon } from '../InContactsIcon';
|
|
import { UserText } from '../UserText';
|
|
import type { ContactNameData } from './ContactName';
|
|
import {
|
|
MessageRequestActionsConfirmation,
|
|
MessageRequestState,
|
|
} from './MessageRequestActionsConfirmation';
|
|
import type { MinimalConversation } from '../../hooks/useMinimalConversation';
|
|
import { LocalDeleteWarningModal } from '../LocalDeleteWarningModal';
|
|
|
|
function HeaderInfoTitle({
|
|
name,
|
|
title,
|
|
type,
|
|
i18n,
|
|
isMe,
|
|
headerRef,
|
|
}: {
|
|
name: string | null;
|
|
title: string;
|
|
type: ConversationTypeType;
|
|
i18n: LocalizerType;
|
|
isMe: boolean;
|
|
headerRef: React.RefObject<HTMLDivElement>;
|
|
}) {
|
|
if (isMe) {
|
|
return (
|
|
<div className="module-ConversationHeader__header__info__title">
|
|
{i18n('icu:noteToSelf')}
|
|
<span className="ContactModal__official-badge" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="module-ConversationHeader__header__info__title">
|
|
<UserText text={title} />
|
|
{isInSystemContacts({ name: name ?? undefined, type }) ? (
|
|
<InContactsIcon
|
|
className="module-ConversationHeader__header__info__title__in-contacts-icon"
|
|
i18n={i18n}
|
|
tooltipContainerRef={headerRef}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export enum OutgoingCallButtonStyle {
|
|
None,
|
|
JustVideo,
|
|
Both,
|
|
Join,
|
|
}
|
|
|
|
export type PropsDataType = {
|
|
addedByName: ContactNameData | null;
|
|
badge?: BadgeType;
|
|
cannotLeaveBecauseYouAreLastAdmin: boolean;
|
|
conversation: MinimalConversation;
|
|
conversationName: ContactNameData;
|
|
hasPanelShowing?: boolean;
|
|
hasStories?: HasStories;
|
|
localDeleteWarningShown: boolean;
|
|
isDeleteSyncSendEnabled: boolean;
|
|
isMissingMandatoryProfileSharing?: boolean;
|
|
isSelectMode: boolean;
|
|
isSignalConversation?: boolean;
|
|
isSMSOnly?: boolean;
|
|
outgoingCallButtonStyle: OutgoingCallButtonStyle;
|
|
sharedGroupNames: ReadonlyArray<string>;
|
|
theme: ThemeType;
|
|
};
|
|
|
|
export type PropsActionsType = {
|
|
setLocalDeleteWarningShown: () => void;
|
|
|
|
onConversationAccept: () => void;
|
|
onConversationArchive: () => void;
|
|
onConversationBlock: () => void;
|
|
onConversationBlockAndReportSpam: () => void;
|
|
onConversationDelete: () => void;
|
|
onConversationDeleteMessages: () => void;
|
|
onConversationDisappearingMessagesChange: (
|
|
seconds: DurationInSeconds
|
|
) => void;
|
|
onConversationLeaveGroup: () => void;
|
|
onConversationMarkUnread: () => void;
|
|
onConversationMuteExpirationChange: (seconds: number) => void;
|
|
onConversationPin: () => void;
|
|
onConversationUnpin: () => void;
|
|
onConversationReportSpam: () => void;
|
|
onConversationUnarchive: () => void;
|
|
onOutgoingAudioCall: () => void;
|
|
onOutgoingVideoCall: () => void;
|
|
onSearchInConversation: () => void;
|
|
onSelectModeEnter: () => void;
|
|
onShowMembers: () => void;
|
|
onViewConversationDetails: () => void;
|
|
onViewRecentMedia: () => void;
|
|
onViewUserStories: () => void;
|
|
};
|
|
|
|
export type PropsHousekeepingType = {
|
|
i18n: LocalizerType;
|
|
};
|
|
|
|
export type PropsType = PropsDataType &
|
|
PropsActionsType &
|
|
PropsHousekeepingType;
|
|
|
|
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
|
|
|
export const ConversationHeader = memo(function ConversationHeader({
|
|
addedByName,
|
|
badge,
|
|
cannotLeaveBecauseYouAreLastAdmin,
|
|
conversation,
|
|
conversationName,
|
|
hasPanelShowing,
|
|
hasStories,
|
|
i18n,
|
|
isDeleteSyncSendEnabled,
|
|
isMissingMandatoryProfileSharing,
|
|
isSelectMode,
|
|
isSignalConversation,
|
|
isSMSOnly,
|
|
localDeleteWarningShown,
|
|
onConversationAccept,
|
|
onConversationArchive,
|
|
onConversationBlock,
|
|
onConversationBlockAndReportSpam,
|
|
onConversationDelete,
|
|
onConversationDeleteMessages,
|
|
onConversationDisappearingMessagesChange,
|
|
onConversationLeaveGroup,
|
|
onConversationMarkUnread,
|
|
onConversationMuteExpirationChange,
|
|
onConversationPin,
|
|
onConversationReportSpam,
|
|
onConversationUnarchive,
|
|
onConversationUnpin,
|
|
onOutgoingAudioCall,
|
|
onOutgoingVideoCall,
|
|
onSearchInConversation,
|
|
onSelectModeEnter,
|
|
onShowMembers,
|
|
onViewConversationDetails,
|
|
onViewRecentMedia,
|
|
onViewUserStories,
|
|
outgoingCallButtonStyle,
|
|
setLocalDeleteWarningShown,
|
|
sharedGroupNames,
|
|
theme,
|
|
}: PropsType): JSX.Element | null {
|
|
// Comes from a third-party dependency
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const menuTriggerRef = useRef<any>(null);
|
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [
|
|
hasCustomDisappearingTimeoutModal,
|
|
setHasCustomDisappearingTimeoutModal,
|
|
] = useState(false);
|
|
const [hasDeleteMessagesConfirmation, setHasDeleteMessagesConfirmation] =
|
|
useState(false);
|
|
const [hasLeaveGroupConfirmation, setHasLeaveGroupConfirmation] =
|
|
useState(false);
|
|
const [
|
|
hasCannotLeaveGroupBecauseYouAreLastAdminAlert,
|
|
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert,
|
|
] = useState(false);
|
|
const [isNarrow, setIsNarrow] = useState(false);
|
|
const [messageRequestState, setMessageRequestState] = useState(
|
|
MessageRequestState.default
|
|
);
|
|
|
|
const triggerId = `conversation-${conversation.id}`;
|
|
|
|
if (hasPanelShowing) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{hasCustomDisappearingTimeoutModal && (
|
|
<DisappearingTimeDialog
|
|
i18n={i18n}
|
|
initialValue={conversation.expireTimer}
|
|
onSubmit={value => {
|
|
setHasCustomDisappearingTimeoutModal(false);
|
|
onConversationDisappearingMessagesChange(value);
|
|
}}
|
|
onClose={() => {
|
|
setHasCustomDisappearingTimeoutModal(false);
|
|
}}
|
|
/>
|
|
)}
|
|
{hasDeleteMessagesConfirmation && (
|
|
<DeleteMessagesConfirmationDialog
|
|
i18n={i18n}
|
|
isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
|
|
localDeleteWarningShown={localDeleteWarningShown}
|
|
onDestroyMessages={() => {
|
|
setHasDeleteMessagesConfirmation(false);
|
|
onConversationDeleteMessages();
|
|
}}
|
|
onClose={() => {
|
|
setHasDeleteMessagesConfirmation(false);
|
|
}}
|
|
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
|
|
/>
|
|
)}
|
|
{hasLeaveGroupConfirmation && (
|
|
<LeaveGroupConfirmationDialog
|
|
i18n={i18n}
|
|
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
|
onClose={() => {
|
|
setHasLeaveGroupConfirmation(false);
|
|
}}
|
|
onLeaveGroup={() => {
|
|
setHasLeaveGroupConfirmation(false);
|
|
if (!cannotLeaveBecauseYouAreLastAdmin) {
|
|
onConversationLeaveGroup();
|
|
} else {
|
|
setHasLeaveGroupConfirmation(false);
|
|
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert(true);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
{hasCannotLeaveGroupBecauseYouAreLastAdminAlert && (
|
|
<CannotLeaveGroupBecauseYouAreLastAdminAlert
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert(false);
|
|
}}
|
|
/>
|
|
)}
|
|
<SizeObserver
|
|
onSizeChange={size => {
|
|
setIsNarrow(size.width < 500);
|
|
}}
|
|
>
|
|
{measureRef => (
|
|
<div
|
|
className={classNames('module-ConversationHeader', {
|
|
'module-ConversationHeader--narrow': isNarrow,
|
|
})}
|
|
ref={measureRef}
|
|
>
|
|
<HeaderContent
|
|
conversation={conversation}
|
|
badge={badge ?? null}
|
|
hasStories={hasStories ?? null}
|
|
headerRef={headerRef}
|
|
i18n={i18n}
|
|
sharedGroupNames={sharedGroupNames}
|
|
theme={theme}
|
|
onViewUserStories={onViewUserStories}
|
|
onViewConversationDetails={onViewConversationDetails}
|
|
/>
|
|
{!isSMSOnly && !isSignalConversation && (
|
|
<OutgoingCallButtons
|
|
conversation={conversation}
|
|
i18n={i18n}
|
|
isNarrow={isNarrow}
|
|
onOutgoingAudioCall={onOutgoingAudioCall}
|
|
onOutgoingVideoCall={onOutgoingVideoCall}
|
|
outgoingCallButtonStyle={outgoingCallButtonStyle}
|
|
/>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={onSearchInConversation}
|
|
className={classNames(
|
|
'module-ConversationHeader__button',
|
|
'module-ConversationHeader__button--search'
|
|
)}
|
|
aria-label={i18n('icu:search')}
|
|
/>
|
|
<ContextMenuTrigger
|
|
id={triggerId}
|
|
ref={menuTriggerRef}
|
|
disable={isSelectMode}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
menuTriggerRef.current?.handleContextClick(event);
|
|
}}
|
|
className={classNames(
|
|
'module-ConversationHeader__button',
|
|
'module-ConversationHeader__button--more'
|
|
)}
|
|
aria-label={i18n('icu:moreInfo')}
|
|
disabled={isSelectMode}
|
|
/>
|
|
</ContextMenuTrigger>
|
|
<HeaderMenu
|
|
i18n={i18n}
|
|
conversation={conversation}
|
|
isMissingMandatoryProfileSharing={
|
|
isMissingMandatoryProfileSharing ?? false
|
|
}
|
|
isSelectMode={isSelectMode}
|
|
isSignalConversation={isSignalConversation ?? false}
|
|
onChangeDisappearingMessages={
|
|
onConversationDisappearingMessagesChange
|
|
}
|
|
onChangeMuteExpiration={onConversationMuteExpirationChange}
|
|
onConversationAccept={onConversationAccept}
|
|
onConversationArchive={onConversationArchive}
|
|
onConversationBlock={() => {
|
|
setMessageRequestState(MessageRequestState.blocking);
|
|
}}
|
|
onConversationDelete={() => {
|
|
setMessageRequestState(MessageRequestState.deleting);
|
|
}}
|
|
onConversationDeleteMessages={() => {
|
|
setHasDeleteMessagesConfirmation(true);
|
|
}}
|
|
onConversationLeaveGroup={() => {
|
|
if (cannotLeaveBecauseYouAreLastAdmin) {
|
|
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert(true);
|
|
} else {
|
|
setHasLeaveGroupConfirmation(true);
|
|
}
|
|
}}
|
|
onConversationMarkUnread={onConversationMarkUnread}
|
|
onConversationPin={onConversationPin}
|
|
onConversationReportAndMaybeBlock={() => {
|
|
setMessageRequestState(
|
|
MessageRequestState.reportingAndMaybeBlocking
|
|
);
|
|
}}
|
|
onConversationUnarchive={onConversationUnarchive}
|
|
onConversationUnblock={() => {
|
|
setMessageRequestState(MessageRequestState.unblocking);
|
|
}}
|
|
onConversationUnpin={onConversationUnpin}
|
|
onSelectModeEnter={onSelectModeEnter}
|
|
onSetupCustomDisappearingTimeout={() => {
|
|
setHasCustomDisappearingTimeoutModal(true);
|
|
}}
|
|
onShowMembers={onShowMembers}
|
|
onViewRecentMedia={onViewRecentMedia}
|
|
onViewConversationDetails={onViewConversationDetails}
|
|
triggerId={triggerId}
|
|
/>
|
|
<MessageRequestActionsConfirmation
|
|
i18n={i18n}
|
|
conversationId={conversation.id}
|
|
conversationType={conversation.type}
|
|
addedByName={addedByName}
|
|
conversationName={conversationName}
|
|
isBlocked={conversation.isBlocked ?? false}
|
|
isReported={conversation.isReported ?? false}
|
|
state={messageRequestState}
|
|
acceptConversation={onConversationAccept}
|
|
blockAndReportSpam={onConversationBlockAndReportSpam}
|
|
blockConversation={onConversationBlock}
|
|
reportSpam={onConversationReportSpam}
|
|
deleteConversation={onConversationDelete}
|
|
onChangeState={setMessageRequestState}
|
|
/>
|
|
</div>
|
|
)}
|
|
</SizeObserver>
|
|
</>
|
|
);
|
|
});
|
|
|
|
function HeaderContent({
|
|
conversation,
|
|
badge,
|
|
hasStories,
|
|
headerRef,
|
|
i18n,
|
|
sharedGroupNames,
|
|
theme,
|
|
onViewUserStories,
|
|
onViewConversationDetails,
|
|
}: {
|
|
conversation: MinimalConversation;
|
|
badge: BadgeType | null;
|
|
hasStories: HasStories | null;
|
|
headerRef: RefObject<HTMLDivElement>;
|
|
i18n: LocalizerType;
|
|
sharedGroupNames: ReadonlyArray<string>;
|
|
theme: ThemeType;
|
|
onViewUserStories: () => void;
|
|
onViewConversationDetails: () => void;
|
|
}) {
|
|
let onClick: undefined | (() => void);
|
|
const { type } = conversation;
|
|
switch (type) {
|
|
case 'direct':
|
|
onClick = onViewConversationDetails;
|
|
break;
|
|
case 'group': {
|
|
const hasGV2AdminEnabled = conversation.groupVersion === 2;
|
|
onClick = hasGV2AdminEnabled ? onViewConversationDetails : undefined;
|
|
break;
|
|
}
|
|
default:
|
|
throw missingCaseError(type);
|
|
}
|
|
|
|
const avatar = (
|
|
<span className="module-ConversationHeader__header__avatar">
|
|
<Avatar
|
|
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
|
avatarPath={conversation.avatarPath ?? undefined}
|
|
badge={badge ?? undefined}
|
|
color={conversation.color ?? undefined}
|
|
conversationType={conversation.type}
|
|
i18n={i18n}
|
|
isMe={conversation.isMe}
|
|
noteToSelf={conversation.isMe}
|
|
onClick={hasStories ? onViewUserStories : onClick}
|
|
phoneNumber={conversation.phoneNumber ?? undefined}
|
|
profileName={conversation.profileName ?? undefined}
|
|
sharedGroupNames={sharedGroupNames}
|
|
size={AvatarSize.THIRTY_TWO}
|
|
// user may have stories, but we don't show that on Note to Self conversation
|
|
storyRing={conversation.isMe ? undefined : hasStories ?? undefined}
|
|
theme={theme}
|
|
title={conversation.title}
|
|
unblurredAvatarPath={conversation.unblurredAvatarPath ?? undefined}
|
|
/>
|
|
</span>
|
|
);
|
|
|
|
const contents = (
|
|
<div className="module-ConversationHeader__header__info">
|
|
<HeaderInfoTitle
|
|
name={conversation.name ?? null}
|
|
title={conversation.title}
|
|
type={conversation.type}
|
|
i18n={i18n}
|
|
isMe={conversation.isMe}
|
|
headerRef={headerRef}
|
|
/>
|
|
{(conversation.expireTimer != null || conversation.isVerified) && (
|
|
<div className="module-ConversationHeader__header__info__subtitle">
|
|
{conversation.expireTimer != null &&
|
|
conversation.expireTimer !== 0 && (
|
|
<div className="module-ConversationHeader__header__info__subtitle__expiration">
|
|
{expirationTimer.format(i18n, conversation.expireTimer)}
|
|
</div>
|
|
)}
|
|
{conversation.isVerified && (
|
|
<div className="module-ConversationHeader__header__info__subtitle__verified">
|
|
{i18n('icu:verified')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
if (onClick) {
|
|
return (
|
|
<div className="module-ConversationHeader__header">
|
|
{avatar}
|
|
<div>
|
|
<button
|
|
type="button"
|
|
className="module-ConversationHeader__header--clickable"
|
|
onClick={onClick}
|
|
>
|
|
{contents}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="module-ConversationHeader__header" ref={headerRef}>
|
|
{avatar}
|
|
{contents}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HeaderMenu({
|
|
conversation,
|
|
i18n,
|
|
isMissingMandatoryProfileSharing,
|
|
isSelectMode,
|
|
isSignalConversation,
|
|
onChangeDisappearingMessages,
|
|
onChangeMuteExpiration,
|
|
onConversationAccept,
|
|
onConversationArchive,
|
|
onConversationBlock,
|
|
onConversationDelete,
|
|
onConversationDeleteMessages,
|
|
onConversationLeaveGroup,
|
|
onConversationMarkUnread,
|
|
onConversationPin,
|
|
onConversationReportAndMaybeBlock,
|
|
onConversationUnarchive,
|
|
onConversationUnblock,
|
|
onConversationUnpin,
|
|
onSelectModeEnter,
|
|
onSetupCustomDisappearingTimeout,
|
|
onShowMembers,
|
|
onViewRecentMedia,
|
|
onViewConversationDetails,
|
|
triggerId,
|
|
}: {
|
|
conversation: MinimalConversation;
|
|
i18n: LocalizerType;
|
|
isMissingMandatoryProfileSharing: boolean;
|
|
isSelectMode: boolean;
|
|
isSignalConversation: boolean;
|
|
onChangeDisappearingMessages: (seconds: DurationInSeconds) => void;
|
|
onChangeMuteExpiration: (seconds: number) => void;
|
|
onConversationAccept: () => void;
|
|
onConversationArchive: () => void;
|
|
onConversationBlock: () => void;
|
|
onConversationDelete: () => void;
|
|
onConversationDeleteMessages: () => void;
|
|
onConversationLeaveGroup: () => void;
|
|
onConversationMarkUnread: () => void;
|
|
onConversationPin: () => void;
|
|
onConversationReportAndMaybeBlock: () => void;
|
|
onConversationUnarchive: () => void;
|
|
onConversationUnblock: () => void;
|
|
onConversationUnpin: () => void;
|
|
onSelectModeEnter: () => void;
|
|
onSetupCustomDisappearingTimeout: () => void;
|
|
onShowMembers: () => void;
|
|
onViewRecentMedia: () => void;
|
|
onViewConversationDetails: () => void;
|
|
triggerId: string;
|
|
}) {
|
|
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
|
const muteOptions = getMuteOptions(conversation.muteExpiresAt, i18n);
|
|
const isGroup = conversation.type === 'group';
|
|
const disableTimerChanges = Boolean(
|
|
!conversation.canChangeTimer ||
|
|
!conversation.acceptedMessageRequest ||
|
|
conversation.left ||
|
|
isMissingMandatoryProfileSharing
|
|
);
|
|
const hasGV2AdminEnabled = isGroup && conversation.groupVersion === 2;
|
|
|
|
const isActiveExpireTimer = (value: number): boolean => {
|
|
if (!conversation.expireTimer) {
|
|
return value === 0;
|
|
}
|
|
|
|
// Custom time...
|
|
if (value === -1) {
|
|
return !expirationTimer.DEFAULT_DURATIONS_SET.has(
|
|
conversation.expireTimer
|
|
);
|
|
}
|
|
return value === conversation.expireTimer;
|
|
};
|
|
|
|
if (isSelectMode) {
|
|
return null;
|
|
}
|
|
|
|
const muteTitle = <span>{i18n('icu:muteNotificationsTitle')}</span>;
|
|
const disappearingTitle = <span>{i18n('icu:disappearingMessages')}</span>;
|
|
|
|
if (isSignalConversation) {
|
|
const isMuted =
|
|
conversation.muteExpiresAt && isConversationMuted(conversation);
|
|
|
|
return (
|
|
<ContextMenu id={triggerId} rtl={isRTL}>
|
|
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
|
{isMuted ? (
|
|
<MenuItem
|
|
onClick={() => {
|
|
onChangeMuteExpiration(0);
|
|
}}
|
|
>
|
|
{i18n('icu:unmute')}
|
|
</MenuItem>
|
|
) : (
|
|
<MenuItem
|
|
onClick={() => {
|
|
onChangeMuteExpiration(Number.MAX_SAFE_INTEGER);
|
|
}}
|
|
>
|
|
{i18n('icu:muteAlways')}
|
|
</MenuItem>
|
|
)}
|
|
</SubMenu>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
|
|
if (isGroup && conversation.groupVersion !== 2) {
|
|
return (
|
|
<ContextMenu id={triggerId}>
|
|
<MenuItem onClick={onShowMembers}>{i18n('icu:showMembers')}</MenuItem>
|
|
<MenuItem onClick={onViewRecentMedia}>
|
|
{i18n('icu:viewRecentMedia')}
|
|
</MenuItem>
|
|
<MenuItem divider />
|
|
{conversation.isArchived ? (
|
|
<MenuItem onClick={onConversationUnarchive}>
|
|
{i18n('icu:moveConversationToInbox')}
|
|
</MenuItem>
|
|
) : (
|
|
<MenuItem onClick={onConversationArchive}>
|
|
{i18n('icu:archiveConversation')}
|
|
</MenuItem>
|
|
)}
|
|
|
|
<MenuItem onClick={onConversationDeleteMessages}>
|
|
{i18n('icu:deleteMessagesInConversation')}
|
|
</MenuItem>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
|
|
const expireDurations: ReadonlyArray<ReactNode> = [
|
|
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
|
|
DurationInSeconds.fromSeconds(-1),
|
|
].map(seconds => {
|
|
let text: string;
|
|
|
|
if (seconds === -1) {
|
|
text = i18n('icu:customDisappearingTimeOption');
|
|
} else {
|
|
text = expirationTimer.format(i18n, seconds, {
|
|
capitalizeOff: true,
|
|
});
|
|
}
|
|
|
|
const onDurationClick = () => {
|
|
if (seconds === -1) {
|
|
onSetupCustomDisappearingTimeout();
|
|
} else {
|
|
onChangeDisappearingMessages(seconds);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<MenuItem key={seconds} onClick={onDurationClick}>
|
|
<div
|
|
className={classNames(
|
|
TIMER_ITEM_CLASS,
|
|
isActiveExpireTimer(seconds) && `${TIMER_ITEM_CLASS}--active`
|
|
)}
|
|
>
|
|
{text}
|
|
</div>
|
|
</MenuItem>
|
|
);
|
|
});
|
|
|
|
return createPortal(
|
|
<ContextMenu id={triggerId} rtl={isRTL}>
|
|
{!conversation.acceptedMessageRequest && (
|
|
<>
|
|
{!conversation.isBlocked && (
|
|
<MenuItem onClick={onConversationBlock}>
|
|
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
|
</MenuItem>
|
|
)}
|
|
{conversation.isBlocked && (
|
|
<MenuItem onClick={onConversationUnblock}>
|
|
{i18n('icu:ConversationHeader__MenuItem--Unblock')}
|
|
</MenuItem>
|
|
)}
|
|
{!conversation.isBlocked && (
|
|
<MenuItem onClick={onConversationAccept}>
|
|
{i18n('icu:ConversationHeader__MenuItem--Accept')}
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem onClick={onConversationReportAndMaybeBlock}>
|
|
{i18n('icu:ConversationHeader__MenuItem--ReportSpam')}
|
|
</MenuItem>
|
|
<MenuItem onClick={onConversationDelete}>
|
|
{i18n('icu:ConversationHeader__MenuItem--DeleteChat')}
|
|
</MenuItem>
|
|
</>
|
|
)}
|
|
{conversation.acceptedMessageRequest && (
|
|
<>
|
|
{disableTimerChanges ? null : (
|
|
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
|
{expireDurations}
|
|
</SubMenu>
|
|
)}
|
|
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
|
{muteOptions.map(item => (
|
|
<MenuItem
|
|
key={item.name}
|
|
disabled={item.disabled}
|
|
onClick={() => {
|
|
onChangeMuteExpiration(item.value);
|
|
}}
|
|
>
|
|
{item.name}
|
|
</MenuItem>
|
|
))}
|
|
</SubMenu>
|
|
{!isGroup || hasGV2AdminEnabled ? (
|
|
<MenuItem onClick={onViewConversationDetails}>
|
|
{isGroup
|
|
? i18n('icu:showConversationDetails')
|
|
: i18n('icu:showConversationDetails--direct')}
|
|
</MenuItem>
|
|
) : null}
|
|
<MenuItem onClick={onViewRecentMedia}>
|
|
{i18n('icu:viewRecentMedia')}
|
|
</MenuItem>
|
|
<MenuItem divider />
|
|
<MenuItem onClick={onSelectModeEnter}>
|
|
{i18n('icu:ConversationHeader__menu__selectMessages')}
|
|
</MenuItem>
|
|
<MenuItem divider />
|
|
{!conversation.markedUnread ? (
|
|
<MenuItem onClick={onConversationMarkUnread}>
|
|
{i18n('icu:markUnread')}
|
|
</MenuItem>
|
|
) : null}
|
|
{conversation.isPinned ? (
|
|
<MenuItem onClick={onConversationUnpin}>
|
|
{i18n('icu:unpinConversation')}
|
|
</MenuItem>
|
|
) : (
|
|
<MenuItem onClick={onConversationPin}>
|
|
{i18n('icu:pinConversation')}
|
|
</MenuItem>
|
|
)}
|
|
{conversation.isArchived ? (
|
|
<MenuItem onClick={onConversationUnarchive}>
|
|
{i18n('icu:moveConversationToInbox')}
|
|
</MenuItem>
|
|
) : (
|
|
<MenuItem onClick={onConversationArchive}>
|
|
{i18n('icu:archiveConversation')}
|
|
</MenuItem>
|
|
)}
|
|
{!conversation.isBlocked && (
|
|
<MenuItem onClick={onConversationBlock}>
|
|
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
|
</MenuItem>
|
|
)}
|
|
{conversation.isBlocked && (
|
|
<MenuItem onClick={onConversationUnblock}>
|
|
{i18n('icu:ConversationHeader__MenuItem--Unblock')}
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem onClick={onConversationDeleteMessages}>
|
|
{i18n('icu:deleteMessagesInConversation')}
|
|
</MenuItem>
|
|
{isGroup && (
|
|
<MenuItem onClick={onConversationLeaveGroup}>
|
|
{i18n(
|
|
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
|
)}
|
|
</MenuItem>
|
|
)}
|
|
</>
|
|
)}
|
|
</ContextMenu>,
|
|
document.body
|
|
);
|
|
}
|
|
|
|
function OutgoingCallButtons({
|
|
conversation,
|
|
i18n,
|
|
isNarrow,
|
|
onOutgoingAudioCall,
|
|
onOutgoingVideoCall,
|
|
outgoingCallButtonStyle,
|
|
}: { isNarrow: boolean } & Pick<
|
|
PropsType,
|
|
| 'i18n'
|
|
| 'conversation'
|
|
| 'onOutgoingAudioCall'
|
|
| 'onOutgoingVideoCall'
|
|
| 'outgoingCallButtonStyle'
|
|
>): JSX.Element | null {
|
|
const videoButton = (
|
|
<button
|
|
aria-label={i18n('icu:makeOutgoingVideoCall')}
|
|
className={classNames(
|
|
'module-ConversationHeader__button',
|
|
'module-ConversationHeader__button--video',
|
|
conversation.announcementsOnly && !conversation.areWeAdmin
|
|
? 'module-ConversationHeader__button--show-disabled'
|
|
: undefined
|
|
)}
|
|
onClick={onOutgoingVideoCall}
|
|
type="button"
|
|
/>
|
|
);
|
|
|
|
const startCallShortcuts = useStartCallShortcuts(
|
|
onOutgoingAudioCall,
|
|
onOutgoingVideoCall
|
|
);
|
|
useKeyboardShortcuts(startCallShortcuts);
|
|
|
|
switch (outgoingCallButtonStyle) {
|
|
case OutgoingCallButtonStyle.None:
|
|
return null;
|
|
case OutgoingCallButtonStyle.JustVideo:
|
|
return videoButton;
|
|
case OutgoingCallButtonStyle.Both:
|
|
return (
|
|
<>
|
|
{videoButton}
|
|
<button
|
|
type="button"
|
|
onClick={onOutgoingAudioCall}
|
|
className={classNames(
|
|
'module-ConversationHeader__button',
|
|
'module-ConversationHeader__button--audio'
|
|
)}
|
|
aria-label={i18n('icu:makeOutgoingCall')}
|
|
/>
|
|
</>
|
|
);
|
|
case OutgoingCallButtonStyle.Join:
|
|
return (
|
|
<button
|
|
aria-label={i18n('icu:joinOngoingCall')}
|
|
className={classNames(
|
|
'module-ConversationHeader__button',
|
|
'module-ConversationHeader__button--join-call',
|
|
conversation.announcementsOnly && !conversation.areWeAdmin
|
|
? 'module-ConversationHeader__button--show-disabled'
|
|
: undefined
|
|
)}
|
|
onClick={onOutgoingVideoCall}
|
|
type="button"
|
|
>
|
|
{isNarrow ? null : i18n('icu:joinOngoingCall')}
|
|
</button>
|
|
);
|
|
default:
|
|
throw missingCaseError(outgoingCallButtonStyle);
|
|
}
|
|
}
|
|
|
|
function LeaveGroupConfirmationDialog({
|
|
cannotLeaveBecauseYouAreLastAdmin,
|
|
i18n,
|
|
onLeaveGroup,
|
|
onClose,
|
|
}: {
|
|
cannotLeaveBecauseYouAreLastAdmin: boolean;
|
|
i18n: LocalizerType;
|
|
onLeaveGroup: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<ConfirmationDialog
|
|
dialogName="ConversationHeader.leaveGroup"
|
|
title={i18n('icu:ConversationHeader__LeaveGroupConfirmation__title')}
|
|
actions={[
|
|
{
|
|
disabled: cannotLeaveBecauseYouAreLastAdmin,
|
|
action: onLeaveGroup,
|
|
style: 'negative',
|
|
text: i18n(
|
|
'icu:ConversationHeader__LeaveGroupConfirmation__confirmButton'
|
|
),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={onClose}
|
|
>
|
|
{i18n('icu:ConversationHeader__LeaveGroupConfirmation__description')}
|
|
</ConfirmationDialog>
|
|
);
|
|
}
|
|
|
|
function CannotLeaveGroupBecauseYouAreLastAdminAlert({
|
|
i18n,
|
|
onClose,
|
|
}: {
|
|
i18n: LocalizerType;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<Alert
|
|
i18n={i18n}
|
|
body={i18n(
|
|
'icu:ConversationHeader__CannotLeaveGroupBecauseYouAreLastAdminAlert__description'
|
|
)}
|
|
onClose={onClose}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DeleteMessagesConfirmationDialog({
|
|
isDeleteSyncSendEnabled,
|
|
i18n,
|
|
localDeleteWarningShown,
|
|
onDestroyMessages,
|
|
onClose,
|
|
setLocalDeleteWarningShown,
|
|
}: {
|
|
isDeleteSyncSendEnabled: boolean;
|
|
i18n: LocalizerType;
|
|
localDeleteWarningShown: boolean;
|
|
onDestroyMessages: () => void;
|
|
onClose: () => void;
|
|
setLocalDeleteWarningShown: () => void;
|
|
}) {
|
|
if (!localDeleteWarningShown && isDeleteSyncSendEnabled) {
|
|
return (
|
|
<LocalDeleteWarningModal
|
|
i18n={i18n}
|
|
onClose={setLocalDeleteWarningShown}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ConfirmationDialog
|
|
dialogName="ConversationHeader.destroyMessages"
|
|
title={i18n(
|
|
'icu:ConversationHeader__DeleteMessagesInConversationConfirmation__title'
|
|
)}
|
|
actions={[
|
|
{
|
|
action: onDestroyMessages,
|
|
style: 'negative',
|
|
text: i18n('icu:delete'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={onClose}
|
|
>
|
|
{i18n(
|
|
'icu:ConversationHeader__DeleteMessagesInConversationConfirmation__description'
|
|
)}
|
|
</ConfirmationDialog>
|
|
);
|
|
}
|