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 8.9KB

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