2023-01-12 17:31:19 +00:00
/*
2024-09-06 10:22:13 +02:00
Copyright 2022-2024 New Vector Ltd.
2023-01-12 17:31:19 +00:00
2025-02-18 17:59:58 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-06 10:22:13 +02:00
Please see LICENSE in the repository root for full details.
2023-01-12 17:31:19 +00:00
*/
2026-04-27 18:15:29 +02:00
import { useCallback , useMemo , useRef } from "react" ;
2026-04-27 14:20:13 +02:00
import { logger } from "matrix-js-sdk/lib/logger" ;
2023-01-12 17:31:19 +00:00
import { useEventTarget } from "./useEvents" ;
2024-12-11 09:27:55 +00:00
import {
type ReactionOption ,
ReactionSet ,
ReactionsRowSize ,
} from "./reactions" ;
2023-01-12 17:31:19 +00:00
2023-04-19 15:51:44 -04:00
/**
* Determines whether focus is in the same part of the tree as the given
2024-07-24 18:42:21 -04:00
* element (specifically, if the element or an ancestor of it is focused).
2023-04-19 15:51:44 -04:00
*/
2026-04-27 14:20:13 +02:00
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 ;
2026-04-28 13:49:36 +02:00
logger . warn (
`[mayReceiveKeyEvents] nothingInFocus ${ nothingInFocus } , focusOnBody ${ focusOnBody } , noPrimaryFocus ${ noPrimaryFocus } ` ,
);
2026-04-27 14:20:13 +02:00
// Only if we do not have a primary focus we allow keyboard shortcut events.
return noPrimaryFocus ;
2023-04-19 15:54:39 -04:00
};
2023-04-19 15:51:44 -04:00
2026-04-27 18:15:29 +02:00
/**
* Only do push to talk behavior if the active element is not a button or button like.
*/
const mayReceiveSpaceKeyEvents = () : boolean => {
const activeElement = document . activeElement ;
if ( activeElement === null ) return true ;
return activeElement . tagName . toLowerCase () !== "button" ;
};
2024-11-19 16:57:57 +00:00
const KeyToReactionMap : Record < string , ReactionOption > = Object . fromEntries (
ReactionSet . slice ( 0 , ReactionsRowSize ). map (( r , i ) => [( i + 1 ). toString (), r ]),
);
2026-04-27 14:20:13 +02:00
/**
* 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.
2026-05-04 18:25:04 +02:00
*
* Note: This function incorrectly assumes that there is a camera and microphone, which is not always the case.
2026-04-27 14:20:13 +02:00
*/
2026-05-04 18:25:04 +02:00
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
2023-01-13 11:52:40 +00:00
export function useCallViewKeyboardShortcuts (
2025-08-29 18:46:24 +02:00
toggleAudio : (() => void ) | null ,
toggleVideo : (() => void ) | null ,
setAudioEnabled : (( enabled : boolean ) => void ) | null ,
2026-04-27 14:20:13 +02:00
sendReaction : (( reaction : ReactionOption ) => void ) | null ,
toggleHandRaised : (() => void ) | null ,
2023-09-22 18:05:13 -04:00
) : void {
2023-04-19 15:17:32 -04:00
const spacebarHeld = useRef ( false );
2023-01-12 17:31:19 +00:00
2023-04-19 15:51:44 -04:00
// These event handlers are set on the window because we want users to be able
// to trigger them without going to the trouble of focusing something
2023-01-12 17:31:19 +00:00
useEventTarget (
window ,
"keydown" ,
useCallback (
( event : KeyboardEvent ) => {
2026-04-27 14:20:13 +02:00
logger . info ( "Keydown event" , event );
if ( ! mayReceiveKeyEvents ()) return ;
2024-11-22 13:17:05 -05:00
if ( event . altKey || event . ctrlKey || event . metaKey || event . shiftKey )
return ;
2023-01-12 17:31:19 +00:00
if ( event . key === "m" ) {
2024-07-24 18:42:21 -04:00
event . preventDefault ();
2025-08-29 18:46:24 +02:00
toggleAudio ? .();
} else if ( event . key === "v" ) {
2024-07-24 18:42:21 -04:00
event . preventDefault ();
2025-08-29 18:46:24 +02:00
toggleVideo ? .();
2026-04-27 18:15:29 +02:00
} else if ( event . key === " " && mayReceiveSpaceKeyEvents ()) {
2024-07-24 18:42:21 -04:00
event . preventDefault ();
if ( ! spacebarHeld . current ) {
spacebarHeld . current = true ;
2025-08-29 18:46:24 +02:00
setAudioEnabled ? .( true );
2024-07-24 18:42:21 -04:00
}
2024-11-19 16:57:57 +00:00
} else if ( event . key === "h" ) {
event . preventDefault ();
2026-04-27 14:20:13 +02:00
toggleHandRaised ? .();
2024-11-19 16:57:57 +00:00
} else if ( KeyToReactionMap [ event . key ]) {
event . preventDefault ();
2026-04-27 14:20:13 +02:00
sendReaction ? .( KeyToReactionMap [ event . key ]);
2026-04-22 11:51:33 +02:00
} else if ( event . key === "Escape" ) {
2026-04-28 13:49:36 +02:00
logger . info ( "Escape key pressed, triggering onBackButtonPressed" );
2026-04-22 11:51:33 +02:00
window . controls . onBackButtonPressed ? .();
2023-01-12 17:31:19 +00:00
}
},
2023-04-19 15:55:55 -04:00
[
2025-08-29 18:46:24 +02:00
toggleVideo ,
toggleAudio ,
setAudioEnabled ,
2024-11-19 16:57:57 +00:00
sendReaction ,
toggleHandRaised ,
2023-10-11 10:42:04 -04:00
],
),
2024-07-24 18:42:21 -04:00
// Because this is set on the window, to prevent shortcuts from activating
// another event callback at the same time, we need to preventDefault
// *before* child elements receive the event by using capture mode
useMemo (() => ({ capture : true }), []),
2023-01-12 17:31:19 +00:00
);
useEventTarget (
window ,
"keyup" ,
useCallback (
( event : KeyboardEvent ) => {
2026-04-27 18:15:29 +02:00
if ( ! mayReceiveKeyEvents () || ! mayReceiveSpaceKeyEvents ()) return ;
2023-01-12 17:31:19 +00:00
if ( event . key === " " ) {
2023-04-19 15:17:32 -04:00
spacebarHeld . current = false ;
2025-08-29 18:46:24 +02:00
setAudioEnabled ? .( false );
2023-01-12 17:31:19 +00:00
}
},
2026-04-27 14:20:13 +02:00
[ setAudioEnabled ],
2023-10-11 10:42:04 -04:00
),
2023-01-12 17:31:19 +00:00
);
useEventTarget (
window ,
"blur" ,
useCallback (() => {
2023-04-21 10:18:43 +01:00
if ( spacebarHeld . current ) {
2023-04-19 15:17:32 -04:00
spacebarHeld . current = false ;
2025-08-29 18:46:24 +02:00
setAudioEnabled ? .( true );
2023-01-12 17:31:19 +00:00
}
2025-08-29 18:46:24 +02:00
}, [ setAudioEnabled , spacebarHeld ]),
2023-01-12 17:31:19 +00:00
);
}