diff --git a/_locales/en/messages.json b/_locales/en/messages.json index df9361b1b9..ef1a400e69 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2934,6 +2934,10 @@ "messageformat": "Mark selected text as a spoiler", "description": "Description of command to bold text in composer" }, + "icu:Keyboard--open-context-menu": { + "messageformat": "Open context menu for selected message", + "description": "Shown in shortcuts guide" + }, "icu:FormatMenu--guide--bold": { "messageformat": "Bold", "description": "Shown when you hover over the bold button in the popup formatting menu" diff --git a/ts/components/ShortcutGuide.tsx b/ts/components/ShortcutGuide.tsx index 7b72e71df1..7da2a0d607 100644 --- a/ts/components/ShortcutGuide.tsx +++ b/ts/components/ShortcutGuide.tsx @@ -24,6 +24,8 @@ type KeyType = | 'tab' | 'ctrl' | 'F6' + | 'F10' + | 'F12' | '↑' | '↓' | ',' @@ -53,8 +55,17 @@ type KeyType = type ShortcutType = { id: string; description: string; - keys: Array>; -}; +} & ( + | { + keys: Array>; + } + | { + keysByPlatform: { + macOS: Array>; + other: Array>; + }; + } +); function getNavigationShortcuts(i18n: LocalizerType): Array { return [ @@ -217,6 +228,14 @@ function getMessageShortcuts(i18n: LocalizerType): Array { description: i18n('icu:Keyboard--forward-messages'), keys: [['commandOrCtrl', 'shift', 'S']], }, + { + id: 'Keyboard--open-context-menu', + description: i18n('icu:Keyboard--open-context-menu'), + keysByPlatform: { + macOS: [['commandOrCtrl', 'F12']], + other: [['shift', 'F10']], + }, + }, ]; } @@ -439,13 +458,23 @@ function renderShortcut( isMacOS: boolean, i18n: LocalizerType ) { + let keysToRender: Array> = []; + + if ('keys' in shortcut) { + keysToRender = shortcut.keys; + } else if ('keysByPlatform' in shortcut) { + keysToRender = isMacOS + ? shortcut.keysByPlatform.macOS + : shortcut.keysByPlatform.other; + } + return (
{shortcut.description}
- {shortcut.keys.map(keys => ( + {keysToRender.map(keys => (
k).join('-')}`} className="module-shortcut-guide__shortcut__key-inner-container" diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 7157d408b6..4ae4c4c4d9 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -26,7 +26,11 @@ import type { import type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; import { doesMessageBodyOverflow } from './MessageBodyReadMore'; import type { Props as ReactionPickerProps } from './ReactionPicker'; -import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; +import { + useKeyboardShortcutsConditionally, + useOpenContextMenu, + useToggleReactionPicker, +} from '../../hooks/useKeyboardShortcuts'; import { PanelType } from '../../types/Panels'; import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals'; @@ -73,7 +77,9 @@ export type Props = PropsData & }; type Trigger = { - handleContextClick: (event: React.MouseEvent) => void; + handleContextClick: ( + event: React.MouseEvent | MouseEvent + ) => void; }; /** @@ -265,15 +271,21 @@ export function TimelineMessage(props: Props): JSX.Element { handleReact || noop ); - useEffect(() => { - if (isTargeted) { - document.addEventListener('keydown', toggleReactionPickerKeyboard); + const handleOpenContextMenu = useCallback(() => { + if (!menuTriggerRef.current) { + return; } + const event = new MouseEvent('click'); + menuTriggerRef.current.handleContextClick(event); + }, []); - return () => { - document.removeEventListener('keydown', toggleReactionPickerKeyboard); - }; - }, [isTargeted, toggleReactionPickerKeyboard]); + const openContextMenuKeyboard = useOpenContextMenu(handleOpenContextMenu); + + useKeyboardShortcutsConditionally( + Boolean(isTargeted), + openContextMenuKeyboard, + toggleReactionPickerKeyboard + ); const renderMenu = useCallback(() => { return ( diff --git a/ts/hooks/useKeyboardShortcuts.tsx b/ts/hooks/useKeyboardShortcuts.tsx index 9f74d0cb0f..f042b33868 100644 --- a/ts/hooks/useKeyboardShortcuts.tsx +++ b/ts/hooks/useKeyboardShortcuts.tsx @@ -234,6 +234,39 @@ export function useToggleReactionPicker( ); } +export function useOpenContextMenu( + openContextMenu: () => unknown +): KeyboardShortcutHandlerType { + const hasOverlay = useHasAnyOverlay(); + + return useCallback( + ev => { + if (hasOverlay) { + return false; + } + + const { shiftKey } = ev; + const key = KeyboardLayout.lookup(ev); + + const isMacOS = get(window, 'platform') === 'darwin'; + + if ( + (!isMacOS && shiftKey && key === 'F10') || + (isMacOS && isCmdOrCtrl(ev) && key === 'F12') + ) { + ev.preventDefault(); + ev.stopPropagation(); + + openContextMenu(); + return true; + } + + return false; + }, + [hasOverlay, openContextMenu] + ); +} + export function useEditLastMessageSent( maybeEditMessage: () => boolean ): KeyboardShortcutHandlerType { @@ -277,3 +310,23 @@ export function useKeyboardShortcuts( }; }, [eventHandlers]); } + +export function useKeyboardShortcutsConditionally( + condition: boolean, + ...eventHandlers: Array +): void { + useEffect(() => { + if (!condition) { + return; + } + + function handleKeydown(ev: KeyboardEvent): void { + eventHandlers.some(eventHandler => eventHandler(ev)); + } + + document.addEventListener('keydown', handleKeydown); + return () => { + document.removeEventListener('keydown', handleKeydown); + }; + }, [condition, eventHandlers]); +} diff --git a/ts/services/addGlobalKeyboardShortcuts.ts b/ts/services/addGlobalKeyboardShortcuts.ts index 014cf6ca83..25917d79b9 100644 --- a/ts/services/addGlobalKeyboardShortcuts.ts +++ b/ts/services/addGlobalKeyboardShortcuts.ts @@ -11,11 +11,13 @@ import { getQuotedMessageSelector } from '../state/selectors/composer'; import { removeLinkPreview } from './LinkPreview'; export function addGlobalKeyboardShortcuts(): void { + const isMacOS = window.platform === 'darwin'; + document.addEventListener('keydown', event => { const { ctrlKey, metaKey, shiftKey, altKey } = event; - const commandKey = window.platform === 'darwin' && metaKey; - const controlKey = window.platform !== 'darwin' && ctrlKey; + const commandKey = isMacOS && metaKey; + const controlKey = !isMacOS && ctrlKey; const commandOrCtrl = commandKey || controlKey; const state = window.reduxStore.getState();