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.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { batch } from 'react-redux';
  2. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
  3. import { getConferenceState } from '../base/conference/functions';
  4. import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
  5. import { MEDIA_TYPE, MediaType } from '../base/media/constants';
  6. import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
  7. import { raiseHand } from '../base/participants/actions';
  8. import {
  9. getLocalParticipant,
  10. getRemoteParticipants,
  11. hasRaisedHand,
  12. isLocalParticipantModerator,
  13. isParticipantModerator
  14. } from '../base/participants/functions';
  15. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  16. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  17. import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
  18. import { hideNotification, showNotification } from '../notifications/actions';
  19. import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
  20. import { muteLocal } from '../video-menu/actions.any';
  21. import {
  22. DISABLE_MODERATION,
  23. ENABLE_MODERATION,
  24. LOCAL_PARTICIPANT_APPROVED,
  25. LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
  26. LOCAL_PARTICIPANT_REJECTED,
  27. PARTICIPANT_APPROVED,
  28. PARTICIPANT_REJECTED,
  29. REQUEST_DISABLE_AUDIO_MODERATION,
  30. REQUEST_DISABLE_VIDEO_MODERATION,
  31. REQUEST_ENABLE_AUDIO_MODERATION,
  32. REQUEST_ENABLE_VIDEO_MODERATION
  33. } from './actionTypes';
  34. import {
  35. disableModeration,
  36. dismissPendingAudioParticipant,
  37. dismissPendingParticipant,
  38. enableModeration,
  39. localParticipantApproved,
  40. localParticipantRejected,
  41. participantApproved,
  42. participantPendingAudio,
  43. participantRejected
  44. } from './actions';
  45. import {
  46. ASKED_TO_UNMUTE_NOTIFICATION_ID,
  47. ASKED_TO_UNMUTE_SOUND_ID,
  48. AUDIO_MODERATION_NOTIFICATION_ID,
  49. VIDEO_MODERATION_NOTIFICATION_ID
  50. } from './constants';
  51. import {
  52. isEnabledFromState,
  53. isParticipantApproved,
  54. isParticipantPending
  55. } from './functions';
  56. import { ASKED_TO_UNMUTE_FILE } from './sounds';
  57. MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
  58. const { type } = action;
  59. const { conference } = getConferenceState(getState());
  60. switch (type) {
  61. case APP_WILL_MOUNT: {
  62. dispatch(registerSound(ASKED_TO_UNMUTE_SOUND_ID, ASKED_TO_UNMUTE_FILE));
  63. break;
  64. }
  65. case APP_WILL_UNMOUNT: {
  66. dispatch(unregisterSound(ASKED_TO_UNMUTE_SOUND_ID));
  67. break;
  68. }
  69. case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
  70. let descriptionKey;
  71. let titleKey;
  72. let uid = '';
  73. const localParticipant = getLocalParticipant(getState);
  74. const raisedHand = hasRaisedHand(localParticipant);
  75. switch (action.mediaType) {
  76. case MEDIA_TYPE.AUDIO: {
  77. titleKey = 'notify.moderationInEffectTitle';
  78. uid = AUDIO_MODERATION_NOTIFICATION_ID;
  79. break;
  80. }
  81. case MEDIA_TYPE.VIDEO: {
  82. titleKey = 'notify.moderationInEffectVideoTitle';
  83. uid = VIDEO_MODERATION_NOTIFICATION_ID;
  84. break;
  85. }
  86. }
  87. dispatch(showNotification({
  88. customActionNameKey: [ 'notify.raiseHandAction' ],
  89. customActionHandler: [ () => batch(() => {
  90. !raisedHand && dispatch(raiseHand(true));
  91. dispatch(hideNotification(uid));
  92. }) ],
  93. descriptionKey,
  94. sticky: true,
  95. titleKey,
  96. uid
  97. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  98. break;
  99. }
  100. case REQUEST_DISABLE_AUDIO_MODERATION: {
  101. conference?.disableAVModeration(MEDIA_TYPE.AUDIO);
  102. break;
  103. }
  104. case REQUEST_DISABLE_VIDEO_MODERATION: {
  105. conference?.disableAVModeration(MEDIA_TYPE.VIDEO);
  106. break;
  107. }
  108. case REQUEST_ENABLE_AUDIO_MODERATION: {
  109. conference?.enableAVModeration(MEDIA_TYPE.AUDIO);
  110. break;
  111. }
  112. case REQUEST_ENABLE_VIDEO_MODERATION: {
  113. conference?.enableAVModeration(MEDIA_TYPE.VIDEO);
  114. break;
  115. }
  116. case PARTICIPANT_UPDATED: {
  117. const state = getState();
  118. const audioModerationEnabled = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
  119. const participant = action.participant;
  120. if (participant && audioModerationEnabled) {
  121. if (isLocalParticipantModerator(state)) {
  122. // this is handled only by moderators
  123. if (hasRaisedHand(participant)) {
  124. // if participant raises hand show notification
  125. !isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state)
  126. && dispatch(participantPendingAudio(participant));
  127. } else {
  128. // if participant lowers hand hide notification
  129. isParticipantPending(participant, MEDIA_TYPE.AUDIO)(state)
  130. && dispatch(dismissPendingAudioParticipant(participant));
  131. }
  132. } else if (participant.id === getLocalParticipant(state)?.id
  133. && /* the new role */ isParticipantModerator(participant)) {
  134. // this is the granted moderator case
  135. getRemoteParticipants(state).forEach(p => {
  136. hasRaisedHand(p) && !isParticipantApproved(p.id, MEDIA_TYPE.AUDIO)(state)
  137. && dispatch(participantPendingAudio(p));
  138. });
  139. }
  140. }
  141. break;
  142. }
  143. case ENABLE_MODERATION: {
  144. if (typeof APP !== 'undefined') {
  145. APP.API.notifyModerationChanged(action.mediaType, true);
  146. }
  147. break;
  148. }
  149. case DISABLE_MODERATION: {
  150. if (typeof APP !== 'undefined') {
  151. APP.API.notifyModerationChanged(action.mediaType, false);
  152. }
  153. break;
  154. }
  155. case LOCAL_PARTICIPANT_APPROVED: {
  156. if (typeof APP !== 'undefined') {
  157. const local = getLocalParticipant(getState());
  158. APP.API.notifyParticipantApproved(local?.id, action.mediaType);
  159. }
  160. break;
  161. }
  162. case PARTICIPANT_APPROVED: {
  163. if (typeof APP !== 'undefined') {
  164. APP.API.notifyParticipantApproved(action.id, action.mediaType);
  165. }
  166. break;
  167. }
  168. case LOCAL_PARTICIPANT_REJECTED: {
  169. if (typeof APP !== 'undefined') {
  170. const local = getLocalParticipant(getState());
  171. APP.API.notifyParticipantRejected(local?.id, action.mediaType);
  172. }
  173. break;
  174. }
  175. case PARTICIPANT_REJECTED: {
  176. if (typeof APP !== 'undefined') {
  177. APP.API.notifyParticipantRejected(action.id, action.mediaType);
  178. }
  179. break;
  180. }
  181. }
  182. return next(action);
  183. });
  184. /**
  185. * Registers a change handler for state['features/base/conference'].conference to
  186. * set the event listeners needed for the A/V moderation feature to operate.
  187. */
  188. StateListenerRegistry.register(
  189. state => state['features/base/conference'].conference,
  190. (conference, { dispatch }, previousConference) => {
  191. if (conference && !previousConference) {
  192. // local participant is allowed to unmute
  193. conference.on(JitsiConferenceEvents.AV_MODERATION_APPROVED, ({ mediaType }: { mediaType: MediaType; }) => {
  194. dispatch(localParticipantApproved(mediaType));
  195. // Audio & video moderation are both enabled at the same time.
  196. // Avoid displaying 2 different notifications.
  197. if (mediaType === MEDIA_TYPE.AUDIO) {
  198. dispatch(showNotification({
  199. titleKey: 'notify.hostAskedUnmute',
  200. sticky: true,
  201. customActionNameKey: [ 'notify.unmute' ],
  202. customActionHandler: [ () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) ],
  203. uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
  204. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  205. dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
  206. }
  207. });
  208. conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }: { mediaType: MediaType; }) => {
  209. dispatch(localParticipantRejected(mediaType));
  210. });
  211. conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }: {
  212. actor: Object; enabled: boolean; mediaType: MediaType;
  213. }) => {
  214. enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor));
  215. });
  216. // this is received by moderators
  217. conference.on(
  218. JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_APPROVED,
  219. ({ participant, mediaType }: { mediaType: MediaType; participant: { _id: string; }; }) => {
  220. const { _id: id } = participant;
  221. batch(() => {
  222. // store in the whitelist
  223. dispatch(participantApproved(id, mediaType));
  224. // remove from pending list
  225. dispatch(dismissPendingParticipant(id, mediaType));
  226. });
  227. });
  228. // this is received by moderators
  229. conference.on(
  230. JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED,
  231. ({ participant, mediaType }: { mediaType: MediaType; participant: { _id: string; }; }) => {
  232. const { _id: id } = participant;
  233. dispatch(participantRejected(id, mediaType));
  234. });
  235. }
  236. });