You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

actions.ts 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { batch } from 'react-redux';
  2. import { AnyAction } from 'redux';
  3. import { ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED, createShortcutEvent } from '../analytics/AnalyticsEvents';
  4. import { sendAnalytics } from '../analytics/functions';
  5. import { IStore } from '../app/types';
  6. import { clickOnVideo } from '../filmstrip/actions.web';
  7. import { openSettingsDialog } from '../settings/actions.web';
  8. import { SETTINGS_TABS } from '../settings/constants';
  9. import { iAmVisitor } from '../visitors/functions';
  10. import {
  11. DISABLE_KEYBOARD_SHORTCUTS,
  12. ENABLE_KEYBOARD_SHORTCUTS,
  13. REGISTER_KEYBOARD_SHORTCUT,
  14. UNREGISTER_KEYBOARD_SHORTCUT
  15. } from './actionTypes';
  16. import { areKeyboardShortcutsEnabled, getKeyboardShortcuts } from './functions';
  17. import logger from './logger';
  18. import { IKeyboardShortcut } from './types';
  19. import { getKeyboardKey, getPriorityFocusedElement } from './utils';
  20. /**
  21. * Action to register a new shortcut.
  22. *
  23. * @param {IKeyboardShortcut} shortcut - The shortcut to register.
  24. * @returns {AnyAction}
  25. */
  26. export const registerShortcut = (shortcut: IKeyboardShortcut): AnyAction => {
  27. return {
  28. type: REGISTER_KEYBOARD_SHORTCUT,
  29. shortcut
  30. };
  31. };
  32. /**
  33. * Action to unregister a shortcut.
  34. *
  35. * @param {string} character - The character of the shortcut to unregister.
  36. * @param {boolean} altKey - Whether the shortcut used altKey.
  37. * @returns {AnyAction}
  38. */
  39. export const unregisterShortcut = (character: string, altKey = false): AnyAction => {
  40. return {
  41. alt: altKey,
  42. type: UNREGISTER_KEYBOARD_SHORTCUT,
  43. character
  44. };
  45. };
  46. /**
  47. * Action to enable keyboard shortcuts.
  48. *
  49. * @returns {AnyAction}
  50. */
  51. export const enableKeyboardShortcuts = (): AnyAction => {
  52. return {
  53. type: ENABLE_KEYBOARD_SHORTCUTS
  54. };
  55. };
  56. /**
  57. * Action to enable keyboard shortcuts.
  58. *
  59. * @returns {AnyAction}
  60. */
  61. export const disableKeyboardShortcuts = (): AnyAction => {
  62. return {
  63. type: DISABLE_KEYBOARD_SHORTCUTS
  64. };
  65. };
  66. type KeyHandler = ((e: KeyboardEvent) => void) | undefined;
  67. let keyDownHandler: KeyHandler;
  68. let keyUpHandler: KeyHandler;
  69. /**
  70. * Initialise global shortcuts.
  71. * Global shortcuts are shortcuts for features that don't have a button or
  72. * link associated with the action. In other words they represent actions
  73. * triggered _only_ with a shortcut.
  74. *
  75. * @param {Function} dispatch - The redux dispatch function.
  76. * @returns {void}
  77. */
  78. function initGlobalKeyboardShortcuts(dispatch: IStore['dispatch']) {
  79. batch(() => {
  80. dispatch(registerShortcut({
  81. character: '?',
  82. helpDescription: 'keyboardShortcuts.toggleShortcuts',
  83. handler: () => {
  84. sendAnalytics(createShortcutEvent('help'));
  85. dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
  86. }
  87. }));
  88. // register SPACE shortcut in two steps to insure visibility of help message
  89. dispatch(registerShortcut({
  90. character: ' ',
  91. helpCharacter: 'SPACE',
  92. helpDescription: 'keyboardShortcuts.pushToTalk',
  93. handler: () => {
  94. // Handled directly on the global handler.
  95. }
  96. }));
  97. dispatch(registerShortcut({
  98. character: '0',
  99. helpDescription: 'keyboardShortcuts.focusLocal',
  100. handler: () => {
  101. dispatch(clickOnVideo(0));
  102. }
  103. }));
  104. for (let num = 1; num < 10; num++) {
  105. dispatch(registerShortcut({
  106. character: `${num}`,
  107. // only show help hint for the first shortcut
  108. helpCharacter: num === 1 ? '1-9' : undefined,
  109. helpDescription: num === 1 ? 'keyboardShortcuts.focusRemote' : undefined,
  110. handler: () => {
  111. dispatch(clickOnVideo(num));
  112. }
  113. }));
  114. }
  115. });
  116. }
  117. /**
  118. * Unregisters global shortcuts.
  119. *
  120. * @param {Function} dispatch - The redux dispatch function.
  121. * @returns {void}
  122. */
  123. function unregisterGlobalKeyboardShortcuts(dispatch: IStore['dispatch']) {
  124. batch(() => {
  125. dispatch(unregisterShortcut('?'));
  126. // register SPACE shortcut in two steps to insure visibility of help message
  127. dispatch(unregisterShortcut(' '));
  128. dispatch(unregisterShortcut('0'));
  129. for (let num = 1; num < 10; num++) {
  130. dispatch(unregisterShortcut(`${num}`));
  131. }
  132. });
  133. }
  134. /**
  135. * Initializes keyboard shortcuts.
  136. *
  137. * @returns {Function}
  138. */
  139. export function initKeyboardShortcuts() {
  140. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  141. initGlobalKeyboardShortcuts(dispatch);
  142. const pttDelay = 50;
  143. let pttTimeout: number | undefined;
  144. // Used to chain the push to talk operations in order to fix an issue when on press we actually need to create
  145. // a new track and the release happens before the track is created. In this scenario the release is ignored.
  146. // The chaining would also prevent creating multiple new tracks if the space bar is pressed and released
  147. // multiple times before the new track creation finish.
  148. // TODO: Revisit the fix once we have better track management in LJM. It is possible that we would not need the
  149. // chaining at all.
  150. let mutePromise = Promise.resolve();
  151. keyUpHandler = (e: KeyboardEvent) => {
  152. const state = getState();
  153. const enabled = areKeyboardShortcutsEnabled(state);
  154. const shortcuts = getKeyboardShortcuts(state);
  155. if (!enabled || getPriorityFocusedElement()) {
  156. return;
  157. }
  158. const key = getKeyboardKey(e).toUpperCase();
  159. if (key === ' ') {
  160. clearTimeout(pttTimeout);
  161. pttTimeout = window.setTimeout(() => {
  162. sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
  163. logger.log('Talk shortcut released');
  164. mutePromise = mutePromise.then(() =>
  165. APP.conference.muteAudio(true).catch(() => { /* nothing to be done */ }));
  166. }, pttDelay);
  167. }
  168. if (shortcuts.has(key)) {
  169. shortcuts.get(key)?.handler(e);
  170. }
  171. };
  172. keyDownHandler = (e: KeyboardEvent) => {
  173. const state = getState();
  174. const enabled = areKeyboardShortcutsEnabled(state);
  175. if (!enabled || iAmVisitor(state)) {
  176. return;
  177. }
  178. const focusedElement = getPriorityFocusedElement();
  179. const key = getKeyboardKey(e).toUpperCase();
  180. if (key === ' ' && !focusedElement) {
  181. clearTimeout(pttTimeout);
  182. sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
  183. logger.log('Talk shortcut pressed');
  184. mutePromise = mutePromise.then(() =>
  185. APP.conference.muteAudio(false).catch(() => { /* nothing to be done */ }));
  186. } else if (key === 'ESCAPE') {
  187. focusedElement?.blur();
  188. }
  189. };
  190. window.addEventListener('keyup', keyUpHandler);
  191. window.addEventListener('keydown', keyDownHandler);
  192. };
  193. }
  194. /**
  195. * Unregisters the global shortcuts and removes the global keyboard listeners.
  196. *
  197. * @returns {Function}
  198. */
  199. export function disposeKeyboardShortcuts() {
  200. return (dispatch: IStore['dispatch']) => {
  201. // The components that are registering shortcut should take care of unregistering them.
  202. unregisterGlobalKeyboardShortcuts(dispatch);
  203. keyUpHandler && window.removeEventListener('keyup', keyUpHandler);
  204. keyDownHandler && window.removeEventListener('keydown', keyDownHandler);
  205. keyDownHandler = keyUpHandler = undefined;
  206. };
  207. }