ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux (#13260)

* ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux

fix unsynced default value between config flag and local storage

* code review

* fix build
pull/13269/head jitsi-meet_8602
Avram Tudor 2 years ago committed by GitHub
parent a9863e65c3
commit 1402a63324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app.js
  2. 6
      conference.js
  3. 6
      globals.d.ts
  4. 261
      modules/keyboardshortcut/keyboardshortcut.js
  5. 1
      react/features/app/middlewares.web.ts
  6. 1
      react/features/app/reducers.web.ts
  7. 2
      react/features/app/types.ts
  8. 16
      react/features/filmstrip/components/web/Filmstrip.tsx
  9. 24
      react/features/keyboard-shortcuts/actionTypes.ts
  10. 60
      react/features/keyboard-shortcuts/actions.any.ts
  11. 1
      react/features/keyboard-shortcuts/actions.native.ts
  12. 119
      react/features/keyboard-shortcuts/actions.web.ts
  13. 31
      react/features/keyboard-shortcuts/functions.ts
  14. 3
      react/features/keyboard-shortcuts/logger.ts
  15. 39
      react/features/keyboard-shortcuts/middleware.ts
  16. 72
      react/features/keyboard-shortcuts/reducer.ts
  17. 23
      react/features/keyboard-shortcuts/types.ts
  18. 75
      react/features/keyboard-shortcuts/utils.ts
  19. 6
      react/features/remote-control/actions.ts
  20. 9
      react/features/settings/actions.web.ts
  21. 10
      react/features/settings/components/web/ShortcutsTab.tsx
  22. 6
      react/features/settings/functions.web.ts
  23. 23
      react/features/toolbox/components/AudioMuteButton.ts
  24. 23
      react/features/toolbox/components/VideoMuteButton.ts
  25. 52
      react/features/toolbox/components/web/Toolbox.tsx

@ -18,7 +18,6 @@ import './react/features/base/jitsi-local-storage/setup';
import conference from './conference';
import API from './modules/API';
import UI from './modules/UI/UI';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import translation from './modules/translation/translation';
// Initialize Olm as early as possible.
@ -38,7 +37,6 @@ window.APP = {
'index.loaded': window.indexLoadedTime
},
keyboardshortcut,
translation,
UI
};

