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.

middleware.ts 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /* eslint-disable import/order */
  2. import { batch } from 'react-redux';
  3. // @ts-ignore
  4. import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics';
  5. import { IStore } from '../app/types';
  6. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
  7. import {
  8. CONFERENCE_JOIN_IN_PROGRESS,
  9. SET_START_REACTIONS_MUTED,
  10. setStartReactionsMuted
  11. // @ts-ignore
  12. } from '../base/conference';
  13. import {
  14. getParticipantById,
  15. getParticipantCount,
  16. isLocalParticipantModerator
  17. } from '../base/participants/functions';
  18. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  19. import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
  20. // @ts-ignore
  21. import { updateSettings } from '../base/settings/actions';
  22. // @ts-ignore
  23. import { playSound, registerSound, unregisterSound } from '../base/sounds';
  24. // @ts-ignore
  25. import { getDisabledSounds } from '../base/sounds/functions.any';
  26. // @ts-ignore
  27. import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
  28. import {
  29. ADD_REACTION_BUFFER,
  30. FLUSH_REACTION_BUFFER,
  31. SEND_REACTIONS,
  32. PUSH_REACTIONS,
  33. SHOW_SOUNDS_NOTIFICATION
  34. } from './actionTypes';
  35. import {
  36. addReactionsToChat,
  37. flushReactionBuffer,
  38. pushReactions,
  39. sendReactions,
  40. setReactionQueue
  41. } from './actions.any';
  42. import { displayReactionSoundsNotification } from './actions.web';
  43. import {
  44. ENDPOINT_REACTION_NAME,
  45. RAISE_HAND_SOUND_ID,
  46. REACTIONS,
  47. REACTION_SOUND,
  48. SOUNDS_THRESHOLDS,
  49. MUTE_REACTIONS_COMMAND,
  50. MuteCommandAttributes
  51. } from './constants';
  52. import {
  53. getReactionMessageFromBuffer,
  54. getReactionsSoundsThresholds,
  55. getReactionsWithId,
  56. sendReactionsWebhook
  57. } from './functions.any';
  58. import logger from './logger';
  59. import { RAISE_HAND_SOUND_FILE } from './sounds';
  60. /**
  61. * Middleware which intercepts Reactions actions to handle changes to the
  62. * visibility timeout of the Reactions.
  63. *
  64. * @param {IStore} store - The redux store.
  65. * @returns {Function}
  66. */
  67. MiddlewareRegistry.register((store: IStore) => (next: Function) => (action:any) => {
  68. const { dispatch, getState } = store;
  69. switch (action.type) {
  70. case APP_WILL_MOUNT:
  71. batch(() => {
  72. Object.keys(REACTIONS).forEach(key => {
  73. for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
  74. dispatch(registerSound(
  75. `${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`,
  76. REACTIONS[key].soundFiles[i]
  77. )
  78. );
  79. }
  80. }
  81. );
  82. dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE));
  83. });
  84. break;
  85. case APP_WILL_UNMOUNT:
  86. batch(() => {
  87. Object.keys(REACTIONS).forEach(key => {
  88. for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
  89. dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`));
  90. }
  91. });
  92. dispatch(unregisterSound(RAISE_HAND_SOUND_ID));
  93. });
  94. break;
  95. case ADD_REACTION_BUFFER: {
  96. const { timeoutID, buffer } = getState()['features/reactions'];
  97. const { reaction } = action;
  98. clearTimeout(timeoutID);
  99. buffer.push(reaction);
  100. action.buffer = buffer;
  101. action.timeoutID = setTimeout(() => {
  102. dispatch(flushReactionBuffer());
  103. }, 500);
  104. break;
  105. }
  106. case CONFERENCE_JOIN_IN_PROGRESS: {
  107. const { conference } = action;
  108. conference.addCommandListener(
  109. MUTE_REACTIONS_COMMAND, ({ attributes }: { attributes: MuteCommandAttributes }, id: any) => {
  110. _onMuteReactionsCommand(attributes, id, store);
  111. });
  112. break;
  113. }
  114. case FLUSH_REACTION_BUFFER: {
  115. const state = getState();
  116. const { buffer } = state['features/reactions'];
  117. const participantCount = getParticipantCount(state);
  118. batch(() => {
  119. if (participantCount > 1) {
  120. dispatch(sendReactions());
  121. }
  122. dispatch(addReactionsToChat(getReactionMessageFromBuffer(buffer)));
  123. dispatch(pushReactions(buffer));
  124. });
  125. sendReactionsWebhook(state, buffer);
  126. break;
  127. }
  128. case PUSH_REACTIONS: {
  129. const state = getState();
  130. const { queue, notificationDisplayed } = state['features/reactions'];
  131. const { soundsReactions } = state['features/base/settings'];
  132. const disabledSounds = getDisabledSounds(state);
  133. const reactions = action.reactions;
  134. batch(() => {
  135. if (!notificationDisplayed && soundsReactions && !disabledSounds.includes(REACTION_SOUND)
  136. && displayReactionSoundsNotification) {
  137. dispatch(displayReactionSoundsNotification());
  138. }
  139. if (soundsReactions) {
  140. const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions);
  141. reactionSoundsThresholds.forEach(reaction =>
  142. dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`))
  143. );
  144. }
  145. dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
  146. });
  147. break;
  148. }
  149. case SEND_REACTIONS: {
  150. const state = getState();
  151. const { buffer } = state['features/reactions'];
  152. const { conference } = state['features/base/conference'];
  153. if (conference) {
  154. conference.sendEndpointMessage('', {
  155. name: ENDPOINT_REACTION_NAME,
  156. reactions: buffer,
  157. timestamp: Date.now()
  158. });
  159. }
  160. break;
  161. }
  162. // Settings changed for mute reactions in the meeting
  163. case SET_START_REACTIONS_MUTED: {
  164. const state = getState();
  165. const { conference } = state['features/base/conference'];
  166. const { muted, updateBackend } = action;
  167. if (conference && isLocalParticipantModerator(state) && updateBackend) {
  168. conference.sendCommand(MUTE_REACTIONS_COMMAND, { attributes: { startReactionsMuted: Boolean(muted) } });
  169. }
  170. break;
  171. }
  172. case SETTINGS_UPDATED: {
  173. const { soundsReactions } = getState()['features/base/settings'];
  174. if (action.settings.soundsReactions === false && soundsReactions === true) {
  175. sendAnalytics(createReactionSoundsDisabledEvent());
  176. }
  177. break;
  178. }
  179. case SHOW_SOUNDS_NOTIFICATION: {
  180. const state = getState();
  181. const isModerator = isLocalParticipantModerator(state);
  182. const { disableReactionsModeration } = state['features/base/config'];
  183. const customActions = [ 'notify.reactionSounds' ];
  184. const customFunctions = [ () => dispatch(updateSettings({
  185. soundsReactions: false
  186. })) ];
  187. if (isModerator && !disableReactionsModeration) {
  188. customActions.push('notify.reactionSoundsForAll');
  189. customFunctions.push(() => batch(() => {
  190. dispatch(setStartReactionsMuted(true));
  191. dispatch(updateSettings({ soundsReactions: false }));
  192. }));
  193. }
  194. dispatch(showNotification({
  195. titleKey: 'toolbar.disableReactionSounds',
  196. customActionNameKey: customActions,
  197. customActionHandler: customFunctions
  198. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  199. break;
  200. }
  201. }
  202. return next(action);
  203. });
  204. /**
  205. * Notifies this instance about a "Mute Reaction Sounds" command received by the Jitsi
  206. * conference.
  207. *
  208. * @param {Object} attributes - The attributes carried by the command.
  209. * @param {string} id - The identifier of the participant who issuing the
  210. * command. A notable idiosyncrasy to be mindful of here is that the command
  211. * may be issued by the local participant.
  212. * @param {Object} store - The redux store. Used to calculate and dispatch
  213. * updates.
  214. * @private
  215. * @returns {void}
  216. */
  217. function _onMuteReactionsCommand(attributes: MuteCommandAttributes = {}, id: string, store: IStore) {
  218. const state = store.getState();
  219. // We require to know who issued the command because (1) only a
  220. // moderator is allowed to send commands and (2) a command MUST be
  221. // issued by a defined commander.
  222. if (typeof id === 'undefined') {
  223. return;
  224. }
  225. const participantSendingCommand = getParticipantById(state, id);
  226. // The Command(s) API will send us our own commands and we don't want
  227. // to act upon them.
  228. if (participantSendingCommand?.local) {
  229. return;
  230. }
  231. if (participantSendingCommand?.role !== 'moderator') {
  232. logger.warn('Received mute-reactions command not from moderator');
  233. return;
  234. }
  235. const oldState = Boolean(state['features/base/conference'].startReactionsMuted);
  236. // @ts-ignore
  237. const newState = attributes.startReactionsMuted === 'true';
  238. if (oldState !== newState) {
  239. batch(() => {
  240. store.dispatch(setStartReactionsMuted(newState));
  241. store.dispatch(updateSettings({ soundsReactions: !newState }));
  242. });
  243. }
  244. }