Add contextMenu for deleting call events on right-click

This commit is contained in:
trevor-signal 2023-12-12 11:11:39 -05:00 committed by GitHub
parent 7fb01f102d
commit 88fd42a46b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 394 additions and 269 deletions

View file

@ -7114,6 +7114,14 @@ button.module-image__border-overlay:focus {
user-select: none; user-select: none;
visibility: hidden; visibility: hidden;
// style a menu with only one option
&:not(:has(:nth-child(2))) {
padding-block: 0;
.react-contextmenu-item {
padding-block: 9px;
border-radius: 4px;
}
}
@include light-theme { @include light-theme {
background-color: $color-white; background-color: $color-white;
} }

View file

@ -58,6 +58,7 @@ const getCommonProps = (options: {
mode === CallMode.Group ? getDefaultGroup() : getDefaultConversation(); mode === CallMode.Group ? getDefaultGroup() : getDefaultConversation();
return { return {
id: 'message-id',
conversationId: conversation.id, conversationId: conversation.id,
i18n, i18n,
isNextItemCallingNotification: false, isNextItemCallingNotification: false,
@ -67,6 +68,7 @@ const getCommonProps = (options: {
onOutgoingVideoCallInConversation: action( onOutgoingVideoCallInConversation: action(
'onOutgoingVideoCallInConversation' 'onOutgoingVideoCallInConversation'
), ),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
returnToActiveCall: action('returnToActiveCall'), returnToActiveCall: action('returnToActiveCall'),
callHistory: { callHistory: {
callId: '123', callId: '123',
@ -83,6 +85,8 @@ const getCommonProps = (options: {
groupCallEnded, groupCallEnded,
maxDevices, maxDevices,
deviceCount, deviceCount,
isSelectMode: false,
isTargeted: false,
}; };
}; };

View file

@ -4,6 +4,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React from 'react'; import React from 'react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { ContextMenuTrigger } from 'react-contextmenu';
import { SystemMessage, SystemMessageKind } from './SystemMessage'; import { SystemMessage, SystemMessageKind } from './SystemMessage';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
@ -24,15 +25,27 @@ import {
DirectCallStatus, DirectCallStatus,
GroupCallStatus, GroupCallStatus,
} from '../../types/CallDisposition'; } from '../../types/CallDisposition';
import {
type ContextMenuTriggerType,
MessageContextMenu,
useHandleMessageContextMenu,
} from './MessageContextMenu';
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
import {
useKeyboardShortcutsConditionally,
useOpenContextMenu,
} from '../../hooks/useKeyboardShortcuts';
export type PropsActionsType = { export type PropsActionsType = {
onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void;
returnToActiveCall: () => void; returnToActiveCall: () => void;
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
}; };
type PropsHousekeeping = { type PropsHousekeeping = {
i18n: LocalizerType; i18n: LocalizerType;
id: string;
conversationId: string; conversationId: string;
isNextItemCallingNotification: boolean; isNextItemCallingNotification: boolean;
}; };
@ -43,36 +56,81 @@ export type PropsType = CallingNotificationType &
export const CallingNotification: React.FC<PropsType> = React.memo( export const CallingNotification: React.FC<PropsType> = React.memo(
function CallingNotificationInner(props) { function CallingNotificationInner(props) {
const menuTriggerRef = React.useRef<ContextMenuTriggerType | null>(null);
const handleContextMenu = useHandleMessageContextMenu(menuTriggerRef);
const openContextMenuKeyboard = useOpenContextMenu(handleContextMenu);
useKeyboardShortcutsConditionally(
!props.isSelectMode && props.isTargeted,
openContextMenuKeyboard
);
const { i18n } = props; const { i18n } = props;
if (props.callHistory == null) { if (props.callHistory == null) {
return null; return null;
} }
const { type, direction, status, timestamp } = props.callHistory; const { type, direction, status, timestamp } = props.callHistory;
const icon = getCallingIcon(type, direction, status); const icon = getCallingIcon(type, direction, status);
return ( return (
<SystemMessage <>
button={renderCallingNotificationButton(props)} <div
contents={ onContextMenu={handleContextMenu}
<> // @ts-expect-error -- React/TS doesn't know about inert
{getCallingNotificationText(props, i18n)} &middot;{' '} // eslint-disable-next-line react/no-unknown-property
<MessageTimestamp inert={props.isSelectMode ? '' : undefined}
direction="outgoing" >
i18n={i18n} <SystemMessage
timestamp={timestamp} button={renderCallingNotificationButton(props)}
withImageNoCaption={false} contents={
withSticker={false} <>
withTapToViewExpired={false} {getCallingNotificationText(props, i18n)} &middot;{' '}
/> <MessageTimestamp
</> direction="outgoing"
} i18n={i18n}
icon={icon} timestamp={timestamp}
kind={ withImageNoCaption={false}
status === DirectCallStatus.Missed || withSticker={false}
status === GroupCallStatus.Missed withTapToViewExpired={false}
? SystemMessageKind.Danger />
: SystemMessageKind.Normal </>
} }
/> icon={icon}
kind={
status === DirectCallStatus.Missed ||
status === GroupCallStatus.Missed
? SystemMessageKind.Danger
: SystemMessageKind.Normal
}
/>
</div>
<ContextMenuTrigger
id={props.id}
ref={ref => {
// react-contextmenu's typings are incorrect here
menuTriggerRef.current = ref as unknown as ContextMenuTriggerType;
}}
/>
<MessageContextMenu
i18n={i18n}
triggerId={props.id}
onDeleteMessage={() => {
props.toggleDeleteMessagesModal({
conversationId: props.conversationId,
messageIds: [props.id],
});
}}
shouldShowAdditional={false}
onDownload={undefined}
onEdit={undefined}
onReplyToMessage={undefined}
onReact={undefined}
onRetryMessageSend={undefined}
onRetryDeleteForEveryone={undefined}
onCopy={undefined}
onSelect={undefined}
onForward={undefined}
onMoreInfo={undefined}
/>
</>
); );
} }
); );

View file

@ -0,0 +1,244 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { type RefObject } from 'react';
import { ContextMenu, MenuItem } from 'react-contextmenu';
import ReactDOM from 'react-dom';
import type { LocalizerType } from '../../types/I18N';
export type ContextMenuTriggerType = {
handleContextClick: (
event: React.MouseEvent<HTMLDivElement> | MouseEvent
) => void;
};
type MessageContextProps = {
i18n: LocalizerType;
triggerId: string;
shouldShowAdditional: boolean;
onDownload: (() => void) | undefined;
onEdit: (() => void) | undefined;
onReplyToMessage: (() => void) | undefined;
onReact: (() => void) | undefined;
onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined;
onCopy: (() => void) | undefined;
onForward: (() => void) | undefined;
onDeleteMessage: () => void;
onMoreInfo: (() => void) | undefined;
onSelect: (() => void) | undefined;
};
export const MessageContextMenu = ({
i18n,
triggerId,
shouldShowAdditional,
onDownload,
onEdit,
onReplyToMessage,
onReact,
onMoreInfo,
onCopy,
onSelect,
onRetryMessageSend,
onRetryDeleteForEveryone,
onForward,
onDeleteMessage,
}: MessageContextProps): JSX.Element => {
const menu = (
<ContextMenu id={triggerId}>
{shouldShowAdditional && (
<>
{onDownload && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__download',
}}
onClick={onDownload}
>
{i18n('icu:MessageContextMenu__download')}
</MenuItem>
)}
{onReplyToMessage && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__reply',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onReplyToMessage();
}}
>
{i18n('icu:MessageContextMenu__reply')}
</MenuItem>
)}
{onReact && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__react',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onReact();
}}
>
{i18n('icu:MessageContextMenu__react')}
</MenuItem>
)}
</>
)}
{onForward && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__forward-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onForward();
}}
>
{i18n('icu:MessageContextMenu__forward')}
</MenuItem>
)}
{onEdit && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__edit-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onEdit();
}}
>
{i18n('icu:edit')}
</MenuItem>
)}
{onSelect && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__select',
}}
onClick={() => {
onSelect();
}}
>
{i18n('icu:MessageContextMenu__select')}
</MenuItem>
)}
{onCopy && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__copy-timestamp',
}}
onClick={() => {
onCopy();
}}
>
{i18n('icu:copy')}
</MenuItem>
)}
{onMoreInfo && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__more-info',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onMoreInfo();
}}
>
{i18n('icu:MessageContextMenu__info')}
</MenuItem>
)}
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__delete-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onDeleteMessage();
}}
>
{i18n('icu:MessageContextMenu__deleteMessage')}
</MenuItem>
{onRetryMessageSend && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__retry-send',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onRetryMessageSend();
}}
>
{i18n('icu:retrySend')}
</MenuItem>
)}
{onRetryDeleteForEveryone && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__delete-message-for-everyone',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onRetryDeleteForEveryone();
}}
>
{i18n('icu:retryDeleteForEveryone')}
</MenuItem>
)}
</ContextMenu>
);
return ReactDOM.createPortal(menu, document.body);
};
export function useHandleMessageContextMenu(
menuTriggerRef: RefObject<ContextMenuTriggerType>
): ContextMenuTriggerType['handleContextClick'] {
return React.useCallback(
(event: React.MouseEvent<HTMLDivElement> | MouseEvent): void => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed) {
return;
}
if (event && event.target instanceof HTMLAnchorElement) {
return;
}
if (menuTriggerRef.current) {
menuTriggerRef.current.handleContextClick(
event ?? new MouseEvent('click')
);
}
},
[menuTriggerRef]
);
}

