2021-09-29 20:23:06 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
import { useCallback, useEffect } from 'react';
|
2021-09-29 20:23:06 +00:00
|
|
|
import { get } from 'lodash';
|
2023-05-25 16:15:16 +00:00
|
|
|
import { useSelector } from 'react-redux';
|
2021-09-29 22:21:51 +00:00
|
|
|
import * as KeyboardLayout from '../services/keyboardLayout';
|
2023-07-26 22:23:32 +00:00
|
|
|
import { getHasPanelOpen } from '../state/selectors/conversations';
|
2023-05-25 16:15:16 +00:00
|
|
|
import { isInFullScreenCall } from '../state/selectors/calling';
|
|
|
|
import { isShowingAnyModal } from '../state/selectors/globalModals';
|
2023-12-12 16:11:39 +00:00
|
|
|
import type { ContextMenuTriggerType } from '../components/conversation/MessageContextMenu';
|
2021-09-29 22:21:51 +00:00
|
|
|
|
2021-09-29 20:23:06 +00:00
|
|
|
type KeyboardShortcutHandlerType = (ev: KeyboardEvent) => boolean;
|
|
|
|
|
2023-12-22 20:51:27 +00:00
|
|
|
export function isCmdOrCtrl(ev: KeyboardEvent): boolean {
|
2021-09-29 20:23:06 +00:00
|
|
|
const { ctrlKey, metaKey } = ev;
|
|
|
|
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
|
|
|
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
|
|
|
|
return commandKey || controlKey;
|
|
|
|
}
|
|
|
|
|
2022-05-10 18:14:08 +00:00
|
|
|
function isCtrlOrAlt(ev: KeyboardEvent): boolean {
|
|
|
|
const { altKey, ctrlKey } = ev;
|
|
|
|
const controlKey = get(window, 'platform') === 'darwin' && ctrlKey;
|
|
|
|
const theAltKey = get(window, 'platform') !== 'darwin' && altKey;
|
|
|
|
return controlKey || theAltKey;
|
|
|
|
}
|
|
|
|
|
2024-03-26 21:20:17 +00:00
|
|
|
type Mods = {
|
|
|
|
// Mac: Meta (Command), Windows: Control
|
|
|
|
controlOrMeta: boolean;
|
|
|
|
// Shift
|
|
|
|
shift: boolean;
|
|
|
|
// Mac: Option, Windows: Alt
|
|
|
|
alt: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultsMods: Mods = {
|
|
|
|
controlOrMeta: false,
|
|
|
|
shift: false,
|
|
|
|
alt: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a keyboard event has the exact modifiers specified in the options,
|
|
|
|
* and no others currently pressed.
|
|
|
|
*/
|
|
|
|
function hasExactModifiers(
|
|
|
|
event: KeyboardEvent,
|
|
|
|
options: Mods | 'none'
|
|
|
|
): boolean {
|
|
|
|
const mods = options === 'none' ? defaultsMods : options;
|
|
|
|
const isApple = get(window, 'platform') === 'darwin';
|
|
|
|
|
|
|
|
if (isApple) {
|
|
|
|
if (event.metaKey !== mods.controlOrMeta) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (event.ctrlKey) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (event.ctrlKey !== mods.controlOrMeta) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (event.metaKey) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.shiftKey !== mods.shift) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.altKey !== mods.alt) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-05-25 16:15:16 +00:00
|
|
|
function useHasPanels(): boolean {
|
2023-07-26 22:23:32 +00:00
|
|
|
return useSelector(getHasPanelOpen);
|
2023-05-25 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function useHasGlobalModal(): boolean {
|
2024-03-13 20:44:13 +00:00
|
|
|
return useSelector(isShowingAnyModal);
|
2023-05-25 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function useHasCalling(): boolean {
|
2024-03-13 20:44:13 +00:00
|
|
|
return useSelector(isInFullScreenCall);
|
2023-05-25 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function useHasAnyOverlay(): boolean {
|
|
|
|
const panels = useHasPanels();
|
|
|
|
const globalModal = useHasGlobalModal();
|
|
|
|
const calling = useHasCalling();
|
|
|
|
|
2023-08-09 00:53:06 +00:00
|
|
|
return panels || globalModal || calling;
|
2023-05-25 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
2022-05-10 18:14:08 +00:00
|
|
|
export function useActiveCallShortcuts(
|
2022-08-16 23:52:09 +00:00
|
|
|
hangUp: (reason: string) => unknown
|
2022-05-10 18:14:08 +00:00
|
|
|
): KeyboardShortcutHandlerType {
|
|
|
|
return useCallback(
|
|
|
|
ev => {
|
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'e' || key === 'E')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
2022-08-16 23:52:09 +00:00
|
|
|
hangUp('Keyboard shortcut');
|
2022-05-10 18:14:08 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
[hangUp]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useIncomingCallShortcuts(
|
|
|
|
acceptAudioCall: () => unknown,
|
|
|
|
acceptVideoCall: () => unknown,
|
|
|
|
declineCall: () => unknown
|
|
|
|
): KeyboardShortcutHandlerType {
|
|
|
|
return useCallback(
|
|
|
|
ev => {
|
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'v' || key === 'V')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
acceptVideoCall();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'a' || key === 'A')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
acceptAudioCall();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'd' || key === 'D')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
declineCall();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
[acceptAudioCall, acceptVideoCall, declineCall]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useStartCallShortcuts(
|
|
|
|
startAudioCall: () => unknown,
|
|
|
|
startVideoCall: () => unknown
|
|
|
|
): KeyboardShortcutHandlerType {
|
|
|
|
return useCallback(
|
|
|
|
ev => {
|
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'c' || key === 'C')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
startAudioCall();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isCtrlOrAlt(ev) && shiftKey && (key === 'y' || key === 'Y')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
startVideoCall();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
[startAudioCall, startVideoCall]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
export function useStartRecordingShortcut(
|
2021-09-29 20:23:06 +00:00
|
|
|
startAudioRecording: () => unknown
|
|
|
|
): KeyboardShortcutHandlerType {
|
2023-05-25 16:15:16 +00:00
|
|
|
const hasOverlay = useHasAnyOverlay();
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
return useCallback(
|
|
|
|
ev => {
|
2023-05-25 16:15:16 +00:00
|
|
|
if (hasOverlay) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
2021-09-29 22:21:51 +00:00
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
if (isCmdOrCtrl(ev) && shiftKey && (key === 'v' || key === 'V')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2021-09-29 20:23:06 +00:00
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
startAudioRecording();
|
|
|
|
return true;
|
|
|
|
}
|
2021-09-29 20:23:06 +00:00
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
return false;
|
|
|
|
},
|
2023-05-25 16:15:16 +00:00
|
|
|
[hasOverlay, startAudioRecording]
|
2021-10-15 18:51:58 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useAttachFileShortcut(
|
|
|
|
attachFile: () => unknown
|
|
|
|
): KeyboardShortcutHandlerType {
|
2023-05-25 16:15:16 +00:00
|
|
|
const hasOverlay = useHasAnyOverlay();
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
return useCallback(
|
|
|
|
ev => {
|
2023-05-25 16:15:16 +00:00
|
|
|
if (hasOverlay) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
|
|
|
if (isCmdOrCtrl(ev) && !shiftKey && (key === 'u' || key === 'U')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
attachFile();
|
|
|
|
return true;
|
|
|
|
}
|
2021-09-29 20:23:06 +00:00
|
|
|
|
2021-10-15 18:51:58 +00:00
|
|
|
return false;
|
|
|
|
},
|
2023-05-25 16:15:16 +00:00
|
|
|
[attachFile, hasOverlay]
|
2021-10-15 18:51:58 +00:00
|
|
|
);
|
2021-09-29 20:23:06 +00:00
|
|
|
}
|
|
|
|
|
2022-12-03 00:40:33 +00:00
|
|
|
export function useToggleReactionPicker(
|
|
|
|
handleReact: () => unknown
|
|
|
|
): KeyboardShortcutHandlerType {
|
2023-05-25 16:15:16 +00:00
|
|
|
const hasOverlay = useHasAnyOverlay();
|
|
|
|
|
2022-12-03 00:40:33 +00:00
|
|
|
return useCallback(
|
|
|
|
ev => {
|
2023-05-25 16:15:16 +00:00
|
|
|
if (hasOverlay) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-03 00:40:33 +00:00
|
|
|
const { shiftKey } = ev;
|
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
|
|
|
if (isCmdOrCtrl(ev) && shiftKey && (key === 'e' || key === 'E')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
handleReact();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
2023-05-25 16:15:16 +00:00
|
|
|
[handleReact, hasOverlay]
|
2022-12-03 00:40:33 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-06-21 16:54:05 +00:00
|
|
|
export function useOpenContextMenu(
|
2023-12-12 16:11:39 +00:00
|
|
|
openContextMenu: ContextMenuTriggerType['handleContextClick'] | undefined
|
2023-06-21 16:54:05 +00:00
|
|
|
): 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();
|
|
|
|
|
2023-12-12 16:11:39 +00:00
|
|
|
openContextMenu?.(new MouseEvent('click'));
|
2023-06-21 16:54:05 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
[hasOverlay, openContextMenu]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-12 00:27:19 +00:00
|
|
|
export function useEditLastMessageSent(
|
|
|
|
maybeEditMessage: () => boolean
|
|
|
|
): KeyboardShortcutHandlerType {
|
2023-05-25 16:15:16 +00:00
|
|
|
const hasOverlay = useHasAnyOverlay();
|
|
|
|
|
2023-05-12 00:27:19 +00:00
|
|
|
return useCallback(
|
|
|
|
ev => {
|
2023-05-25 16:15:16 +00:00
|
|
|
if (hasOverlay) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-12 00:27:19 +00:00
|
|
|
const key = KeyboardLayout.lookup(ev);
|
|
|
|
|
2024-03-26 21:20:17 +00:00
|
|
|
// None of the modifiers should be pressed
|
|
|
|
if (!hasExactModifiers(ev, 'none')) {
|
2023-10-16 19:34:25 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-12 00:27:19 +00:00
|
|
|
if (key === 'ArrowUp') {
|
|
|
|
const value = maybeEditMessage();
|
|
|
|
if (value) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
2023-05-25 16:15:16 +00:00
|
|
|
[hasOverlay, maybeEditMessage]
|
2023-05-12 00:27:19 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-29 20:23:06 +00:00
|
|
|
export function useKeyboardShortcuts(
|
|
|
|
...eventHandlers: Array<KeyboardShortcutHandlerType>
|
|
|
|
): void {
|
|
|
|
useEffect(() => {
|
|
|
|
function handleKeydown(ev: KeyboardEvent): void {
|
|
|
|
eventHandlers.some(eventHandler => eventHandler(ev));
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeydown);
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', handleKeydown);
|
|
|
|
};
|
|
|
|
}, [eventHandlers]);
|
|
|
|
}
|
2023-06-21 16:54:05 +00:00
|
|
|
|
|
|
|
export function useKeyboardShortcutsConditionally(
|
|
|
|
condition: boolean,
|
|
|
|
...eventHandlers: Array<KeyboardShortcutHandlerType>
|
|
|
|
): 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]);
|
|
|
|
}
|