From 89281c6d702cb17c4b89edbdb1796c71df339935 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 27 Apr 2026 14:20:13 +0200 Subject: [PATCH] Refactor leveraging the fact, things blocking shortcuts are using react portals. --- src/room/InCallView.tsx | 1 - src/room/LobbyView.tsx | 6 ++++ src/useCallViewKeyboardShortcuts.ts | 52 ++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 4940f4d8..d1cfdf10 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -230,7 +230,6 @@ export const InCallView: FC = ({ // This function incorrectly assumes that there is a camera and microphone, which is not always the case. // TODO: Make sure that this module is resilient when it comes to camera/microphone availability! useCallViewKeyboardShortcuts( - containerRef1, toggleAudio, toggleVideo, setAudioEnabled, diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 367dc8df..e2d92126 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -47,6 +47,7 @@ import { usePageTitle } from "../usePageTitle"; import { getValue } from "../utils/observable"; import { useBehavior } from "../useBehavior"; import { CallFooter } from "../components/CallFooter"; +import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; interface Props { client: MatrixClient; @@ -91,6 +92,11 @@ export const LobbyView: FC = ({ const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsTab, setSettingsTab] = useState(defaultSettingsTab); + // This function incorrectly assumes that there is a camera and microphone, which is not always the case. + // TODO: Make sure that this module is resilient when it comes to camera/microphone availability! + // Next to the keyboard shortcuts, this is also responsible for catching escape key presses and forwarding the to mobile -> pip. + useCallViewKeyboardShortcuts(toggleAudio, toggleVideo, null, null, null); + const openSettings = useCallback( () => setSettingsModalOpen(true), [setSettingsModalOpen], diff --git a/src/useCallViewKeyboardShortcuts.ts b/src/useCallViewKeyboardShortcuts.ts index 3d9654be..7eb66717 100644 --- a/src/useCallViewKeyboardShortcuts.ts +++ b/src/useCallViewKeyboardShortcuts.ts @@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { type RefObject, useCallback, useMemo, useRef } from "react"; +import { logger } from "matrix-js-sdk/lib/logger"; import { useEventTarget } from "./useEvents"; import { @@ -18,22 +19,46 @@ import { * Determines whether focus is in the same part of the tree as the given * element (specifically, if the element or an ancestor of it is focused). */ -const mayReceiveKeyEvents = (e: HTMLElement): boolean => { - const focusedElement = document.activeElement; - return focusedElement !== null && focusedElement.contains(e); +const mayReceiveKeyEvents = (): boolean => { + const root = document.getElementById("root"); + if (root === null) { + logger.warn( + "[mayReceiveKeyEvents] Root element not found, always allow keyboard shortcuts (m,v,esc...)", + ); + return true; + } + const focusElement = document.activeElement; + const nothingInFocus = focusElement === null; + const focusOnBody = focusElement === document.body; + const noPrimaryFocus = + nothingInFocus || root.contains(focusElement) || focusOnBody; + + // Only if we do not have a primary focus we allow keyboard shortcut events. + return noPrimaryFocus; }; const KeyToReactionMap: Record = Object.fromEntries( ReactionSet.slice(0, ReactionsRowSize).map((r, i) => [(i + 1).toString(), r]), ); +/** + * This hook sets up gloabl keyboard shortcuts. It will filter for keyboard presses that should be ignored due to user + * currently focussing on a modal. + * This is achieved by using the fact, that all modal inputs are outside the #root element and use react portals to get rendered. + * The following shortcuts are auspported (optional): + * @param toggleAudio - triggered on (m) + * @param toggleVideo - triggered on (v) + * @param setAudioEnabled - push to talk behavior controlled via (space) + * @param sendReaction - triggered on (1,2,3,...) + * @param toggleHandRaised - triggered on (h) + * Additionally this method listens to the (escape) key to trigger the onBackButtonPressed callback, which is used to navigate to pip in the native app. + */ export function useCallViewKeyboardShortcuts( - focusElement: RefObject, toggleAudio: (() => void) | null, toggleVideo: (() => void) | null, setAudioEnabled: ((enabled: boolean) => void) | null, - sendReaction: (reaction: ReactionOption) => void, - toggleHandRaised: () => void, + sendReaction: ((reaction: ReactionOption) => void) | null, + toggleHandRaised: (() => void) | null, ): void { const spacebarHeld = useRef(false); @@ -45,8 +70,8 @@ export function useCallViewKeyboardShortcuts( "keydown", useCallback( (event: KeyboardEvent) => { - if (focusElement.current === null) return; - if (!mayReceiveKeyEvents(focusElement.current)) return; + logger.info("Keydown event", event); + if (!mayReceiveKeyEvents()) return; if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; @@ -64,16 +89,15 @@ export function useCallViewKeyboardShortcuts( } } else if (event.key === "h") { event.preventDefault(); - toggleHandRaised(); + toggleHandRaised?.(); } else if (KeyToReactionMap[event.key]) { event.preventDefault(); - sendReaction(KeyToReactionMap[event.key]); + sendReaction?.(KeyToReactionMap[event.key]); } else if (event.key === "Escape") { window.controls.onBackButtonPressed?.(); } }, [ - focusElement, toggleVideo, toggleAudio, setAudioEnabled, @@ -92,15 +116,13 @@ export function useCallViewKeyboardShortcuts( "keyup", useCallback( (event: KeyboardEvent) => { - if (focusElement.current === null) return; - if (!mayReceiveKeyEvents(focusElement.current)) return; - + if (!mayReceiveKeyEvents()) return; if (event.key === " ") { spacebarHeld.current = false; setAudioEnabled?.(false); } }, - [focusElement, setAudioEnabled], + [setAudioEnabled], ), );