@ -140,6 +140,7 @@ import { downloadJSON } from './react/features/base/util/downloadJSON';
import { showDesktopPicker } from './react/features/desktop-picker/actions';
import { appendSuffix } from './react/features/display-name/functions';
import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedback/actions';
import { initKeyboardShortcuts } from './react/features/keyboard-shortcuts/actions';
import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
import { hideNotification, showNotification, showWarningNotification } from './react/features/notifications/actions';
@ -2308,10 +2309,7 @@ export default {
APP.UI.initConference();
if (!config.disableShortcuts) {
APP.keyboardshortcut.init();
}
dispatch(initKeyboardShortcuts());
dispatch(conferenceJoined(room));
const jwt = APP.store.getState()['features/base/jwt'];

6
globals.d.ts vendored

@ -10,12 +10,6 @@ declare global {
API: any;
conference: any;
debugLogs: any;
keyboardshortcut: {
registerShortcut: Function;
unregisterShortcut: Function;
openDialog: Function;
enable: Function;
}
};
const interfaceConfig: any;

@ -1,261 +0,0 @@
/* global APP */
import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import {
ACTION_SHORTCUT_PRESSED as PRESSED,
ACTION_SHORTCUT_RELEASED as RELEASED,
createShortcutEvent
} from '../../react/features/analytics/AnalyticsEvents';
import { sendAnalytics } from '../../react/features/analytics/functions';
import { clickOnVideo } from '../../react/features/filmstrip/actions';
import { openSettingsDialog } from '../../react/features/settings/actions';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
const logger = Logger.getLogger(__filename);
/**
* Map of shortcuts. When a shortcut is registered it enters the mapping.
* @type {Map}
*/
const _shortcuts = new Map();
/**
* Map of registered keyboard keys and translation keys describing the
* action performed by the key.
* @type {Map}
*/
const _shortcutsHelp = new Map();
/**
* The key used to save in local storage if keyboard shortcuts are enabled.
*/
const _enableShortcutsKey = 'enableShortcuts';
/**
* Prefer keyboard handling of these elements over global shortcuts.
* If a button is triggered using the Spacebar it should not trigger PTT.
* If an input element is focused and M is pressed it should not mute audio.
*/
const _elementsBlacklist = [
'input',
'textarea',
'button',
'[role=button]',
'[role=menuitem]',
'[role=radio]',
'[role=tab]',
'[role=option]',
'[role=switch]',
'[role=range]',
'[role=log]'
];
/**
* An element selector for elements that have their own keyboard handling.
*/
const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
/**
* Maps keycode to character, id of popover for given function and function.
*/
const KeyboardShortcut = {
init() {
this._initGlobalShortcuts();
window.onkeyup = e => {
if (!this.getEnabled()) {
return;
}
const key = this._getKeyboardKey(e).toUpperCase();
const num = parseInt(key, 10);
if (!document.querySelector(_focusedElementsSelector)) {
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.store.dispatch(clickOnVideo(num));
}
}
};
window.onkeydown = e => {
if (!this.getEnabled()) {
return;
}
const focusedElement = document.querySelector(_focusedElementsSelector);
if (!focusedElement) {
if (this._getKeyboardKey(e).toUpperCase() === ' ') {
if (APP.conference.isLocalAudioMuted()) {
sendAnalytics(createShortcutEvent(
'push.to.talk',
PRESSED));
logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false);
}
}
} else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
// Allow to remove focus from selected elements using ESC key.
if (focusedElement && focusedElement.blur) {
focusedElement.blur();
}
}
};
},
/**
* Enables/Disables the keyboard shortcuts.
* @param {boolean} value - the new value.
*/
enable(value) {
jitsiLocalStorage.setItem(_enableShortcutsKey, value);
},
getEnabled() {
// Should be enabled if not explicitly set to false
// eslint-disable-next-line no-unneeded-ternary
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
},
getShortcutsDescriptions() {
return _shortcutsHelp;
},
/**
* Opens the {@SettingsDialog} dialog on the Shortcuts page.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
},
/**
* Registers a new shortcut.
*
* @param shortcutChar the shortcut character triggering the action
* @param shortcutAttr the "shortcut" html element attribute mapping an
* element to this shortcut and used to show the shortcut character on the
* element tooltip
* @param exec the function to be executed when the shortcut is pressed
* @param helpDescription the description of the shortcut that would appear
* in the help menu
* @param altKey whether or not the alt key must be pressed.
*/
registerShortcut(// eslint-disable-line max-params
shortcutChar,
shortcutAttr,
exec,
helpDescription,
altKey = false) {
_shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
character: shortcutChar,
function: exec,
shortcutAttr,
altKey
});
if (helpDescription) {
this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
}
},
/**
* Unregisters a shortcut.
*
* @param shortcutChar unregisters the given shortcut, which means it will
* no longer be usable
* @param altKey whether or not shortcut is combo with alt key
*/
unregisterShortcut(shortcutChar, altKey = false) {
_shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
_shortcutsHelp.delete(shortcutChar);
},
/**
* @param e a KeyboardEvent
* @returns {string} e.key or something close if not supported
*/
_getKeyboardKey(e) {
// If alt is pressed a different char can be returned so this takes
// the char from the code. It also prefixes with a colon to differentiate
// alt combo from simple keypress.
if (e.altKey) {
const key = e.code.replace('Key', '');
return `:${key}`;
}
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof e.key === 'string' && e.key !== 'Unidentified') {
return e.key;
}
if (e.type === 'keypress'
&& ((e.which >= 32 && e.which <= 126)
|| (e.which >= 160 && e.which <= 255))) {
return String.fromCharCode(e.which);
}
// try to fallback (0-9A-Za-z and QWERTY keyboard)
switch (e.which) {
case 27:
return 'Escape';
case 191:
return e.shiftKey ? '?' : '/';
}
if (e.shiftKey || e.type === 'keypress') {
return String.fromCharCode(e.which);
}
return String.fromCharCode(e.which).toLowerCase();
},
/**
* Adds the given shortcut to the help dialog.
*
* @param shortcutChar the shortcut character
* @param shortcutDescriptionKey the description of the shortcut
* @private
*/
_addShortcutToHelp(shortcutChar, shortcutDescriptionKey) {
_shortcutsHelp.set(shortcutChar, shortcutDescriptionKey);
},
/**
* 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.
*/
_initGlobalShortcuts() {
this.registerShortcut('?', null, () => {
sendAnalytics(createShortcutEvent('help'));
this.openDialog();
}, 'keyboardShortcuts.toggleShortcuts');
// register SPACE shortcut in two steps to insure visibility of help
// message
this.registerShortcut(' ', null, () => {
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
/**
* FIXME: Currently focus keys are directly implemented below in
* onkeyup. They should be moved to the SmallVideo instead.
*/
this._addShortcutToHelp('0', 'keyboardShortcuts.focusLocal');
this._addShortcutToHelp('1-9', 'keyboardShortcuts.focusRemote');
}
};
export default KeyboardShortcut;

@ -5,6 +5,7 @@ import '../base/media/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';
import '../no-audio-signal/middleware';
import '../notifications/middleware';
import '../noise-detection/middleware';

@ -3,6 +3,7 @@ import '../base/tooltip/reducer';
import '../e2ee/reducer';
import '../face-landmarks/reducer';
import '../feedback/reducer';
import '../keyboard-shortcuts/reducer';
import '../no-audio-signal/reducer';
import '../noise-detection/reducer';
import '../participants-pane/reducer';

@ -43,6 +43,7 @@ import { IGifsState } from '../gifs/reducer';
import { IGoogleApiState } from '../google-api/reducer';
import { IInviteState } from '../invite/reducer';
import { IJaaSState } from '../jaas/reducer';
import { IKeyboardShortcutsState } from '../keyboard-shortcuts/types';
import { ILargeVideoState } from '../large-video/reducer';
import { ILobbyState } from '../lobby/reducer';
import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
@ -133,6 +134,7 @@ export interface IReduxState {
'features/google-api': IGoogleApiState;
'features/invite': IInviteState;
'features/jaas': IJaaSState;
'features/keyboard-shortcuts': IKeyboardShortcutsState;
'features/large-video': ILargeVideoState;
'features/lobby': ILobbyState;
'features/mobile/audio-mode': IMobileAudioModeState;

@ -15,6 +15,7 @@ import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS } from '../../../video-layout/constants';
@ -295,12 +296,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
* @inheritdoc
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
this.props.dispatch(registerShortcut({
character: 'F',
helpDescription: 'keyboardShortcuts.toggleFilmstrip',
handler: this._onShortcutToggleFilmstrip
}));
document.addEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore
@ -313,7 +314,8 @@ class Filmstrip extends PureComponent <IProps, IState> {
* @inheritdoc
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('F');
this.props.dispatch(unregisterShortcut('F'));
document.removeEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore

@ -0,0 +1,24 @@
/**
* The type of the action which signals that the keyboard shortcuts should be initialized.
*/
export const INIT_KEYBOARD_SHORTCUTS = 'INIT_KEYBOARD_SHORTCUTS';
/**
* The type of the action which signals that a keyboard shortcut should be registered.
*/
export const REGISTER_KEYBOARD_SHORTCUT = 'REGISTER_KEYBOARD_SHORTCUT';
/**
* The type of the action which signals that a keyboard shortcut should be unregistered.
*/
export const UNREGISTER_KEYBOARD_SHORTCUT = 'UNREGISTER_KEYBOARD_SHORTCUT';
/**
* The type of the action which signals that a keyboard shortcut should be enabled.
*/
export const ENABLE_KEYBOARD_SHORTCUTS = 'ENABLE_KEYBOARD_SHORTCUTS';
/**
* The type of the action which signals that a keyboard shortcut should be disabled.
*/
export const DISABLE_KEYBOARD_SHORTCUTS = 'DISABLE_KEYBOARD_SHORTCUTS';

@ -0,0 +1,60 @@
import { AnyAction } from 'redux';
import {
DISABLE_KEYBOARD_SHORTCUTS,
ENABLE_KEYBOARD_SHORTCUTS,
REGISTER_KEYBOARD_SHORTCUT,
UNREGISTER_KEYBOARD_SHORTCUT
} from './actionTypes';
import { IKeyboardShortcut } from './types';
/**
* 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.
* @returns {AnyAction}
*/
export const unregisterShortcut = (character: string, altKey = false): AnyAction => {
return {
altKey,
type: UNREGISTER_KEYBOARD_SHORTCUT,
character
};
};
/**
* 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
};
};

@ -0,0 +1 @@
export * from './actions.any';

@ -0,0 +1,119 @@
import { batch } from 'react-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 { registerShortcut } from './actions.any';
import { areKeyboardShortcutsEnabled, getKeyboardShortcuts } from './functions';
import logger from './logger';
import { getKeyboardKey, getPriorityFocusedElement } from './utils';
export * from './actions.any';
/**
* 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.
*
* @returns {Function}
*/
const 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: () => {
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
}
}));
dispatch(registerShortcut({
character: '0',
helpDescription: 'keyboardShortcuts.focusLocal',
handler: () => {
dispatch(clickOnVideo(0));
}
}));
Array(9).fill(1)
.forEach((_, index) => {
const num = index + 1;
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));
}
}));
});
});
};
/**
* Initializes keyboard shortcuts.
*
* @returns {Function}
*/
export const initKeyboardShortcuts = () =>
(dispatch: IStore['dispatch'], getState: IStore['getState']) => {
dispatch(initGlobalKeyboardShortcuts());
window.onkeyup = (e: KeyboardEvent) => {
const state = getState();
const enabled = areKeyboardShortcutsEnabled(state);
const shortcuts = getKeyboardShortcuts(state);
if (!enabled || getPriorityFocusedElement()) {
return;
}
const key = getKeyboardKey(e).toUpperCase();
if (shortcuts.has(key)) {
shortcuts.get(key)?.handler(e);
}
};
window.onkeydown = (e: KeyboardEvent) => {
const state = getState();
const enabled = areKeyboardShortcutsEnabled(state);
if (!enabled) {
return;
}
const focusedElement = getPriorityFocusedElement();
const key = getKeyboardKey(e).toUpperCase();
if (key === ' ' && !focusedElement) {
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false);
} else if (key === 'ESCAPE') {
focusedElement?.blur();
}
};
};

