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

middleware.ts 9.0KB

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