// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode } from 'react'; import React from 'react'; import Measure from 'react-measure'; import classNames from 'classnames'; import { ContextMenu, ContextMenuTrigger, MenuItem, SubMenu, } from 'react-contextmenu'; import { Emojify } from './Emojify'; import { DisappearingTimeDialog } from '../DisappearingTimeDialog'; import { Avatar, AvatarSize } from '../Avatar'; import { InContactsIcon } from '../InContactsIcon'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import type { BadgeType } from '../../badges/types'; import { getMuteOptions } from '../../util/getMuteOptions'; import * as expirationTimer from '../../util/expirationTimer'; import { missingCaseError } from '../../util/missingCaseError'; import { isInSystemContacts } from '../../util/isInSystemContacts'; export enum OutgoingCallButtonStyle { None, JustVideo, Both, Join, } export type PropsDataType = { badge?: BadgeType; conversationTitle?: string; isMissingMandatoryProfileSharing?: boolean; outgoingCallButtonStyle: OutgoingCallButtonStyle; showBackButton?: boolean; isSMSOnly?: boolean; theme: ThemeType; } & Pick< ConversationType, | 'acceptedMessageRequest' | 'announcementsOnly' | 'areWeAdmin' | 'avatarPath' | 'canChangeTimer' | 'color' | 'expireTimer' | 'groupVersion' | 'id' | 'isArchived' | 'isMe' | 'isPinned' | 'isVerified' | 'left' | 'markedUnread' | 'muteExpiresAt' | 'name' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' | 'type' | 'unblurredAvatarPath' >; export type PropsActionsType = { onSetMuteNotifications: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void; onDeleteMessages: () => void; onSearchInConversation: () => void; onOutgoingAudioCallInConversation: () => void; onOutgoingVideoCallInConversation: () => void; onSetPin: (value: boolean) => void; onShowConversationDetails: () => void; onShowAllMedia: () => void; onShowGroupMembers: () => void; onGoBack: () => void; onArchive: () => void; onMarkUnread: () => void; onMoveToInbox: () => void; }; export type PropsHousekeepingType = { i18n: LocalizerType; }; export type PropsType = PropsDataType & PropsActionsType & PropsHousekeepingType; enum ModalState { NothingOpen, CustomDisappearingTimeout, } type StateType = { isNarrow: boolean; modalState: ModalState; }; const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item'; 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 headerRef: React.RefObject; public constructor(props: PropsType) { super(props); this.state = { isNarrow: false, modalState: ModalState.NothingOpen }; this.menuTriggerRef = React.createRef(); this.headerRef = 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 { acceptedMessageRequest, canChangeTimer, expireTimer, groupVersion, i18n, isArchived, isMissingMandatoryProfileSharing, isPinned, left, markedUnread, muteExpiresAt, onArchive, onDeleteMessages, onMarkUnread, onMoveToInbox, onSetDisappearingMessages, onSetMuteNotifications, onSetPin, onShowAllMedia, onShowConversationDetails, onShowGroupMembers, type, } = this.props; const muteOptions = getMuteOptions(muteExpiresAt, 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; 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 = [ ...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS, -1, ].map((seconds: number) => { 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 { onSetDisappearingMessages(seconds); } }; return (
{text}
); }); return ( {disableTimerChanges ? null : ( {expireDurations} )} {muteOptions.map(item => ( { onSetMuteNotifications(item.value); }} > {item.name} ))} {!isGroup || hasGV2AdminEnabled ? ( {isGroup ? i18n('showConversationDetails') : i18n('showConversationDetails--direct')} ) : null} {isGroup && !hasGV2AdminEnabled ? ( {i18n('showMembers')} ) : null} {i18n('viewRecentMedia')} {!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, onShowConversationDetails, type, } = this.props; if (conversationTitle !== undefined) { return (
{conversationTitle}
); } let onClick: undefined | (() => void); switch (type) { case 'direct': onClick = () => { onShowConversationDetails(); }; 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, isSMSOnly, i18n, onSetDisappearingMessages, expireTimer, } = this.props; const { isNarrow, modalState } = this.state; const triggerId = `conversation-${id}`; let modalNode: ReactNode; if (modalState === ModalState.NothingOpen) { modalNode = undefined; } else if (modalState === ModalState.CustomDisappearingTimeout) { modalNode = ( { this.setState({ modalState: ModalState.NothingOpen }); onSetDisappearingMessages(value); }} onClose={() => this.setState({ modalState: ModalState.NothingOpen })} /> ); } else { throw missingCaseError(modalState); } return ( <> {modalNode} { if (!bounds || !bounds.width) { return; } this.setState({ isNarrow: bounds.width < 500 }); }} > {({ measureRef }) => (
{this.renderBackButton()} {this.renderHeader()} {!isSMSOnly && this.renderOutgoingCallButtons()} {this.renderSearchButton()} {this.renderMoreButton(triggerId)} {this.renderMenu(triggerId)}
)}
); } }