@ -0,0 +1,31 @@
import { IReduxState } from '../app/types';
/**
* Returns whether or not the keyboard shortcuts are enabled.
*
* @param {Object} state - The redux state.
* @returns {boolean} - Whether or not the keyboard shortcuts are enabled.
*/
export function areKeyboardShortcutsEnabled(state: IReduxState) {
return state['features/keyboard-shortcuts'].enabled;
}
/**
* Returns the keyboard shortcuts map.
*
* @param {Object} state - The redux state.
* @returns {Map} - The keyboard shortcuts map.
*/
export function getKeyboardShortcuts(state: IReduxState) {
return state['features/keyboard-shortcuts'].shortcuts;
}
/**
* Returns the keyboard shortcuts help descriptions.
*
* @param {Object} state - The redux state.
* @returns {Map} - The keyboard shortcuts help descriptions.
*/
export function getKeyboardShortcutsHelpDescriptions(state: IReduxState) {
return state['features/keyboard-shortcuts'].shortcutsHelp;
}

@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/keyboard-shortcuts');

@ -0,0 +1,39 @@
import { IStore } from '../app/types';
import { SET_CONFIG } from '../base/config/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { CAPTURE_EVENTS } from '../remote-control/actionTypes';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from './actions';
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch } = store;
switch (action.type) {
case CAPTURE_EVENTS:
if (action.isCapturingEvents) {
dispatch(disableKeyboardShortcuts());
} else {
dispatch(enableKeyboardShortcuts());
}
return next(action);
case SET_CONFIG: {
const result = next(action);
const state = store.getState();
const { disableShortcuts } = state['features/base/config'];
if (disableShortcuts !== undefined) {
if (disableShortcuts) {
dispatch(disableKeyboardShortcuts());
} else {
dispatch(enableKeyboardShortcuts());
}
}
return result;
}
}
return next(action);
});