View file

@ -253,11 +253,13 @@ export const TimelineItem = memo(function TimelineItem({
} else if (item.type === 'callHistory') { } else if (item.type === 'callHistory') {
notification = ( notification = (
<CallingNotification <CallingNotification
id={id}
conversationId={conversationId} conversationId={conversationId}
i18n={i18n} i18n={i18n}
isNextItemCallingNotification={isNextItemCallingNotification} isNextItemCallingNotification={isNextItemCallingNotification}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation} onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation} onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
toggleDeleteMessagesModal={reducedProps.toggleDeleteMessagesModal}
returnToActiveCall={returnToActiveCall} returnToActiveCall={returnToActiveCall}
{...item.data} {...item.data}
/> />

View file

@ -5,8 +5,8 @@ import classNames from 'classnames';
import { noop } from 'lodash'; import { noop } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { Ref } from 'react'; import type { Ref } from 'react';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { ContextMenuTrigger } from 'react-contextmenu';
import ReactDOM, { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow'; import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
import { isDownloaded } from '../../types/Attachment'; import { isDownloaded } from '../../types/Attachment';
@ -34,6 +34,11 @@ import {
import { PanelType } from '../../types/Panels'; import { PanelType } from '../../types/Panels';
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals'; import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
import { useScrollerLock } from '../../hooks/useScrollLock'; import { useScrollerLock } from '../../hooks/useScrollLock';
import {
type ContextMenuTriggerType,
MessageContextMenu,
useHandleMessageContextMenu,
} from './MessageContextMenu';
export type PropsData = { export type PropsData = {
canDownload: boolean; canDownload: boolean;
@ -77,12 +82,6 @@ export type Props = PropsData &
) => JSX.Element; ) => JSX.Element;
}; };
type Trigger = {
handleContextClick: (
event: React.MouseEvent<HTMLDivElement> | MouseEvent
) => void;
};
/** /**
* Message with menu/context-menu (as necessary for rendering in the timeline) * Message with menu/context-menu (as necessary for rendering in the timeline)
*/ */
@ -132,7 +131,7 @@ export function TimelineMessage(props: Props): JSX.Element {
const [reactionPickerRoot, setReactionPickerRoot] = useState< const [reactionPickerRoot, setReactionPickerRoot] = useState<
HTMLDivElement | undefined HTMLDivElement | undefined
>(undefined); >(undefined);
const menuTriggerRef = useRef<Trigger | null>(null); const menuTriggerRef = useRef<ContextMenuTriggerType | null>(null);
const isWindowWidthNotNarrow = const isWindowWidthNotNarrow =
containerWidthBreakpoint !== WidthBreakpoint.Narrow; containerWidthBreakpoint !== WidthBreakpoint.Narrow;
@ -228,22 +227,7 @@ export function TimelineMessage(props: Props): JSX.Element {
[kickOffAttachmentDownload, saveAttachment, attachments, id, timestamp] [kickOffAttachmentDownload, saveAttachment, attachments, id, timestamp]
); );
const handleContextMenu = React.useCallback( const handleContextMenu = useHandleMessageContextMenu(menuTriggerRef);
(event: React.MouseEvent<HTMLDivElement>): void => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed) {
return;
}
if (event.target instanceof HTMLAnchorElement) {
return;
}
if (menuTriggerRef.current) {
menuTriggerRef.current.handleContextClick(event);
}
},
[menuTriggerRef]
);
const canForward = const canForward =
!isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment; !isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment;
@ -280,15 +264,7 @@ export function TimelineMessage(props: Props): JSX.Element {
handleReact || noop handleReact || noop
); );
const handleOpenContextMenu = useCallback(() => { const openContextMenuKeyboard = useOpenContextMenu(handleContextMenu);
if (!menuTriggerRef.current) {
return;
}
const event = new MouseEvent('click');
menuTriggerRef.current.handleContextClick(event);
}, []);
const openContextMenuKeyboard = useOpenContextMenu(handleOpenContextMenu);
useKeyboardShortcutsConditionally( useKeyboardShortcutsConditionally(
Boolean(isTargeted), Boolean(isTargeted),
@ -419,7 +395,7 @@ type MessageMenuProps = {
i18n: LocalizerType; i18n: LocalizerType;
triggerId: string; triggerId: string;
isWindowWidthNotNarrow: boolean; isWindowWidthNotNarrow: boolean;
menuTriggerRef: Ref<Trigger>; menuTriggerRef: Ref<ContextMenuTriggerType>;
showMenu: (event: React.MouseEvent<HTMLDivElement>) => void; showMenu: (event: React.MouseEvent<HTMLDivElement>) => void;
onDownload: (() => void) | undefined; onDownload: (() => void) | undefined;
onReplyToMessage: (() => void) | undefined; onReplyToMessage: (() => void) | undefined;
@ -569,208 +545,3 @@ function MessageMenu({
</div> </div>
); );
} }
type MessageContextProps = {
i18n: LocalizerType;
triggerId: string;
shouldShowAdditional: boolean;
onDownload: (() => void) | undefined;
onEdit: (() => void) | undefined;
onReplyToMessage: (() => void) | undefined;
onReact: (() => void) | undefined;
onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined;
onCopy: (() => void) | undefined;
onForward: (() => void) | undefined;
onDeleteMessage: () => void;
onMoreInfo: () => void;
onSelect: () => void;
};
const MessageContextMenu = ({
i18n,
triggerId,
shouldShowAdditional,
onDownload,
onEdit,
onReplyToMessage,
onReact,
onMoreInfo,
onCopy,
onSelect,
onRetryMessageSend,
onRetryDeleteForEveryone,
onForward,
onDeleteMessage,
}: MessageContextProps): JSX.Element => {
const menu = (
<ContextMenu id={triggerId}>
{shouldShowAdditional && (
<>
{onDownload && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__download',
}}
onClick={onDownload}
>
{i18n('icu:MessageContextMenu__download')}
</MenuItem>
)}
{onReplyToMessage && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__reply',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onReplyToMessage();
}}
>
{i18n('icu:MessageContextMenu__reply')}
</MenuItem>
)}
{onReact && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__react',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onReact();
}}
>
{i18n('icu:MessageContextMenu__react')}
</MenuItem>
)}
</>
)}
{onForward && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__forward-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onForward();
}}
>
{i18n('icu:MessageContextMenu__forward')}
</MenuItem>
)}
{onEdit && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__edit-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onEdit();
}}
>
{i18n('icu:edit')}
</MenuItem>
)}
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__select',
}}
onClick={() => {
onSelect();
}}
>
{i18n('icu:MessageContextMenu__select')}
</MenuItem>
{onCopy && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__copy-timestamp',
}}
onClick={() => {
onCopy();
}}
>
{i18n('icu:copy')}
</MenuItem>
)}
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__more-info',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onMoreInfo();
}}
>
{i18n('icu:MessageContextMenu__info')}
</MenuItem>
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__delete-message',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onDeleteMessage();
}}
>
{i18n('icu:MessageContextMenu__deleteMessage')}
</MenuItem>
{onRetryMessageSend && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__retry-send',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onRetryMessageSend();
}}
>
{i18n('icu:retrySend')}
</MenuItem>
)}
{onRetryDeleteForEveryone && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__delete-message-for-everyone',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onRetryDeleteForEveryone();
}}
>
{i18n('icu:retryDeleteForEveryone')}
</MenuItem>
)}
</ContextMenu>
);
return ReactDOM.createPortal(menu, document.body);
};

