Add contextMenu for deleting call events on right-click
This commit is contained in:
parent
7fb01f102d
commit
88fd42a46b
12 changed files with 394 additions and 269 deletions
|
@ -7114,6 +7114,14 @@ button.module-image__border-overlay:focus {
|
|||
user-select: none;
|
||||
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 {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ const getCommonProps = (options: {
|
|||
mode === CallMode.Group ? getDefaultGroup() : getDefaultConversation();
|
||||
|
||||
return {
|
||||
id: 'message-id',
|
||||
conversationId: conversation.id,
|
||||
i18n,
|
||||
isNextItemCallingNotification: false,
|
||||
|
@ -67,6 +68,7 @@ const getCommonProps = (options: {
|
|||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
callHistory: {
|
||||
callId: '123',
|
||||
|
@ -83,6 +85,8 @@ const getCommonProps = (options: {
|
|||
groupCallEnded,
|
||||
maxDevices,
|
||||
deviceCount,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { ContextMenuTrigger } from 'react-contextmenu';
|
||||
|
||||
import { SystemMessage, SystemMessageKind } from './SystemMessage';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
|
@ -24,15 +25,27 @@ import {
|
|||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} 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 = {
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
returnToActiveCall: () => void;
|
||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
id: string;
|
||||
conversationId: string;
|
||||
isNextItemCallingNotification: boolean;
|
||||
};
|
||||
|
@ -43,36 +56,81 @@ export type PropsType = CallingNotificationType &
|
|||
|
||||
export const CallingNotification: React.FC<PropsType> = React.memo(
|
||||
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;
|
||||
if (props.callHistory == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { type, direction, status, timestamp } = props.callHistory;
|
||||
const icon = getCallingIcon(type, direction, status);
|
||||
return (
|
||||
<SystemMessage
|
||||
button={renderCallingNotificationButton(props)}
|
||||
contents={
|
||||
<>
|
||||
{getCallingNotificationText(props, i18n)} ·{' '}
|
||||
<MessageTimestamp
|
||||
direction="outgoing"
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={false}
|
||||
withSticker={false}
|
||||
withTapToViewExpired={false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
icon={icon}
|
||||
kind={
|
||||
status === DirectCallStatus.Missed ||
|
||||
status === GroupCallStatus.Missed
|
||||
? SystemMessageKind.Danger
|
||||
: SystemMessageKind.Normal
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
onContextMenu={handleContextMenu}
|
||||
// @ts-expect-error -- React/TS doesn't know about inert
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
inert={props.isSelectMode ? '' : undefined}
|
||||
>
|
||||
<SystemMessage
|
||||
button={renderCallingNotificationButton(props)}
|
||||
contents={
|
||||
<>
|
||||
{getCallingNotificationText(props, i18n)} ·{' '}
|
||||
<MessageTimestamp
|
||||
direction="outgoing"
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={false}
|
||||
withSticker={false}
|
||||
withTapToViewExpired={false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
244
ts/components/conversation/MessageContextMenu.tsx
Normal file
244
ts/components/conversation/MessageContextMenu.tsx
Normal 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]
|
||||
);
|
||||
}
|
|
@ -253,11 +253,13 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
} else if (item.type === 'callHistory') {
|
||||
notification = (
|
||||
<CallingNotification
|
||||
id={id}
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
isNextItemCallingNotification={isNextItemCallingNotification}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
toggleDeleteMessagesModal={reducedProps.toggleDeleteMessagesModal}
|
||||
returnToActiveCall={returnToActiveCall}
|
||||
{...item.data}
|
||||
/>
|
||||
|
|
|
@ -5,8 +5,8 @@ import classNames from 'classnames';
|
|||
import { noop } from 'lodash';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { Ref } from 'react';
|
||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||
import ReactDOM, { createPortal } from 'react-dom';
|
||||
import { ContextMenuTrigger } from 'react-contextmenu';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
|
@ -34,6 +34,11 @@ import {
|
|||
import { PanelType } from '../../types/Panels';
|
||||
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
|
||||
import { useScrollerLock } from '../../hooks/useScrollLock';
|
||||
import {
|
||||
type ContextMenuTriggerType,
|
||||
MessageContextMenu,
|
||||
useHandleMessageContextMenu,
|
||||
} from './MessageContextMenu';
|
||||
|
||||
export type PropsData = {
|
||||
canDownload: boolean;
|
||||
|
@ -77,12 +82,6 @@ export type Props = PropsData &
|
|||
) => JSX.Element;
|
||||
};
|
||||
|
||||
type Trigger = {
|
||||
handleContextClick: (
|
||||
event: React.MouseEvent<HTMLDivElement> | MouseEvent
|
||||
) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<
|
||||
HTMLDivElement | undefined
|
||||
>(undefined);
|
||||
const menuTriggerRef = useRef<Trigger | null>(null);
|
||||
const menuTriggerRef = useRef<ContextMenuTriggerType | null>(null);
|
||||
|
||||
const isWindowWidthNotNarrow =
|
||||
containerWidthBreakpoint !== WidthBreakpoint.Narrow;
|
||||
|
@ -228,22 +227,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
[kickOffAttachmentDownload, saveAttachment, attachments, id, timestamp]
|
||||
);
|
||||
|
||||
const handleContextMenu = React.useCallback(
|
||||
(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 handleContextMenu = useHandleMessageContextMenu(menuTriggerRef);
|
||||
const canForward =
|
||||
!isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment;
|
||||
|
||||
|
@ -280,15 +264,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
handleReact || noop
|
||||
);
|
||||
|
||||
const handleOpenContextMenu = useCallback(() => {
|
||||
if (!menuTriggerRef.current) {
|
||||
return;
|
||||
}
|
||||
const event = new MouseEvent('click');
|
||||
menuTriggerRef.current.handleContextClick(event);
|
||||
}, []);
|
||||
|
||||
const openContextMenuKeyboard = useOpenContextMenu(handleOpenContextMenu);
|
||||
const openContextMenuKeyboard = useOpenContextMenu(handleContextMenu);
|
||||
|
||||
useKeyboardShortcutsConditionally(
|
||||
Boolean(isTargeted),
|
||||
|
@ -419,7 +395,7 @@ type MessageMenuProps = {
|
|||
i18n: LocalizerType;
|
||||
triggerId: string;
|
||||
isWindowWidthNotNarrow: boolean;
|
||||
menuTriggerRef: Ref<Trigger>;
|
||||
menuTriggerRef: Ref<ContextMenuTriggerType>;
|
||||
showMenu: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onDownload: (() => void) | undefined;
|
||||
onReplyToMessage: (() => void) | undefined;
|
||||
|
@ -569,208 +545,3 @@ function MessageMenu({
|
|||
</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);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as KeyboardLayout from '../services/keyboardLayout';
|
|||
import { getHasPanelOpen } from '../state/selectors/conversations';
|
||||
import { isInFullScreenCall } from '../state/selectors/calling';
|
||||
import { isShowingAnyModal } from '../state/selectors/globalModals';
|
||||
import type { ContextMenuTriggerType } from '../components/conversation/MessageContextMenu';
|
||||
|
||||
type KeyboardShortcutHandlerType = (ev: KeyboardEvent) => boolean;
|
||||
|
||||
|
@ -225,7 +226,7 @@ export function useToggleReactionPicker(
|
|||
}
|
||||
|
||||
export function useOpenContextMenu(
|
||||
openContextMenu: () => unknown
|
||||
openContextMenu: ContextMenuTriggerType['handleContextClick'] | undefined
|
||||
): KeyboardShortcutHandlerType {
|
||||
const hasOverlay = useHasAnyOverlay();
|
||||
|
||||
|
@ -247,7 +248,7 @@ export function useOpenContextMenu(
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
openContextMenu();
|
||||
openContextMenu?.(new MouseEvent('click'));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1328,6 +1328,8 @@ export type GetPropsForCallHistoryOptions = Pick<
|
|||
| 'callHistorySelector'
|
||||
| 'conversationSelector'
|
||||
| 'ourConversationId'
|
||||
| 'selectedMessageIds'
|
||||
| 'targetedMessageId'
|
||||
>;
|
||||
|
||||
const emptyCallNotification: CallingNotificationType = {
|
||||
|
@ -1337,6 +1339,8 @@ const emptyCallNotification: CallingNotificationType = {
|
|||
groupCallEnded: null,
|
||||
maxDevices: Infinity,
|
||||
deviceCount: 0,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
};
|
||||
|
||||
export function getPropsForCallHistory(
|
||||
|
@ -1347,6 +1351,8 @@ export function getPropsForCallHistory(
|
|||
activeCall,
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
selectedMessageIds,
|
||||
targetedMessageId,
|
||||
}: GetPropsForCallHistoryOptions
|
||||
): CallingNotificationType {
|
||||
const { callId } = message;
|
||||
|
@ -1368,6 +1374,8 @@ export function getPropsForCallHistory(
|
|||
'getPropsForCallHistory: Missing conversation'
|
||||
);
|
||||
|
||||
const isSelectMode = selectedMessageIds != null;
|
||||
|
||||
let callCreator: ConversationType | null = null;
|
||||
if (callHistory.ringerId) {
|
||||
callCreator = conversationSelector(callHistory.ringerId);
|
||||
|
@ -1383,6 +1391,8 @@ export function getPropsForCallHistory(
|
|||
groupCallEnded: false,
|
||||
deviceCount: 0,
|
||||
maxDevices: Infinity,
|
||||
isSelectMode,
|
||||
isTargeted: message.id === targetedMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1410,6 +1420,8 @@ export function getPropsForCallHistory(
|
|||
groupCallEnded: callId !== conversationCallId || deviceCount === 0,
|
||||
deviceCount,
|
||||
maxDevices,
|
||||
isSelectMode,
|
||||
isTargeted: message.id === targetedMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ describe('calling notification helpers', () => {
|
|||
groupCallEnded: true,
|
||||
deviceCount: 1,
|
||||
maxDevices: 23,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
},
|
||||
i18n
|
||||
),
|
||||
|
@ -72,6 +74,8 @@ describe('calling notification helpers', () => {
|
|||
groupCallEnded: false,
|
||||
deviceCount: 1,
|
||||
maxDevices: 23,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
},
|
||||
i18n
|
||||
),
|
||||
|
@ -102,6 +106,8 @@ describe('calling notification helpers', () => {
|
|||
groupCallEnded: false,
|
||||
deviceCount: 1,
|
||||
maxDevices: 23,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
},
|
||||
i18n
|
||||
),
|
||||
|
@ -131,6 +137,8 @@ describe('calling notification helpers', () => {
|
|||
groupCallEnded: false,
|
||||
deviceCount: 1,
|
||||
maxDevices: 23,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
},
|
||||
i18n
|
||||
),
|
||||
|
@ -157,6 +165,8 @@ describe('calling notification helpers', () => {
|
|||
groupCallEnded: false,
|
||||
deviceCount: 1,
|
||||
maxDevices: 23,
|
||||
isSelectMode: false,
|
||||
isTargeted: false,
|
||||
},
|
||||
i18n
|
||||
),
|
||||
|
|
|
@ -22,6 +22,8 @@ export type CallingNotificationType = Readonly<{
|
|||
groupCallEnded: boolean | null;
|
||||
deviceCount: number;
|
||||
maxDevices: number;
|
||||
isSelectMode: boolean;
|
||||
isTargeted: boolean;
|
||||
}>;
|
||||
|
||||
function getDirectCallNotificationText(
|
||||
|
|
|
@ -16,7 +16,11 @@ import { dropNull } from './dropNull';
|
|||
import { getCallHistorySelector } from '../state/selectors/callHistory';
|
||||
import { getCallSelector, getActiveCall } from '../state/selectors/calling';
|
||||
import { getCallingNotificationText } from './callingNotification';
|
||||
import { getConversationSelector } from '../state/selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getSelectedMessageIds,
|
||||
getTargetedMessage,
|
||||
} from '../state/selectors/conversations';
|
||||
import { getStringForConversationMerge } from './getStringForConversationMerge';
|
||||
import { getStringForProfileChange } from './getStringForProfileChange';
|
||||
import { getTitleNoDefault, getNumber } from './getTitle';
|
||||
|
@ -376,6 +380,8 @@ export function getNotificationDataForMessage(
|
|||
activeCall: getActiveCall(state),
|
||||
callHistorySelector: getCallHistorySelector(state),
|
||||
conversationSelector: getConversationSelector(state),
|
||||
selectedMessageIds: getSelectedMessageIds(state),
|
||||
targetedMessageId: getTargetedMessage(state)?.id,
|
||||
});
|
||||
if (callingNotification) {
|
||||
const text = getCallingNotificationText(callingNotification, window.i18n);
|
||||
|
|
|
@ -3677,10 +3677,17 @@
|
|||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/TimelineMessage.tsx",
|
||||
"line": " const menuTriggerRef = useRef<Trigger | null>(null);",
|
||||
"path": "ts/components/conversation/CallingNotification.tsx",
|
||||
"line": " const menuTriggerRef = React.useRef<ContextMenuTriggerType | null>(null);",
|
||||
"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",
|
||||
|
|
Loading…
Reference in a new issue