@ -0,0 +1,72 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
DISABLE_KEYBOARD_SHORTCUTS,
ENABLE_KEYBOARD_SHORTCUTS,
REGISTER_KEYBOARD_SHORTCUT,
UNREGISTER_KEYBOARD_SHORTCUT
} from './actionTypes';
import { IKeyboardShortcutsState } from './types';
/**
* The redux subtree of this feature.
*/
const STORE_NAME = 'features/keyboard-shortcuts';
const defaultState = {
enabled: true,
shortcuts: new Map(),
shortcutsHelp: new Map()
};
PersistenceRegistry.register(STORE_NAME, {
enabled: true
});
ReducerRegistry.register<IKeyboardShortcutsState>(STORE_NAME,
(state = defaultState, action): IKeyboardShortcutsState => {
switch (action.type) {
case ENABLE_KEYBOARD_SHORTCUTS:
return {
...state,
enabled: true
};
case DISABLE_KEYBOARD_SHORTCUTS:
return {
...state,
enabled: false
};
case REGISTER_KEYBOARD_SHORTCUT: {
const shortcutKey = action.shortcut.alt ? `:${action.shortcut.character}` : action.shortcut.character;
return {
...state,
shortcuts: new Map(state.shortcuts)
.set(shortcutKey, action.shortcut),
shortcutsHelp: action.shortcut.helpDescription
? new Map(state.shortcutsHelp)
.set(action.shortcut.helpCharacter ?? shortcutKey, action.shortcut.helpDescription)
: state.shortcutsHelp
};
}
case UNREGISTER_KEYBOARD_SHORTCUT: {
const shortcutKey = action.alt ? `:${action.character}` : action.character;
const shortcuts = new Map(state.shortcuts);
shortcuts.delete(shortcutKey);
const shortcutsHelp = new Map(state.shortcutsHelp);
shortcutsHelp.delete(shortcutKey);
return {
...state,
shortcuts,
shortcutsHelp
};
}
}
return state;
});

