// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { ReactNode } from 'react'; import Measure from 'react-measure'; import moment from 'moment'; import classNames from 'classnames'; import { ContextMenu, ContextMenuTrigger, MenuItem, SubMenu, } from 'react-contextmenu'; import { Emojify } from './Emojify'; import { Avatar, AvatarSize } from '../Avatar'; import { InContactsIcon } from '../InContactsIcon'; import { LocalizerType } from '../../types/Util'; import { ColorType } from '../../types/Colors'; import { MuteOption, getMuteOptions } from '../../util/getMuteOptions'; import { ExpirationTimerOptions, TimerOption, } from '../../util/ExpirationTimerOptions'; import { isMuted } from '../../util/isMuted'; import { missingCaseError } from '../../util/missingCaseError'; export enum OutgoingCallButtonStyle { None, JustVideo, Both, Join, } export type PropsDataType = { conversationTitle?: string; id: string; name?: string; phoneNumber?: string; profileName?: string; color?: ColorType; avatarPath?: string; type: 'direct' | 'group'; title: string; acceptedMessageRequest?: boolean; isVerified?: boolean; isMe?: boolean; isArchived?: boolean; isPinned?: boolean; isMissingMandatoryProfileSharing?: boolean; left?: boolean; markedUnread?: boolean; groupVersion?: number; canChangeTimer?: boolean; expireTimer?: number; muteExpiresAt?: number; showBackButton?: boolean; outgoingCallButtonStyle: OutgoingCallButtonStyle; }; export type PropsActionsType = { onSetMuteNotifications: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void; onShowContactModal: (contactId: string) => void; onDeleteMessages: () => void; onResetSession: () => void; onSearchInConversation: () => void; onOutgoingAudioCallInConversation: () => void; onOutgoingVideoCallInConversation: () => void; onSetPin: (value: boolean) => void; onShowConversationDetails: () => void; onShowSafetyNumber: () => void; onShowAllMedia: () => void; onShowGroupMembers: () => void; onGoBack: () => void; onArchive: () => void; onMarkUnread: () => void; onMoveToInbox: () => void; }; export type PropsHousekeepingType = { i18n: LocalizerType; }; export type PropsType = PropsDataType & PropsActionsType & PropsHousekeepingType; type StateType = { isNarrow: boolean; }; export class ConversationHeader extends React.Component { private showMenuBound: (event: React.MouseEvent) => void; // Comes from a third-party dependency // eslint-disable-next-line @typescript-eslint/no-explicit-any private menuTriggerRef: React.RefObject; public constructor(props: PropsType) { super(props); this.state = { isNarrow: false }; this.menuTriggerRef = React.createRef(); this.showMenuBound = this.showMenu.bind(this); } private showMenu(event: React.MouseEvent): void { if (this.menuTriggerRef.current) { this.menuTriggerRef.current.handleContextClick(event); } } private renderBackButton(): ReactNode { const { i18n, onGoBack, showBackButton } = this.props; return ( ); default: throw missingCaseError(outgoingCallButtonStyle); } } private renderMenu(triggerId: string): ReactNode { const { i18n, acceptedMessageRequest, canChangeTimer, isArchived, isMe, isPinned, type, markedUnread, muteExpiresAt, isMissingMandatoryProfileSharing, left, groupVersion, onDeleteMessages, onResetSession, onSetDisappearingMessages, onSetMuteNotifications, onShowAllMedia, onShowConversationDetails, onShowGroupMembers, onShowSafetyNumber, onArchive, onMarkUnread, onSetPin, onMoveToInbox, } = this.props; const muteOptions: Array = []; if (isMuted(muteExpiresAt)) { const expires = moment(muteExpiresAt); let muteExpirationLabel: string; if (Number(muteExpiresAt) >= Number.MAX_SAFE_INTEGER) { muteExpirationLabel = i18n('muteExpirationLabelAlways'); } else { const muteExpirationUntil = moment().isSame(expires, 'day') ? expires.format('hh:mm A') : expires.format('M/D/YY, hh:mm A'); muteExpirationLabel = i18n('muteExpirationLabel', [ muteExpirationUntil, ]); } muteOptions.push( ...[ { name: muteExpirationLabel, disabled: true, value: 0, }, { name: i18n('unmute'), value: 0, }, ] ); } muteOptions.push(...getMuteOptions(i18n)); // eslint-disable-next-line @typescript-eslint/no-explicit-any const disappearingTitle = i18n('disappearingMessages') as any; // eslint-disable-next-line @typescript-eslint/no-explicit-any const muteTitle = i18n('muteNotificationsTitle') as any; const isGroup = type === 'group'; const disableTimerChanges = Boolean( !canChangeTimer || !acceptedMessageRequest || left || isMissingMandatoryProfileSharing ); const hasGV2AdminEnabled = isGroup && groupVersion === 2; return ( {disableTimerChanges ? null : ( {ExpirationTimerOptions.map((item: typeof TimerOption) => ( { onSetDisappearingMessages(item.get('seconds')); }} > {item.getName(i18n)} ))} )} {muteOptions.map(item => ( { onSetMuteNotifications(item.value); }} > {item.name} ))} {hasGV2AdminEnabled ? ( {i18n('showConversationDetails')} ) : null} {isGroup && !hasGV2AdminEnabled ? ( {i18n('showMembers')} ) : null} {i18n('viewRecentMedia')} {!isGroup && !isMe ? ( {i18n('showSafetyNumber')} ) : null} {!isGroup && acceptedMessageRequest ? ( {i18n('resetSession')} ) : null} {!markedUnread ? ( {i18n('markUnread')} ) : null} {isArchived ? ( {i18n('moveConversationToInbox')} ) : ( {i18n('archiveConversation')} )} {i18n('deleteMessages')} {isPinned ? ( onSetPin(false)}> {i18n('unpinConversation')} ) : ( onSetPin(true)}> {i18n('pinConversation')} )} ); } private renderHeader(): ReactNode { const { conversationTitle, groupVersion, id, isMe, onShowContactModal, onShowConversationDetails, type, } = this.props; if (conversationTitle !== undefined) { return (
{conversationTitle}
); } let onClick: undefined | (() => void); switch (type) { case 'direct': onClick = isMe ? undefined : () => { onShowContactModal(id); }; break; case 'group': { const hasGV2AdminEnabled = groupVersion === 2; onClick = hasGV2AdminEnabled ? () => { onShowConversationDetails(); } : undefined; break; } default: throw missingCaseError(type); } const contents = ( <> {this.renderAvatar()}
{this.renderHeaderInfoTitle()} {this.renderHeaderInfoSubtitle()}
); if (onClick) { return ( ); } return
{contents}
; } public render(): ReactNode { const { id } = this.props; const { isNarrow } = this.state; const triggerId = `conversation-${id}`; return ( { if (!bounds || !bounds.width) { return; } this.setState({ isNarrow: bounds.width < 500 }); }} > {({ measureRef }) => (
{this.renderBackButton()} {this.renderHeader()} {this.renderOutgoingCallButtons()} {this.renderSearchButton()} {this.renderMoreButton(triggerId)} {this.renderMenu(triggerId)}
)}
); } }