View file

@ -10,6 +10,7 @@ import * as KeyboardLayout from '../services/keyboardLayout';
import { getHasPanelOpen } from '../state/selectors/conversations'; import { getHasPanelOpen } from '../state/selectors/conversations';
import { isInFullScreenCall } from '../state/selectors/calling'; import { isInFullScreenCall } from '../state/selectors/calling';
import { isShowingAnyModal } from '../state/selectors/globalModals'; import { isShowingAnyModal } from '../state/selectors/globalModals';
import type { ContextMenuTriggerType } from '../components/conversation/MessageContextMenu';
type KeyboardShortcutHandlerType = (ev: KeyboardEvent) => boolean; type KeyboardShortcutHandlerType = (ev: KeyboardEvent) => boolean;
@ -225,7 +226,7 @@ export function useToggleReactionPicker(
} }
export function useOpenContextMenu( export function useOpenContextMenu(
openContextMenu: () => unknown openContextMenu: ContextMenuTriggerType['handleContextClick'] | undefined
): KeyboardShortcutHandlerType { ): KeyboardShortcutHandlerType {
const hasOverlay = useHasAnyOverlay(); const hasOverlay = useHasAnyOverlay();
@ -247,7 +248,7 @@ export function useOpenContextMenu(
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
openContextMenu(); openContextMenu?.(new MouseEvent('click'));
return true; return true;
} }

View file

@ -1328,6 +1328,8 @@ export type GetPropsForCallHistoryOptions = Pick<
| 'callHistorySelector' | 'callHistorySelector'
| 'conversationSelector' | 'conversationSelector'
| 'ourConversationId' | 'ourConversationId'
| 'selectedMessageIds'
| 'targetedMessageId'
>; >;
const emptyCallNotification: CallingNotificationType = { const emptyCallNotification: CallingNotificationType = {
@ -1337,6 +1339,8 @@ const emptyCallNotification: CallingNotificationType = {
groupCallEnded: null, groupCallEnded: null,
maxDevices: Infinity, maxDevices: Infinity,
deviceCount: 0, deviceCount: 0,
isSelectMode: false,
isTargeted: false,
}; };
export function getPropsForCallHistory( export function getPropsForCallHistory(
@ -1347,6 +1351,8 @@ export function getPropsForCallHistory(
activeCall, activeCall,
conversationSelector, conversationSelector,
ourConversationId, ourConversationId,
selectedMessageIds,
targetedMessageId,
}: GetPropsForCallHistoryOptions }: GetPropsForCallHistoryOptions
): CallingNotificationType { ): CallingNotificationType {
const { callId } = message; const { callId } = message;
@ -1368,6 +1374,8 @@ export function getPropsForCallHistory(
'getPropsForCallHistory: Missing conversation' 'getPropsForCallHistory: Missing conversation'
); );
const isSelectMode = selectedMessageIds != null;
let callCreator: ConversationType | null = null; let callCreator: ConversationType | null = null;
if (callHistory.ringerId) { if (callHistory.ringerId) {
callCreator = conversationSelector(callHistory.ringerId); callCreator = conversationSelector(callHistory.ringerId);
@ -1383,6 +1391,8 @@ export function getPropsForCallHistory(
groupCallEnded: false, groupCallEnded: false,
deviceCount: 0, deviceCount: 0,
maxDevices: Infinity, maxDevices: Infinity,
isSelectMode,
isTargeted: message.id === targetedMessageId,
}; };
} }
@ -1410,6 +1420,8 @@ export function getPropsForCallHistory(
groupCallEnded: callId !== conversationCallId || deviceCount === 0, groupCallEnded: callId !== conversationCallId || deviceCount === 0,
deviceCount, deviceCount,
maxDevices, maxDevices,
isSelectMode,
isTargeted: message.id === targetedMessageId,
}; };
} }

