From bd2c531c13b014d223233a08c190f117ca8eebc2 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 19 May 2026 20:06:45 -0500 Subject: [PATCH] Add toggle raised hand shortcut in group calls Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com> --- _locales/en/messages.json | 4 +++ ts/components/CallScreen.dom.tsx | 49 ++++++++++----------------- ts/components/ShortcutGuide.dom.tsx | 6 ++++ ts/hooks/useKeyboardShortcuts.dom.tsx | 38 +++++++++++++++++++-- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a986cebb24..ff77dfeda4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4398,6 +4398,10 @@ "messageformat": "Toggle expanded preview on and off", "description": "Shown in the shortcuts guide" }, + "icu:Keyboard--toggle-raise-hand": { + "messageformat": "Raise or lower your hand (group calls only)", + "description": "Shown in the shortcuts guide" + }, "icu:Keyboard--accept-video-call": { "messageformat": "Answer call with video (video calls only)", "description": "Shown in the calling keyboard shortcuts guide" diff --git a/ts/components/CallScreen.dom.tsx b/ts/components/CallScreen.dom.tsx index 9ea02d9ad3..38fe708caf 100644 --- a/ts/components/CallScreen.dom.tsx +++ b/ts/components/CallScreen.dom.tsx @@ -65,7 +65,6 @@ import { CallParticipantCount } from './CallParticipantCount.dom.tsx'; import type { LocalizerType } from '../types/Util.std.ts'; import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal.dom.tsx'; import { missingCaseError } from '../util/missingCaseError.std.ts'; -import * as KeyboardLayout from '../services/keyboardLayout.dom.ts'; import { usePresenter, useActivateSpeakerViewOnPresenting, @@ -75,6 +74,7 @@ import { SPEAKING_LINGER_MS, } from './CallingAudioIndicator.dom.tsx'; import { + makeKeyboardShortcutHandler, useActiveCallShortcuts, useKeyboardShortcuts, } from '../hooks/useKeyboardShortcuts.dom.tsx'; @@ -390,37 +390,6 @@ export function CallScreen({ return clearTimeout.bind(null, timer); }, [showSelfViewControls, setShowSelfViewControls, selfViewHover]); - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent): void => { - let eventHandled = false; - - const key = KeyboardLayout.lookup(event); - - if (event.shiftKey && (key === 'V' || key === 'v')) { - toggleVideo(); - setShowControls(true); - eventHandled = true; - } else if (event.shiftKey && (key === 'M' || key === 'm')) { - toggleAudio(); - setShowControls(true); - eventHandled = true; - } else if (event.shiftKey && (key === 'P' || key === 'p')) { - toggleSelfViewExpanded(); - eventHandled = true; - } - - if (eventHandled) { - event.preventDefault(); - event.stopPropagation(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [setShowControls, toggleAudio, toggleSelfViewExpanded, toggleVideo]); - useEffect(() => { if (!showReactionPicker) { return noop; @@ -956,6 +925,22 @@ export function CallScreen({ toggleParticipants, ]); + useKeyboardShortcuts( + makeKeyboardShortcutHandler('v', { shift: true }, () => { + toggleVideo(); + setShowControls(true); + }), + makeKeyboardShortcutHandler('m', { shift: true }, () => { + toggleAudio(); + setShowControls(true); + }), + makeKeyboardShortcutHandler('h', { shift: true }, () => { + toggleRaiseHand(); + setShowControls(true); + }), + makeKeyboardShortcutHandler('p', { shift: true }, toggleSelfViewExpanded) + ); + let remoteParticipantsElement: ReactNode; switch (activeCall.callMode) { case CallMode.Direct: { diff --git a/ts/components/ShortcutGuide.dom.tsx b/ts/components/ShortcutGuide.dom.tsx index 96ce5fe4b0..a00670f652 100644 --- a/ts/components/ShortcutGuide.dom.tsx +++ b/ts/components/ShortcutGuide.dom.tsx @@ -35,6 +35,7 @@ type KeyType = | 'E' | 'F' | 'G' + | 'H' | 'I' | 'J' | 'K' @@ -329,6 +330,11 @@ function getCallingShortcuts(i18n: LocalizerType): Array { description: i18n('icu:Keyboard--toggle-preview'), keys: [['shift', 'P']], }, + { + id: 'Keyboard--toggle-raise-hand', + description: i18n('icu:Keyboard--toggle-raise-hand'), + keys: [['shift', 'H']], + }, { id: 'Keyboard--accept-video-call', description: i18n('icu:Keyboard--accept-video-call'), diff --git a/ts/hooks/useKeyboardShortcuts.dom.tsx b/ts/hooks/useKeyboardShortcuts.dom.tsx index fb75e4e478..114ba7aa46 100644 --- a/ts/hooks/useKeyboardShortcuts.dom.tsx +++ b/ts/hooks/useKeyboardShortcuts.dom.tsx @@ -1,7 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import lodash from 'lodash'; import { useSelector } from 'react-redux'; import * as KeyboardLayout from '../services/keyboardLayout.dom.ts'; @@ -331,14 +331,46 @@ export function useEditLastMessageSent( export function useKeyboardShortcuts( ...eventHandlers: Array ): void { + const handlersRef = useRef(eventHandlers); + handlersRef.current = eventHandlers; + useEffect(() => { function handleKeydown(ev: KeyboardEvent): void { - eventHandlers.some(eventHandler => eventHandler(ev)); + handlersRef.current.some(eventHandler => eventHandler(ev)); } document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; - }, [eventHandlers]); + }, []); +} + +/** `key` is matched case-insensitively. */ +export function makeKeyboardShortcutHandler( + key: string, + strictMods: Partial, + callback: () => unknown +): KeyboardShortcutHandlerType { + return ev => { + const keyPressed = KeyboardLayout.lookup(ev); + + if ( + hasExactModifiers(ev, { + controlOrMeta: false, + shift: false, + alt: false, + ...strictMods, + }) && + keyPressed?.toLowerCase() === key.toLowerCase() + ) { + ev.preventDefault(); + ev.stopPropagation(); + + callback(); + return true; + } + + return false; + }; }