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.js 8.5KB

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