View file

@ -43,6 +43,8 @@ describe('calling notification helpers', () => {
groupCallEnded: true, groupCallEnded: true,
deviceCount: 1, deviceCount: 1,
maxDevices: 23, maxDevices: 23,
isSelectMode: false,
isTargeted: false,
}, },
i18n i18n
), ),
@ -72,6 +74,8 @@ describe('calling notification helpers', () => {
groupCallEnded: false, groupCallEnded: false,
deviceCount: 1, deviceCount: 1,
maxDevices: 23, maxDevices: 23,
isSelectMode: false,
isTargeted: false,
}, },
i18n i18n
), ),
@ -102,6 +106,8 @@ describe('calling notification helpers', () => {
groupCallEnded: false, groupCallEnded: false,
deviceCount: 1, deviceCount: 1,
maxDevices: 23, maxDevices: 23,
isSelectMode: false,
isTargeted: false,
}, },
i18n i18n
), ),
@ -131,6 +137,8 @@ describe('calling notification helpers', () => {
groupCallEnded: false, groupCallEnded: false,
deviceCount: 1, deviceCount: 1,
maxDevices: 23, maxDevices: 23,
isSelectMode: false,
isTargeted: false,
}, },
i18n i18n
), ),
@ -157,6 +165,8 @@ describe('calling notification helpers', () => {
groupCallEnded: false, groupCallEnded: false,
deviceCount: 1, deviceCount: 1,
maxDevices: 23, maxDevices: 23,
isSelectMode: false,
isTargeted: false,
}, },
i18n i18n
), ),

