Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

hooks.web.ts 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. import { useEffect } from 'react';
  2. import { batch, useDispatch, useSelector } from 'react-redux';
  3. import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent } from '../analytics/AnalyticsEvents';
  4. import { sendAnalytics } from '../analytics/functions';
  5. import { IReduxState } from '../app/types';
  6. import { toggleDialog } from '../base/dialog/actions';
  7. import { isIosMobileBrowser, isIpadMobileBrowser } from '../base/environment/utils';
  8. import { HELP_BUTTON_ENABLED } from '../base/flags/constants';
  9. import { getFeatureFlag } from '../base/flags/functions';
  10. import JitsiMeetJS from '../base/lib-jitsi-meet';
  11. import { raiseHand } from '../base/participants/actions';
  12. import { getLocalParticipant, hasRaisedHand } from '../base/participants/functions';
  13. import { isToggleCameraEnabled } from '../base/tracks/functions.web';
  14. import { toggleChat } from '../chat/actions.web';
  15. import ChatButton from '../chat/components/web/ChatButton';
  16. import { useEmbedButton } from '../embed-meeting/hooks';
  17. import { useEtherpadButton } from '../etherpad/hooks';
  18. import { useFeedbackButton } from '../feedback/hooks.web';
  19. import { setGifMenuVisibility } from '../gifs/actions';
  20. import { isGifEnabled } from '../gifs/function.any';
  21. import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
  22. import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions.web';
  23. import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks.web';
  24. import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
  25. import {
  26. close as closeParticipantsPane,
  27. open as openParticipantsPane
  28. } from '../participants-pane/actions.web';
  29. import {
  30. getParticipantsPaneOpen,
  31. isParticipantsPaneEnabled
  32. } from '../participants-pane/functions';
  33. import { useParticipantPaneButton } from '../participants-pane/hooks.web';
  34. import { addReactionToBuffer } from '../reactions/actions.any';
  35. import { toggleReactionsMenuVisibility } from '../reactions/actions.web';
  36. import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
  37. import { REACTIONS } from '../reactions/constants';
  38. import { shouldDisplayReactionsButtons } from '../reactions/functions.any';
  39. import { useReactionsButton } from '../reactions/hooks.web';
  40. import { useLiveStreamingButton, useRecordingButton } from '../recording/hooks.web';
  41. import { isSalesforceEnabled } from '../salesforce/functions';
  42. import { startScreenShareFlow } from '../screen-share/actions.web';
  43. import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
  44. import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
  45. import { useSecurityDialogButton } from '../security/hooks.web';
  46. import SettingsButton from '../settings/components/web/SettingsButton';
  47. import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
  48. import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
  49. import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
  50. import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
  51. import { useClosedCaptionButton } from '../subtitles/hooks.web';
  52. import { toggleTileView } from '../video-layout/actions.any';
  53. import { shouldDisplayTileView } from '../video-layout/functions.web';
  54. import { useTileViewButton } from '../video-layout/hooks';
  55. import VideoQualityButton from '../video-quality/components/VideoQualityButton.web';
  56. import VideoQualityDialog from '../video-quality/components/VideoQualityDialog.web';
  57. import { useVirtualBackgroundButton } from '../virtual-background/hooks';
  58. import { useWhiteboardButton } from '../whiteboard/hooks';
  59. import { setFullScreen } from './actions.web';
  60. import DownloadButton from './components/DownloadButton';
  61. import HelpButton from './components/HelpButton';
  62. import AudioSettingsButton from './components/web/AudioSettingsButton';
  63. import CustomOptionButton from './components/web/CustomOptionButton';
  64. import FullscreenButton from './components/web/FullscreenButton';
  65. import LinkToSalesforceButton from './components/web/LinkToSalesforceButton';
  66. import ProfileButton from './components/web/ProfileButton';
  67. import ShareDesktopButton from './components/web/ShareDesktopButton';
  68. import ToggleCameraButton from './components/web/ToggleCameraButton';
  69. import VideoSettingsButton from './components/web/VideoSettingsButton';
  70. import { isButtonEnabled, isDesktopShareButtonDisabled } from './functions.web';
  71. import { ICustomToolbarButton, IToolboxButton, ToolbarButton } from './types';
  72. const microphone = {
  73. key: 'microphone',
  74. Content: AudioSettingsButton,
  75. group: 0
  76. };
  77. const camera = {
  78. key: 'camera',
  79. Content: VideoSettingsButton,
  80. group: 0
  81. };
  82. const profile = {
  83. key: 'profile',
  84. Content: ProfileButton,
  85. group: 1
  86. };
  87. const chat = {
  88. key: 'chat',
  89. Content: ChatButton,
  90. group: 2
  91. };
  92. const desktop = {
  93. key: 'desktop',
  94. Content: ShareDesktopButton,
  95. group: 2
  96. };
  97. // In Narrow layout and mobile web we are using drawer for popups and that is why it is better to include
  98. // all forms of reactions in the overflow menu. Otherwise the toolbox will be hidden and the reactions popup
  99. // misaligned.
  100. const raisehand = {
  101. key: 'raisehand',
  102. Content: RaiseHandContainerButton,
  103. group: 2
  104. };
  105. const invite = {
  106. key: 'invite',
  107. Content: InviteButton,
  108. group: 2
  109. };
  110. const toggleCamera = {
  111. key: 'toggle-camera',
  112. Content: ToggleCameraButton,
  113. group: 2
  114. };
  115. const videoQuality = {
  116. key: 'videoquality',
  117. Content: VideoQualityButton,
  118. group: 2
  119. };
  120. const fullscreen = {
  121. key: 'fullscreen',
  122. Content: FullscreenButton,
  123. group: 2
  124. };
  125. const linkToSalesforce = {
  126. key: 'linktosalesforce',
  127. Content: LinkToSalesforceButton,
  128. group: 2
  129. };
  130. const shareVideo = {
  131. key: 'sharedvideo',
  132. Content: SharedVideoButton,
  133. group: 3
  134. };
  135. const shareAudio = {
  136. key: 'shareaudio',
  137. Content: ShareAudioButton,
  138. group: 3
  139. };
  140. const noiseSuppression = {
  141. key: 'noisesuppression',
  142. Content: NoiseSuppressionButton,
  143. group: 3
  144. };
  145. const settings = {
  146. key: 'settings',
  147. Content: SettingsButton,
  148. group: 4
  149. };
  150. const download = {
  151. key: 'download',
  152. Content: DownloadButton,
  153. group: 4
  154. };
  155. const help = {
  156. key: 'help',
  157. Content: HelpButton,
  158. group: 4
  159. };
  160. /**
  161. * A hook that returns the toggle camera button if it is enabled and undefined otherwise.
  162. *
  163. * @returns {Object | undefined}
  164. */
  165. function useToggleCameraButton() {
  166. const toggleCameraEnabled = useSelector(isToggleCameraEnabled);
  167. if (toggleCameraEnabled) {
  168. return toggleCamera;
  169. }
  170. }
  171. /**
  172. * A hook that returns the desktop sharing button if it is enabled and undefined otherwise.
  173. *
  174. * @returns {Object | undefined}
  175. */
  176. function getDesktopSharingButton() {
  177. if (JitsiMeetJS.isDesktopSharingEnabled()) {
  178. return desktop;
  179. }
  180. }
  181. /**
  182. * A hook that returns the fullscreen button if it is enabled and undefined otherwise.
  183. *
  184. * @returns {Object | undefined}
  185. */
  186. function getFullscreenButton() {
  187. if (!isIosMobileBrowser() || isIpadMobileBrowser()) {
  188. return fullscreen;
  189. }
  190. }
  191. /**
  192. * A hook that returns the "link to salesforce" button if it is enabled and undefined otherwise.
  193. *
  194. * @returns {Object | undefined}
  195. */
  196. function useLinkToSalesforceButton() {
  197. const _isSalesforceEnabled = useSelector(isSalesforceEnabled);
  198. if (_isSalesforceEnabled) {
  199. return linkToSalesforce;
  200. }
  201. }
  202. /**
  203. * A hook that returns the share audio button if it is enabled and undefined otherwise.
  204. *
  205. * @returns {Object | undefined}
  206. */
  207. function getShareAudioButton() {
  208. if (JitsiMeetJS.isDesktopSharingEnabled() && isScreenAudioSupported()) {
  209. return shareAudio;
  210. }
  211. }
  212. /**
  213. * A hook that returns the download button if it is enabled and undefined otherwise.
  214. *
  215. * @returns {Object | undefined}
  216. */
  217. function useDownloadButton() {
  218. const visible = useSelector(
  219. (state: IReduxState) => typeof state['features/base/config'].deploymentUrls?.downloadAppsUrl === 'string');
  220. if (visible) {
  221. return download;
  222. }
  223. }
  224. /**
  225. * A hook that returns the help button if it is enabled and undefined otherwise.
  226. *
  227. * @returns {Object | undefined}
  228. */
  229. function useHelpButton() {
  230. const visible = useSelector(
  231. (state: IReduxState) =>
  232. typeof state['features/base/config'].deploymentUrls?.userDocumentationURL === 'string'
  233. && getFeatureFlag(state, HELP_BUTTON_ENABLED, true));
  234. if (visible) {
  235. return help;
  236. }
  237. }
  238. /**
  239. * Returns all buttons that could be rendered.
  240. *
  241. * @param {Object} _customToolbarButtons - An array containing custom buttons objects.
  242. * @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
  243. */
  244. export function useToolboxButtons(
  245. _customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } {
  246. const dekstopSharing = getDesktopSharingButton();
  247. const toggleCameraButton = useToggleCameraButton();
  248. const _fullscreen = getFullscreenButton();
  249. const security = useSecurityDialogButton();
  250. const reactions = useReactionsButton();
  251. const participants = useParticipantPaneButton();
  252. const tileview = useTileViewButton();
  253. const cc = useClosedCaptionButton();
  254. const recording = useRecordingButton();
  255. const liveStreaming = useLiveStreamingButton();
  256. const linktosalesforce = useLinkToSalesforceButton();
  257. const shareaudio = getShareAudioButton();
  258. const whiteboard = useWhiteboardButton();
  259. const etherpad = useEtherpadButton();
  260. const virtualBackground = useVirtualBackgroundButton();
  261. const speakerStats = useSpeakerStatsButton();
  262. const shortcuts = useKeyboardShortcutsButton();
  263. const embed = useEmbedButton();
  264. const feedback = useFeedbackButton();
  265. const _download = useDownloadButton();
  266. const _help = useHelpButton();
  267. const buttons: { [key in ToolbarButton]?: IToolboxButton; } = {
  268. microphone,
  269. camera,
  270. profile,
  271. desktop: dekstopSharing,
  272. chat,
  273. raisehand,
  274. reactions,
  275. 'participants-pane': participants,
  276. invite,
  277. tileview,
  278. 'toggle-camera': toggleCameraButton,
  279. videoquality: videoQuality,
  280. fullscreen: _fullscreen,
  281. security,
  282. closedcaptions: cc,
  283. recording,
  284. livestreaming: liveStreaming,
  285. linktosalesforce,
  286. sharedvideo: shareVideo,
  287. shareaudio,
  288. noisesuppression: noiseSuppression,
  289. whiteboard,
  290. etherpad,
  291. 'select-background': virtualBackground,
  292. stats: speakerStats,
  293. settings,
  294. shortcuts,
  295. embedmeeting: embed,
  296. feedback,
  297. download: _download,
  298. help: _help
  299. };
  300. const buttonKeys = Object.keys(buttons) as ToolbarButton[];
  301. buttonKeys.forEach(
  302. key => typeof buttons[key] === 'undefined' && delete buttons[key]);
  303. const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
  304. prev[id] = {
  305. backgroundColor,
  306. key: id,
  307. id,
  308. Content: CustomOptionButton,
  309. group: 4,
  310. icon,
  311. text
  312. };
  313. return prev;
  314. }, {} as { [key: string]: ICustomToolbarButton; });
  315. return {
  316. ...buttons,
  317. ...customButtons
  318. };
  319. }
  320. export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
  321. const dispatch = useDispatch();
  322. const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
  323. const _isParticipantsPaneEnabled = useSelector(isParticipantsPaneEnabled);
  324. const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
  325. const _toolbarButtons = useSelector(
  326. (state: IReduxState) => toolbarButtons || state['features/toolbox'].toolbarButtons);
  327. const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen);
  328. const desktopSharingButtonDisabled = useSelector(isDesktopShareButtonDisabled);
  329. const desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
  330. const fullScreen = useSelector((state: IReduxState) => state['features/toolbox'].fullScreen);
  331. const gifsEnabled = useSelector(isGifEnabled);
  332. const participantsPaneOpen = useSelector(getParticipantsPaneOpen);
  333. const raisedHand = useSelector((state: IReduxState) => hasRaisedHand(getLocalParticipant(state)));
  334. const screenSharing = useSelector(isScreenVideoShared);
  335. const tileViewEnabled = useSelector(shouldDisplayTileView);
  336. /**
  337. * Creates an analytics keyboard shortcut event and dispatches an action for
  338. * toggling the display of chat.
  339. *
  340. * @private
  341. * @returns {void}
  342. */
  343. function onToggleChat() {
  344. sendAnalytics(createShortcutEvent(
  345. 'toggle.chat',
  346. ACTION_SHORTCUT_TRIGGERED,
  347. {
  348. enable: !chatOpen
  349. }));
  350. // Checks if there was any text selected by the user.
  351. // Used for when we press simultaneously keys for copying
  352. // text messages from the chat board
  353. if (window.getSelection()?.toString() !== '') {
  354. return false;
  355. }
  356. dispatch(toggleChat());
  357. }
  358. /**
  359. * Creates an analytics keyboard shortcut event and dispatches an action for
  360. * toggling the display of the participants pane.
  361. *
  362. * @private
  363. * @returns {void}
  364. */
  365. function onToggleParticipantsPane() {
  366. sendAnalytics(createShortcutEvent(
  367. 'toggle.participants-pane',
  368. ACTION_SHORTCUT_TRIGGERED,
  369. {
  370. enable: !participantsPaneOpen
  371. }));
  372. if (participantsPaneOpen) {
  373. dispatch(closeParticipantsPane());
  374. } else {
  375. dispatch(openParticipantsPane());
  376. }
  377. }
  378. /**
  379. * Creates an analytics keyboard shortcut event and dispatches an action for
  380. * toggling the display of Video Quality.
  381. *
  382. * @private
  383. * @returns {void}
  384. */
  385. function onToggleVideoQuality() {
  386. sendAnalytics(createShortcutEvent('video.quality'));
  387. dispatch(toggleDialog(VideoQualityDialog));
  388. }
  389. /**
  390. * Dispatches an action for toggling the tile view.
  391. *
  392. * @private
  393. * @returns {void}
  394. */
  395. function onToggleTileView() {
  396. sendAnalytics(createShortcutEvent(
  397. 'toggle.tileview',
  398. ACTION_SHORTCUT_TRIGGERED,
  399. {
  400. enable: !tileViewEnabled
  401. }));
  402. dispatch(toggleTileView());
  403. }
  404. /**
  405. * Creates an analytics keyboard shortcut event and dispatches an action for
  406. * toggling full screen mode.
  407. *
  408. * @private
  409. * @returns {void}
  410. */
  411. function onToggleFullScreen() {
  412. sendAnalytics(createShortcutEvent(
  413. 'toggle.fullscreen',
  414. ACTION_SHORTCUT_TRIGGERED,
  415. {
  416. enable: !fullScreen
  417. }));
  418. dispatch(setFullScreen(!fullScreen));
  419. }
  420. /**
  421. * Creates an analytics keyboard shortcut event and dispatches an action for
  422. * toggling raise hand.
  423. *
  424. * @private
  425. * @returns {void}
  426. */
  427. function onToggleRaiseHand() {
  428. sendAnalytics(createShortcutEvent(
  429. 'toggle.raise.hand',
  430. ACTION_SHORTCUT_TRIGGERED,
  431. { enable: !raisedHand }));
  432. dispatch(raiseHand(!raisedHand));
  433. }
  434. /**
  435. * Creates an analytics keyboard shortcut event and dispatches an action for
  436. * toggling screensharing.
  437. *
  438. * @private
  439. * @returns {void}
  440. */
  441. function onToggleScreenshare() {
  442. // Ignore the shortcut if the button is disabled.
  443. if (desktopSharingButtonDisabled) {
  444. return;
  445. }
  446. sendAnalytics(createShortcutEvent(
  447. 'toggle.screen.sharing',
  448. ACTION_SHORTCUT_TRIGGERED,
  449. {
  450. enable: !screenSharing
  451. }));
  452. if (desktopSharingEnabled && !desktopSharingButtonDisabled) {
  453. dispatch(startScreenShareFlow(!screenSharing));
  454. }
  455. }
  456. /**
  457. * Creates an analytics keyboard shortcut event and dispatches an action for
  458. * toggling speaker stats.
  459. *
  460. * @private
  461. * @returns {void}
  462. */
  463. function onSpeakerStats() {
  464. sendAnalytics(createShortcutEvent(
  465. 'speaker.stats'
  466. ));
  467. dispatch(toggleDialog(SpeakerStats, {
  468. conference: APP.conference
  469. }));
  470. }
  471. useEffect(() => {
  472. const KEYBOARD_SHORTCUTS = [
  473. isButtonEnabled('videoquality', _toolbarButtons) && {
  474. character: 'A',
  475. exec: onToggleVideoQuality,
  476. helpDescription: 'toolbar.callQuality'
  477. },
  478. isButtonEnabled('chat', _toolbarButtons) && {
  479. character: 'C',
  480. exec: onToggleChat,
  481. helpDescription: 'keyboardShortcuts.toggleChat'
  482. },
  483. isButtonEnabled('desktop', _toolbarButtons) && {
  484. character: 'D',
  485. exec: onToggleScreenshare,
  486. helpDescription: 'keyboardShortcuts.toggleScreensharing'
  487. },
  488. _isParticipantsPaneEnabled && isButtonEnabled('participants-pane', _toolbarButtons) && {
  489. character: 'P',
  490. exec: onToggleParticipantsPane,
  491. helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
  492. },
  493. isButtonEnabled('raisehand', _toolbarButtons) && {
  494. character: 'R',
  495. exec: onToggleRaiseHand,
  496. helpDescription: 'keyboardShortcuts.raiseHand'
  497. },
  498. isButtonEnabled('fullscreen', _toolbarButtons) && {
  499. character: 'S',
  500. exec: onToggleFullScreen,
  501. helpDescription: 'keyboardShortcuts.fullScreen'
  502. },
  503. isButtonEnabled('tileview', _toolbarButtons) && {
  504. character: 'W',
  505. exec: onToggleTileView,
  506. helpDescription: 'toolbar.tileViewToggle'
  507. },
  508. !_isSpeakerStatsDisabled && isButtonEnabled('stats', _toolbarButtons) && {
  509. character: 'T',
  510. exec: onSpeakerStats,
  511. helpDescription: 'keyboardShortcuts.showSpeakerStats'
  512. }
  513. ];
  514. KEYBOARD_SHORTCUTS.forEach(shortcut => {
  515. if (typeof shortcut === 'object') {
  516. dispatch(registerShortcut({
  517. character: shortcut.character,
  518. handler: shortcut.exec,
  519. helpDescription: shortcut.helpDescription
  520. }));
  521. }
  522. });
  523. // If the buttons for sending reactions are not displayed we should disable the shortcuts too.
  524. if (_shouldDisplayReactionsButtons) {
  525. const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
  526. const onShortcutSendReaction = () => {
  527. dispatch(addReactionToBuffer(key));
  528. sendAnalytics(createShortcutEvent(
  529. `reaction.${key}`
  530. ));
  531. };
  532. return {
  533. character: REACTIONS[key].shortcutChar,
  534. exec: onShortcutSendReaction,
  535. helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`,
  536. altKey: true
  537. };
  538. });
  539. REACTION_SHORTCUTS.forEach(shortcut => {
  540. dispatch(registerShortcut({
  541. alt: shortcut.altKey,
  542. character: shortcut.character,
  543. handler: shortcut.exec,
  544. helpDescription: shortcut.helpDescription
  545. }));
  546. });
  547. if (gifsEnabled) {
  548. const onGifShortcut = () => {
  549. batch(() => {
  550. dispatch(toggleReactionsMenuVisibility());
  551. dispatch(setGifMenuVisibility(true));
  552. });
  553. };
  554. dispatch(registerShortcut({
  555. character: 'G',
  556. handler: onGifShortcut,
  557. helpDescription: 'keyboardShortcuts.giphyMenu'
  558. }));
  559. }
  560. }
  561. return () => {
  562. [ 'A', 'C', 'D', 'P', 'R', 'S', 'W', 'T', 'G' ].forEach(letter =>
  563. dispatch(unregisterShortcut(letter)));
  564. if (_shouldDisplayReactionsButtons) {
  565. Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
  566. .forEach(letter =>
  567. dispatch(unregisterShortcut(letter, true)));
  568. }
  569. };
  570. }, [
  571. _shouldDisplayReactionsButtons,
  572. chatOpen,
  573. desktopSharingButtonDisabled,
  574. desktopSharingEnabled,
  575. fullScreen,
  576. gifsEnabled,
  577. participantsPaneOpen,
  578. raisedHand,
  579. screenSharing,
  580. tileViewEnabled
  581. ]);
  582. };