@ -0,0 +1,23 @@
export interface IKeyboardShortcut {
// whether or not the alt key must be pressed
alt?: boolean;
// the character to be pressed that triggers the action
character: string;
// the function to be executed when the shortcut is pressed
handler: Function;
// character to be displayed in the help dialog shortcuts list
helpCharacter?: string;
// help description of the shortcut, to be displayed in the help dialog
helpDescription?: string;
}
export interface IKeyboardShortcutsState {
enabled: boolean;
shortcuts: Map<string, IKeyboardShortcut>;
shortcutsHelp: Map<string, string>;
}

@ -0,0 +1,75 @@
/**
* Prefer keyboard handling of these elements over global shortcuts.
* If a button is triggered using the Spacebar it should not trigger PTT.
* If an input element is focused and M is pressed it should not mute audio.
*/
const _elementsBlacklist = [
'input',
'textarea',
'button',
'[role=button]',
'[role=menuitem]',
'[role=radio]',
'[role=tab]',
'[role=option]',
'[role=switch]',
'[role=range]',
'[role=log]'
];
/**
* Returns the currently focused element if it is not blacklisted.
*
* @returns {HTMLElement|null} - The currently focused element.
*/
export const getPriorityFocusedElement = (): HTMLElement | null =>
document.querySelector(`:focus:is(${_elementsBlacklist.join(',')})`);
/**
* Returns the keyboard key from a KeyboardEvent.
*
* @param {KeyboardEvent} e - The KeyboardEvent.
* @returns {string} - The keyboard key.
*/
export const getKeyboardKey = (e: KeyboardEvent): string => {
// @ts-ignore
const { altKey, code, key, shiftKey, type, which } = e;
// If alt is pressed a different char can be returned so this takes
// the char from the code. It also prefixes with a colon to differentiate
// alt combo from simple keypress.
if (altKey) {
const replacedKey = code.replace('Key', '');
return `:${replacedKey}`;
}
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof key === 'string' && key !== 'Unidentified') {
return key;
}
if (type === 'keypress'
&& ((which >= 32 && which <= 126)
|| (which >= 160 && which <= 255))) {
return String.fromCharCode(which);
}
// try to fallback (0-9A-Za-z and QWERTY keyboard)
switch (which) {
case 27:
return 'Escape';
case 191:
return shiftKey ? '?' : '/';
}
if (shiftKey || type === 'keypress') {
return String.fromCharCode(which);
}
return String.fromCharCode(which).toLowerCase();
};

