// Copyright 2018 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 { DisappearingTimeDialog } from '../DisappearingTimeDialog'; import { Avatar, AvatarSize } from '../Avatar'; import { InContactsIcon } from '../InContactsIcon'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType, PopPanelForConversationActionType, PushPanelForConversationActionType, } from '../../state/ducks/conversations'; import type { BadgeType } from '../../badges/types'; import type { HasStories } from '../../types/Stories'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import { StoryViewModeType } from '../../types/Stories'; import { getMuteOptions } from '../../util/getMuteOptions'; import * as expirationTimer from '../../util/expirationTimer'; import { missingCaseError } from '../../util/missingCaseError'; import { isInSystemContacts } from '../../util/isInSystemContacts'; import { isConversationMuted } from '../../util/isConversationMuted'; import { ConfirmationDialog } from '../ConfirmationDialog'; import { DurationInSeconds } from '../../util/durations'; import { useStartCallShortcuts, useKeyboardShortcuts, } from '../../hooks/useKeyboardShortcuts'; import { PanelType } from '../../types/Panels'; import { UserText } from '../UserText'; import { Alert } from '../Alert'; export enum OutgoingCallButtonStyle { None, JustVideo, Both, Join, } export type PropsDataType = { badge?: BadgeType; cannotLeaveBecauseYouAreLastAdmin: boolean; conversationTitle?: string; hasStories?: HasStories; isMissingMandatoryProfileSharing?: boolean; outgoingCallButtonStyle: OutgoingCallButtonStyle; showBackButton?: boolean; isSMSOnly?: boolean; isSignalConversation?: 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 = { destroyMessages: (conversationId: string) => void; leaveGroup: (conversationId: string) => void; onArchive: (conversationId: string) => void; onMarkUnread: (conversationId: string) => void; toggleSelectMode: (on: boolean) => void; onMoveToInbox: (conversationId: string) => void; onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void; pushPanelForConversation: PushPanelForConversationActionType; popPanelForConversation: PopPanelForConversationActionType; searchInConversation: (conversationId: string) => void; setDisappearingMessages: ( conversationId: string, seconds: DurationInSeconds ) => void; setMuteExpiration: (conversationId: string, seconds: number) => void; setPinned: (conversationId: string, value: boolean) => void; viewUserStories: ViewUserStoriesActionCreatorType; }; export type PropsHousekeepingType = { i18n: LocalizerType; }; export type PropsType = PropsDataType & PropsActionsType & PropsHousekeepingType; enum ModalState { NothingOpen, CustomDisappearingTimeout, } type StateType = { hasDeleteMessagesConfirmation: boolean; hasLeaveGroupConfirmation: boolean; hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean; 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 = { hasDeleteMessagesConfirmation: false, hasLeaveGroupConfirmation: false, hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false, 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, popPanelForConversation, showBackButton } = this.props; return ( ); } return (
{avatar} {contents}
); } public override render(): ReactNode { const { announcementsOnly, areWeAdmin, expireTimer, i18n, id, isSMSOnly, isSignalConversation, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, outgoingCallButtonStyle, setDisappearingMessages, showBackButton, } = 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 }); setDisappearingMessages(id, value); }} onClose={() => this.setState({ modalState: ModalState.NothingOpen })} /> ); } else { throw missingCaseError(modalState); } return ( <> {modalNode} {this.renderDeleteMessagesConfirmationDialog()} {this.renderLeaveGroupConfirmationDialog()} {this.renderCannotLeaveGroupBecauseYouAreLastAdminAlert()} { if (!bounds || !bounds.width) { return; } this.setState({ isNarrow: bounds.width < 500 }); }} > {({ measureRef }) => (
{this.renderBackButton()} {this.renderHeader()} {!isSMSOnly && !isSignalConversation && ( )} {this.renderSearchButton()} {this.renderMoreButton(triggerId)} {this.renderMenu(triggerId)}
)}
); } } function OutgoingCallButtons({ announcementsOnly, areWeAdmin, i18n, id, isNarrow, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, outgoingCallButtonStyle, showBackButton, }: { isNarrow: boolean } & Pick< PropsType, | 'announcementsOnly' | 'areWeAdmin' | 'i18n' | 'id' | 'onOutgoingAudioCallInConversation' | 'onOutgoingVideoCallInConversation' | 'outgoingCallButtonStyle' | 'showBackButton' >): JSX.Element | null { const videoButton = ( ); default: throw missingCaseError(outgoingCallButtonStyle); } }