Refactor leveraging the fact, things blocking shortcuts are using react

portals.
This commit is contained in:
Timo K
2026-04-27 14:21:02 +02:00
parent 56bab70534
commit 89281c6d70
3 changed files with 43 additions and 16 deletions
-1
View File
@@ -230,7 +230,6 @@ export const InCallView: FC<InCallViewProps> = ({
// This function incorrectly assumes that there is a camera and microphone, which is not always the case. // 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! // TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
useCallViewKeyboardShortcuts( useCallViewKeyboardShortcuts(
containerRef1,
toggleAudio, toggleAudio,
toggleVideo, toggleVideo,
setAudioEnabled, setAudioEnabled,
+6
View File
@@ -47,6 +47,7 @@ import { usePageTitle } from "../usePageTitle";
import { getValue } from "../utils/observable"; import { getValue } from "../utils/observable";
import { useBehavior } from "../useBehavior"; import { useBehavior } from "../useBehavior";
import { CallFooter } from "../components/CallFooter"; import { CallFooter } from "../components/CallFooter";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
@@ -91,6 +92,11 @@ export const LobbyView: FC<Props> = ({
const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab); 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( const openSettings = useCallback(
() => setSettingsModalOpen(true), () => setSettingsModalOpen(true),
[setSettingsModalOpen], [setSettingsModalOpen],
+37 -15
View File
@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { type RefObject, useCallback, useMemo, useRef } from "react"; import { type RefObject, useCallback, useMemo, useRef } from "react";
import { logger } from "matrix-js-sdk/lib/logger";
import { useEventTarget } from "./useEvents"; import { useEventTarget } from "./useEvents";
import { import {
@@ -18,22 +19,46 @@ import {
* Determines whether focus is in the same part of the tree as the given * 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). * element (specifically, if the element or an ancestor of it is focused).
*/ */
const mayReceiveKeyEvents = (e: HTMLElement): boolean => { const mayReceiveKeyEvents = (): boolean => {
const focusedElement = document.activeElement; const root = document.getElementById("root");
return focusedElement !== null && focusedElement.contains(e); 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<string, ReactionOption> = Object.fromEntries( const KeyToReactionMap: Record<string, ReactionOption> = Object.fromEntries(
ReactionSet.slice(0, ReactionsRowSize).map((r, i) => [(i + 1).toString(), r]), 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( export function useCallViewKeyboardShortcuts(
focusElement: RefObject<HTMLElement | null>,
toggleAudio: (() => void) | null, toggleAudio: (() => void) | null,
toggleVideo: (() => void) | null, toggleVideo: (() => void) | null,
setAudioEnabled: ((enabled: boolean) => void) | null, setAudioEnabled: ((enabled: boolean) => void) | null,
sendReaction: (reaction: ReactionOption) => void, sendReaction: ((reaction: ReactionOption) => void) | null,
toggleHandRaised: () => void, toggleHandRaised: (() => void) | null,
): void { ): void {
const spacebarHeld = useRef(false); const spacebarHeld = useRef(false);
@@ -45,8 +70,8 @@ export function useCallViewKeyboardShortcuts(
"keydown", "keydown",
useCallback( useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (focusElement.current === null) return; logger.info("Keydown event", event);
if (!mayReceiveKeyEvents(focusElement.current)) return; if (!mayReceiveKeyEvents()) return;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
return; return;
@@ -64,16 +89,15 @@ export function useCallViewKeyboardShortcuts(
} }
} else if (event.key === "h") { } else if (event.key === "h") {
event.preventDefault(); event.preventDefault();
toggleHandRaised(); toggleHandRaised?.();
} else if (KeyToReactionMap[event.key]) { } else if (KeyToReactionMap[event.key]) {
event.preventDefault(); event.preventDefault();
sendReaction(KeyToReactionMap[event.key]); sendReaction?.(KeyToReactionMap[event.key]);
} else if (event.key === "Escape") { } else if (event.key === "Escape") {
window.controls.onBackButtonPressed?.(); window.controls.onBackButtonPressed?.();
} }
}, },
[ [
focusElement,
toggleVideo, toggleVideo,
toggleAudio, toggleAudio,
setAudioEnabled, setAudioEnabled,
@@ -92,15 +116,13 @@ export function useCallViewKeyboardShortcuts(
"keyup", "keyup",
useCallback( useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (focusElement.current === null) return; if (!mayReceiveKeyEvents()) return;
if (!mayReceiveKeyEvents(focusElement.current)) return;
if (event.key === " ") { if (event.key === " ") {
spacebarHeld.current = false; spacebarHeld.current = false;
setAudioEnabled?.(false); setAudioEnabled?.(false);
} }
}, },
[focusElement, setAudioEnabled], [setAudioEnabled],
), ),
); );