snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
256
react/features/keyboard-shortcuts/actions.ts
Normal file
256
react/features/keyboard-shortcuts/actions.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { batch } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED, createShortcutEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { clickOnVideo } from '../filmstrip/actions.web';
|
||||
import { openSettingsDialog } from '../settings/actions.web';
|
||||
import { SETTINGS_TABS } from '../settings/constants';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import {
|
||||
DISABLE_KEYBOARD_SHORTCUTS,
|
||||
ENABLE_KEYBOARD_SHORTCUTS,
|
||||
REGISTER_KEYBOARD_SHORTCUT,
|
||||
SET_CTRL_ALT_REACTION_SHORTCUTS_ENABLED,
|
||||
UNREGISTER_KEYBOARD_SHORTCUT
|
||||
} from './actionTypes';
|
||||
import { areKeyboardShortcutsEnabled, getKeyboardShortcuts } from './functions';
|
||||
import logger from './logger';
|
||||
import { IKeyboardShortcut } from './types';
|
||||
import { getKeyboardKey, getPriorityFocusedElement } from './utils';
|
||||
|
||||
/**
|
||||
* Action to register a new shortcut.
|
||||
*
|
||||
* @param {IKeyboardShortcut} shortcut - The shortcut to register.
|
||||
* @returns {AnyAction}
|
||||
*/
|
||||
export const registerShortcut = (shortcut: IKeyboardShortcut): AnyAction => {
|
||||
return {
|
||||
type: REGISTER_KEYBOARD_SHORTCUT,
|
||||
shortcut
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to unregister a shortcut.
|
||||
*
|
||||
* @param {string} character - The character of the shortcut to unregister.
|
||||
* @param {boolean} altKey - Whether the shortcut used altKey.
|
||||
* @param {boolean} ctrlKey - Whether the shortcut used ctrlKey.
|
||||
* @returns {AnyAction}
|
||||
*/
|
||||
export const unregisterShortcut = (character: string, altKey = false, ctrlKey = false): AnyAction => {
|
||||
return {
|
||||
alt: altKey,
|
||||
ctrl: ctrlKey,
|
||||
type: UNREGISTER_KEYBOARD_SHORTCUT,
|
||||
character
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets whether Ctrl+Alt aliases for reaction shortcuts are enabled.
|
||||
*
|
||||
* @param {boolean} enabled - Whether the aliases should be enabled.
|
||||
* @returns {AnyAction}
|
||||
*/
|
||||
export const setCtrlAltReactionShortcutsEnabled = (enabled: boolean): AnyAction => {
|
||||
return {
|
||||
type: SET_CTRL_ALT_REACTION_SHORTCUTS_ENABLED,
|
||||
enabled
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to enable keyboard shortcuts.
|
||||
*
|
||||
* @returns {AnyAction}
|
||||
*/
|
||||
export const enableKeyboardShortcuts = (): AnyAction => {
|
||||
return {
|
||||
type: ENABLE_KEYBOARD_SHORTCUTS
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Action to enable keyboard shortcuts.
|
||||
*
|
||||
* @returns {AnyAction}
|
||||
*/
|
||||
export const disableKeyboardShortcuts = (): AnyAction => {
|
||||
return {
|
||||
type: DISABLE_KEYBOARD_SHORTCUTS
|
||||
};
|
||||
};
|
||||
|
||||
type KeyHandler = ((e: KeyboardEvent) => void) | undefined;
|
||||
|
||||
let keyDownHandler: KeyHandler;
|
||||
let keyUpHandler: KeyHandler;
|
||||
|
||||
/**
|
||||
* Initialise global shortcuts.
|
||||
* Global shortcuts are shortcuts for features that don't have a button or
|
||||
* link associated with the action. In other words they represent actions
|
||||
* triggered _only_ with a shortcut.
|
||||
*
|
||||
* @param {Function} dispatch - The redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function initGlobalKeyboardShortcuts(dispatch: IStore['dispatch']) {
|
||||
batch(() => {
|
||||
dispatch(registerShortcut({
|
||||
character: '?',
|
||||
helpDescription: 'keyboardShortcuts.toggleShortcuts',
|
||||
handler: () => {
|
||||
sendAnalytics(createShortcutEvent('help'));
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
|
||||
}
|
||||
}));
|
||||
|
||||
// register SPACE shortcut in two steps to insure visibility of help message
|
||||
dispatch(registerShortcut({
|
||||
character: ' ',
|
||||
helpCharacter: 'SPACE',
|
||||
helpDescription: 'keyboardShortcuts.pushToTalk',
|
||||
handler: () => {
|
||||
// Handled directly on the global handler.
|
||||
}
|
||||
}));
|
||||
|
||||
dispatch(registerShortcut({
|
||||
character: '0',
|
||||
helpDescription: 'keyboardShortcuts.focusLocal',
|
||||
handler: () => {
|
||||
dispatch(clickOnVideo(0));
|
||||
}
|
||||
}));
|
||||
|
||||
for (let num = 1; num < 10; num++) {
|
||||
dispatch(registerShortcut({
|
||||
character: `${num}`,
|
||||
|
||||
// only show help hint for the first shortcut
|
||||
helpCharacter: num === 1 ? '1-9' : undefined,
|
||||
helpDescription: num === 1 ? 'keyboardShortcuts.focusRemote' : undefined,
|
||||
handler: () => {
|
||||
dispatch(clickOnVideo(num));
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters global shortcuts.
|
||||
*
|
||||
* @param {Function} dispatch - The redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function unregisterGlobalKeyboardShortcuts(dispatch: IStore['dispatch']) {
|
||||
batch(() => {
|
||||
dispatch(unregisterShortcut('?'));
|
||||
|
||||
// register SPACE shortcut in two steps to insure visibility of help message
|
||||
dispatch(unregisterShortcut(' '));
|
||||
|
||||
dispatch(unregisterShortcut('0'));
|
||||
|
||||
for (let num = 1; num < 10; num++) {
|
||||
dispatch(unregisterShortcut(`${num}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes keyboard shortcuts.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function initKeyboardShortcuts() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
initGlobalKeyboardShortcuts(dispatch);
|
||||
|
||||
const pttDelay = 50;
|
||||
let pttTimeout: number | undefined;
|
||||
|
||||
// Used to chain the push to talk operations in order to fix an issue when on press we actually need to create
|
||||
// a new track and the release happens before the track is created. In this scenario the release is ignored.
|
||||
// The chaining would also prevent creating multiple new tracks if the space bar is pressed and released
|
||||
// multiple times before the new track creation finish.
|
||||
// TODO: Revisit the fix once we have better track management in LJM. It is possible that we would not need the
|
||||
// chaining at all.
|
||||
let mutePromise = Promise.resolve();
|
||||
|
||||
keyUpHandler = (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
const shortcuts = getKeyboardShortcuts(state);
|
||||
|
||||
if (!enabled || getPriorityFocusedElement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ') {
|
||||
clearTimeout(pttTimeout);
|
||||
pttTimeout = window.setTimeout(() => {
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
|
||||
logger.log('Talk shortcut released');
|
||||
mutePromise = mutePromise.then(() =>
|
||||
APP.conference.muteAudio(true).catch(() => { /* nothing to be done */ }));
|
||||
}, pttDelay);
|
||||
}
|
||||
|
||||
if (shortcuts.has(key)) {
|
||||
shortcuts.get(key)?.handler(e);
|
||||
}
|
||||
};
|
||||
|
||||
keyDownHandler = (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
|
||||
if (!enabled || iAmVisitor(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedElement = getPriorityFocusedElement();
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ' && !focusedElement) {
|
||||
clearTimeout(pttTimeout);
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
|
||||
logger.log('Talk shortcut pressed');
|
||||
mutePromise = mutePromise.then(() =>
|
||||
APP.conference.muteAudio(false).catch(() => { /* nothing to be done */ }));
|
||||
} else if (key === 'ESCAPE') {
|
||||
focusedElement?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keyup', keyUpHandler);
|
||||
window.addEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the global shortcuts and removes the global keyboard listeners.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function disposeKeyboardShortcuts() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// The components that are registering shortcut should take care of unregistering them.
|
||||
unregisterGlobalKeyboardShortcuts(dispatch);
|
||||
|
||||
keyUpHandler && window.removeEventListener('keyup', keyUpHandler);
|
||||
keyDownHandler && window.removeEventListener('keydown', keyDownHandler);
|
||||
keyDownHandler = keyUpHandler = undefined;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user