@ -698,9 +698,6 @@ export function resume() {
logger.log('Resuming remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(false);
area.mousemove((event: React.MouseEvent) => {
dispatch(mouseMoved(event));
});
@ -747,9 +744,6 @@ export function pause() {
logger.log('Pausing remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(true);
const area = getRemoteConrolEventCaptureArea();
if (area) {

@ -1,7 +1,5 @@
import { batch } from 'react-redux';
// @ts-expect-error
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
import { IStore } from '../app/types';
import {
setFollowMe,
@ -11,6 +9,7 @@ import {
import { openDialog } from '../base/dialog/actions';
import i18next from '../base/i18n/i18next';
import { updateSettings } from '../base/settings/actions';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions';
import { toggleBackgroundEffect } from '../virtual-background/actions';
import virtualBackgroundLogger from '../virtual-background/logger';
@ -247,7 +246,11 @@ export function submitShortcutsTab(newState: any) {
const currentState = getShortcutsTabProps(getState());
if (newState.keyboardShortcutsEnabled !== currentState.keyboardShortcutsEnabled) {
keyboardShortcut.enable(newState.keyboardShortcutsEnabled);
if (newState.keyboardShortcutsEnabled) {
dispatch(enableKeyboardShortcuts());
} else {
dispatch(disableKeyboardShortcuts());
}
}
};
}

@ -3,8 +3,6 @@ import { withStyles } from '@mui/styles';
import React from 'react';
import { WithTranslation } from 'react-i18next';
// @ts-expect-error
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
import AbstractDialogTab, {
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../../base/i18n/functions';
@ -30,6 +28,11 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
* Wether the keyboard shortcuts are enabled or not.
*/
keyboardShortcutsEnabled: boolean;
/**
* The keyboard shortcuts descriptions.
*/
keyboardShortcutsHelpDescriptions: Map<string, string>;
}
const styles = (theme: Theme) => {
@ -145,11 +148,12 @@ class ShortcutsTab extends AbstractDialogTab<IProps, any> {
const {
classes,
displayShortcuts,
keyboardShortcutsHelpDescriptions,
keyboardShortcutsEnabled,
t
} = this.props;
const shortcutDescriptions: Map<string, string> = displayShortcuts
? keyboardShortcut.getShortcutsDescriptions()
? keyboardShortcutsHelpDescriptions
: new Map();
return (

@ -1,8 +1,7 @@
// @ts-expect-error
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
import { IStateful } from '../base/app/types';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { toState } from '../base/redux/functions';
import { areKeyboardShortcutsEnabled, getKeyboardShortcutsHelpDescriptions } from '../keyboard-shortcuts/functions';
import { isPrejoinPageVisible } from '../prejoin/functions';
export * from './functions.any';
@ -84,6 +83,7 @@ export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePa
return {
displayShortcuts: !isDisplayedOnWelcomePage && !isPrejoinPageVisible(state),
keyboardShortcutsEnabled: keyboardShortcut.getEnabled()
keyboardShortcutsEnabled: areKeyboardShortcutsEnabled(state),
keyboardShortcutsHelpDescriptions: getKeyboardShortcutsHelpDescriptions(state)
};
}

@ -10,6 +10,7 @@ import { MEDIA_TYPE } from '../../base/media/constants';
import AbstractAudioMuteButton from '../../base/toolbox/components/AbstractAudioMuteButton';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { registerShortcut, unregisterShortcut } from '../../keyboard-shortcuts/actions';
import { muteLocal } from '../../video-menu/actions';
import { isAudioMuteButtonDisabled } from '../functions';
@ -61,12 +62,15 @@ class AudioMuteButton extends AbstractAudioMuteButton<IProps> {
* @returns {void}
*/
componentDidMount() {
typeof APP === 'undefined'
|| APP.keyboardshortcut.registerShortcut(
'M',
null,
this._onKeyboardShortcut,
'keyboardShortcuts.mute');
if (typeof APP === 'undefined') {
return;
}
this.props.dispatch(registerShortcut({
character: 'M',
helpDescription: 'keyboardShortcuts.mute',
handler: this._onKeyboardShortcut
}));
}
/**
@ -76,8 +80,11 @@ class AudioMuteButton extends AbstractAudioMuteButton<IProps> {
* @returns {void}
*/
componentWillUnmount() {
typeof APP === 'undefined'
|| APP.keyboardshortcut.unregisterShortcut('M');
if (typeof APP === 'undefined') {
return;
}
this.props.dispatch(unregisterShortcut('M'));
}
/**

@ -10,6 +10,7 @@ import { MEDIA_TYPE } from '../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import AbstractVideoMuteButton from '../../base/toolbox/components/AbstractVideoMuteButton';
import { isLocalTrackMuted } from '../../base/tracks/functions';
import { registerShortcut, unregisterShortcut } from '../../keyboard-shortcuts/actions';
import { handleToggleVideoMuted } from '../actions.any';
import { isVideoMuteButtonDisabled } from '../functions';
@ -62,12 +63,15 @@ class VideoMuteButton extends AbstractVideoMuteButton<IProps> {
* @returns {void}
*/
componentDidMount() {
typeof APP === 'undefined'
|| APP.keyboardshortcut.registerShortcut(
'V',
null,
this._onKeyboardShortcut,
'keyboardShortcuts.videoMute');
if (typeof APP === 'undefined') {
return;
}
this.props.dispatch(registerShortcut({
character: 'V',
helpDescription: 'keyboardShortcuts.videoMute',
handler: this._onKeyboardShortcut
}));
}
/**
@ -77,8 +81,11 @@ class VideoMuteButton extends AbstractVideoMuteButton<IProps> {
* @returns {void}
*/
componentWillUnmount() {
typeof APP === 'undefined'
|| APP.keyboardshortcut.unregisterShortcut('V');
if (typeof APP === 'undefined') {
return;
}
this.props.dispatch(unregisterShortcut('V'));
}
/**

@ -3,8 +3,6 @@ import React, { Component, RefObject } from 'react';
import { WithTranslation } from 'react-i18next';
import { batch, connect } from 'react-redux';
// @ts-expect-error
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
import { isSpeakerStatsDisabled } from '../../../../features/speaker-stats/functions';
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
@ -42,7 +40,9 @@ import { setGifMenuVisibility } from '../../../gifs/actions';
import { isGifEnabled } from '../../../gifs/functions.web';
import InviteButton from '../../../invite/components/add-people-dialog/web/InviteButton';
import { isVpaasMeeting } from '../../../jaas/functions';
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
import KeyboardShortcutsButton from '../../../keyboard-shortcuts/components/web/KeyboardShortcutsButton';
import { areKeyboardShortcutsEnabled } from '../../../keyboard-shortcuts/functions';
import NoiseSuppressionButton from '../../../noise-suppression/components/NoiseSuppressionButton';
import {
close as closeParticipantsPane,
@ -288,6 +288,11 @@ interface IProps extends WithTranslation {
*/
_sharingVideo?: boolean;
/**
* Whether or not the shortcut buttons are enabled.
*/
_shortcutsEnabled: boolean;
/**
* Whether or not the tile view is enabled.
*/
@ -450,11 +455,11 @@ class Toolbox extends Component<IProps> {
KEYBOARD_SHORTCUTS.forEach(shortcut => {
if (typeof shortcut === 'object') {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription);
dispatch(registerShortcut({
character: shortcut.character,
handler: shortcut.exec,
helpDescription: shortcut.helpDescription
}));
}
});
@ -476,12 +481,12 @@ class Toolbox extends Component<IProps> {
});
REACTION_SHORTCUTS.forEach(shortcut => {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription,
shortcut.altKey);
dispatch(registerShortcut({
alt: shortcut.altKey,
character: shortcut.character,
handler: shortcut.exec,
helpDescription: shortcut.helpDescription
}));
});
if (_gifsEnabled) {
@ -492,12 +497,11 @@ class Toolbox extends Component<IProps> {
});
};
APP.keyboardshortcut.registerShortcut(
'G',
null,
onGifShortcut,
t('keyboardShortcuts.giphyMenu')
);
dispatch(registerShortcut({
character: 'G',
handler: onGifShortcut,
helpDescription: 'keyboardShortcuts.giphyMenu'
}));
}
}
}
@ -538,13 +542,15 @@ class Toolbox extends Component<IProps> {
* @returns {void}
*/
componentWillUnmount() {
const { dispatch } = this.props;
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter));
dispatch(unregisterShortcut(letter)));
if (this.props._reactionsEnabled) {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter, true));
dispatch(unregisterShortcut(letter, true)));
}
}
@ -713,6 +719,7 @@ class Toolbox extends Component<IProps> {
_multiStreamModeEnabled,
_reactionsEnabled,
_screenSharing,
_shortcutsEnabled,
_whiteboardEnabled
} = this.props;
@ -874,7 +881,7 @@ class Toolbox extends Component<IProps> {
group: 4
};
const shortcuts = !_isMobile && keyboardShortcut.getEnabled() && {
const shortcuts = !_isMobile && _shortcutsEnabled && {
key: 'shortcuts',
Content: KeyboardShortcutsButton,
group: 4
@ -1585,6 +1592,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_raisedHand: hasRaisedHand(localParticipant),
_reactionsEnabled: isReactionsEnabled(state),
_screenSharing: isScreenVideoShared(state),
_shortcutsEnabled: areKeyboardShortcutsEnabled(state),
_tileViewEnabled: shouldDisplayTileView(state),
_toolbarButtons: toolbarButtons,
_virtualSource: state['features/virtual-background'].virtualSource,

Loading…
Cancel
Save