View file

@ -22,6 +22,8 @@ export type CallingNotificationType = Readonly<{
groupCallEnded: boolean | null; groupCallEnded: boolean | null;
deviceCount: number; deviceCount: number;
maxDevices: number; maxDevices: number;
isSelectMode: boolean;
isTargeted: boolean;
}>; }>;
function getDirectCallNotificationText( function getDirectCallNotificationText(

View file

@ -16,7 +16,11 @@ import { dropNull } from './dropNull';
import { getCallHistorySelector } from '../state/selectors/callHistory'; import { getCallHistorySelector } from '../state/selectors/callHistory';
import { getCallSelector, getActiveCall } from '../state/selectors/calling'; import { getCallSelector, getActiveCall } from '../state/selectors/calling';
import { getCallingNotificationText } from './callingNotification'; import { getCallingNotificationText } from './callingNotification';
import { getConversationSelector } from '../state/selectors/conversations'; import {
getConversationSelector,
getSelectedMessageIds,
getTargetedMessage,
} from '../state/selectors/conversations';
import { getStringForConversationMerge } from './getStringForConversationMerge'; import { getStringForConversationMerge } from './getStringForConversationMerge';
import { getStringForProfileChange } from './getStringForProfileChange'; import { getStringForProfileChange } from './getStringForProfileChange';
import { getTitleNoDefault, getNumber } from './getTitle'; import { getTitleNoDefault, getNumber } from './getTitle';
@ -376,6 +380,8 @@ export function getNotificationDataForMessage(
activeCall: getActiveCall(state), activeCall: getActiveCall(state),
callHistorySelector: getCallHistorySelector(state), callHistorySelector: getCallHistorySelector(state),
conversationSelector: getConversationSelector(state), conversationSelector: getConversationSelector(state),
selectedMessageIds: getSelectedMessageIds(state),
targetedMessageId: getTargetedMessage(state)?.id,
}); });
if (callingNotification) { if (callingNotification) {
const text = getCallingNotificationText(callingNotification, window.i18n); const text = getCallingNotificationText(callingNotification, window.i18n);

View file

@ -3677,10 +3677,17 @@
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/conversation/TimelineMessage.tsx", "path": "ts/components/conversation/CallingNotification.tsx",
"line": " const menuTriggerRef = useRef<Trigger | null>(null);", "line": " const menuTriggerRef = React.useRef<ContextMenuTriggerType | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-11-03T14:21:47.456Z" "updated": "2023-12-08T20:28:57.595Z"
},
{
"rule": "React-useRef",
"path": "ts/components/conversation/TimelineMessage.tsx",
"line": " const menuTriggerRef = useRef<ContextMenuTriggerType | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-12-08T20:28:57.595Z"
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",