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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. import i18n from 'i18next';
  2. import { batch } from 'react-redux';
  3. import { AnyAction } from 'redux';
  4. // @ts-expect-error
  5. import UIEvents from '../../../../service/UI/UIEvents';
  6. import { IStore } from '../../app/types';
  7. import { approveParticipant } from '../../av-moderation/actions';
  8. import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
  9. import { getBreakoutRooms } from '../../breakout-rooms/functions';
  10. import { toggleE2EE } from '../../e2ee/actions';
  11. import { MAX_MODE } from '../../e2ee/constants';
  12. import { showNotification } from '../../notifications/actions';
  13. import {
  14. LOCAL_RECORDING_NOTIFICATION_ID,
  15. NOTIFICATION_TIMEOUT_TYPE,
  16. RAISE_HAND_NOTIFICATION_ID
  17. } from '../../notifications/constants';
  18. import { isForceMuted } from '../../participants-pane/functions';
  19. import { CALLING, INVITED } from '../../presence-status/constants';
  20. import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
  21. import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
  22. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
  23. import { CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
  24. import { forEachConference, getCurrentConference } from '../conference/functions';
  25. import { IJitsiConference } from '../conference/reducer';
  26. import { SET_CONFIG } from '../config/actionTypes';
  27. import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
  28. import { JitsiConferenceEvents } from '../lib-jitsi-meet';
  29. import { MEDIA_TYPE } from '../media/constants';
  30. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  31. import StateListenerRegistry from '../redux/StateListenerRegistry';
  32. import { playSound, registerSound, unregisterSound } from '../sounds/actions';
  33. import {
  34. DOMINANT_SPEAKER_CHANGED,
  35. GRANT_MODERATOR,
  36. KICK_PARTICIPANT,
  37. LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
  38. LOCAL_PARTICIPANT_RAISE_HAND,
  39. MUTE_REMOTE_PARTICIPANT,
  40. OVERWRITE_PARTICIPANTS_NAMES,
  41. OVERWRITE_PARTICIPANT_NAME,
  42. PARTICIPANT_DISPLAY_NAME_CHANGED,
  43. PARTICIPANT_JOINED,
  44. PARTICIPANT_LEFT,
  45. PARTICIPANT_UPDATED,
  46. RAISE_HAND_UPDATED,
  47. SET_LOCAL_PARTICIPANT_RECORDING_STATUS
  48. } from './actionTypes';
  49. import {
  50. localParticipantIdChanged,
  51. localParticipantJoined,
  52. localParticipantLeft,
  53. overwriteParticipantName,
  54. participantLeft,
  55. participantUpdated,
  56. raiseHand,
  57. raiseHandUpdateQueue,
  58. setLoadableAvatarUrl
  59. } from './actions';
  60. import {
  61. LOCAL_PARTICIPANT_DEFAULT_ID,
  62. LOWER_HAND_AUDIO_LEVEL,
  63. PARTICIPANT_JOINED_SOUND_ID,
  64. PARTICIPANT_LEFT_SOUND_ID
  65. } from './constants';
  66. import {
  67. getDominantSpeakerParticipant,
  68. getFirstLoadableAvatarUrl,
  69. getLocalParticipant,
  70. getParticipantById,
  71. getParticipantCount,
  72. getParticipantDisplayName,
  73. getRaiseHandsQueue,
  74. getRemoteParticipants,
  75. hasRaisedHand,
  76. isLocalParticipantModerator,
  77. isScreenShareParticipant,
  78. isWhiteboardParticipant
  79. } from './functions';
  80. import logger from './logger';
  81. import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
  82. import { IJitsiParticipant } from './types';
  83. import './subscriber';
  84. /**
  85. * Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
  86. * updates respectively ID of local participant.
  87. *
  88. * @param {Store} store - The redux store.
  89. * @returns {Function}
  90. */
  91. MiddlewareRegistry.register(store => next => action => {
  92. switch (action.type) {
  93. case APP_WILL_MOUNT:
  94. _registerSounds(store);
  95. return _localParticipantJoined(store, next, action);
  96. case APP_WILL_UNMOUNT:
  97. _unregisterSounds(store);
  98. return _localParticipantLeft(store, next, action);
  99. case CONFERENCE_WILL_JOIN:
  100. store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
  101. break;
  102. case DOMINANT_SPEAKER_CHANGED: {
  103. // Lower hand through xmpp when local participant becomes dominant speaker.
  104. const { id } = action.participant;
  105. const state = store.getState();
  106. const participant = getLocalParticipant(state);
  107. const dominantSpeaker = getDominantSpeakerParticipant(state);
  108. const isLocal = participant && participant.id === id;
  109. if (isLocal && dominantSpeaker?.id !== id
  110. && hasRaisedHand(participant)
  111. && !getDisableRemoveRaisedHandOnFocus(state)) {
  112. store.dispatch(raiseHand(false));
  113. }
  114. break;
  115. }
  116. case LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED: {
  117. const state = store.getState();
  118. const participant = getDominantSpeakerParticipant(state);
  119. if (
  120. participant?.local
  121. && hasRaisedHand(participant)
  122. && action.level > LOWER_HAND_AUDIO_LEVEL
  123. && !getDisableRemoveRaisedHandOnFocus(state)
  124. ) {
  125. store.dispatch(raiseHand(false));
  126. }
  127. break;
  128. }
  129. case GRANT_MODERATOR: {
  130. const { conference } = store.getState()['features/base/conference'];
  131. conference?.grantOwner(action.id);
  132. break;
  133. }
  134. case KICK_PARTICIPANT: {
  135. const { conference } = store.getState()['features/base/conference'];
  136. conference?.kickParticipant(action.id);
  137. break;
  138. }
  139. case LOCAL_PARTICIPANT_RAISE_HAND: {
  140. const { raisedHandTimestamp } = action;
  141. const localId = getLocalParticipant(store.getState())?.id;
  142. store.dispatch(participantUpdated({
  143. // XXX Only the local participant is allowed to update without
  144. // stating the JitsiConference instance (i.e. participant property
  145. // `conference` for a remote participant) because the local
  146. // participant is uniquely identified by the very fact that there is
  147. // only one local participant.
  148. id: localId ?? '',
  149. local: true,
  150. raisedHandTimestamp
  151. }));
  152. store.dispatch(raiseHandUpdateQueue({
  153. id: localId ?? '',
  154. raisedHandTimestamp
  155. }));
  156. if (typeof APP !== 'undefined') {
  157. APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp);
  158. }
  159. break;
  160. }
  161. case SET_CONFIG: {
  162. const result = next(action);
  163. const state = store.getState();
  164. const { deploymentInfo } = state['features/base/config'];
  165. // if there userRegion set let's use it for the local participant
  166. if (deploymentInfo?.userRegion) {
  167. const localId = getLocalParticipant(state)?.id;
  168. if (localId) {
  169. store.dispatch(participantUpdated({
  170. id: localId,
  171. local: true,
  172. region: deploymentInfo.userRegion
  173. }));
  174. }
  175. }
  176. return result;
  177. }
  178. case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
  179. const state = store.getState();
  180. const { recording, onlySelf } = action;
  181. const localId = getLocalParticipant(state)?.id;
  182. const { localRecording } = state['features/base/config'];
  183. if (localRecording?.notifyAllParticipants && !onlySelf && localId) {
  184. store.dispatch(participantUpdated({
  185. // XXX Only the local participant is allowed to update without
  186. // stating the JitsiConference instance (i.e. participant property
  187. // `conference` for a remote participant) because the local
  188. // participant is uniquely identified by the very fact that there is
  189. // only one local participant.
  190. id: localId,
  191. local: true,
  192. localRecording: recording
  193. }));
  194. }
  195. break;
  196. }
  197. case MUTE_REMOTE_PARTICIPANT: {
  198. const { conference } = store.getState()['features/base/conference'];
  199. conference?.muteParticipant(action.id, action.mediaType);
  200. break;
  201. }
  202. // TODO Remove this middleware when the local display name update flow is
  203. // fully brought into redux.
  204. case PARTICIPANT_DISPLAY_NAME_CHANGED: {
  205. if (typeof APP !== 'undefined') {
  206. const participant = getLocalParticipant(store.getState());
  207. if (participant && participant.id === action.id) {
  208. APP.UI.emitEvent(UIEvents.NICKNAME_CHANGED, action.name);
  209. }
  210. }
  211. break;
  212. }
  213. case RAISE_HAND_UPDATED: {
  214. const { participant } = action;
  215. let queue = getRaiseHandsQueue(store.getState());
  216. if (participant.raisedHandTimestamp) {
  217. queue.push({
  218. id: participant.id,
  219. raisedHandTimestamp: participant.raisedHandTimestamp
  220. });
  221. // sort the queue before adding to store.
  222. queue = queue.sort(({ raisedHandTimestamp: a }, { raisedHandTimestamp: b }) => a - b);
  223. } else {
  224. // no need to sort on remove value.
  225. queue = queue.filter(({ id }) => id !== participant.id);
  226. }
  227. action.queue = queue;
  228. break;
  229. }
  230. case PARTICIPANT_JOINED: {
  231. // Do not play sounds when a screenshare or whiteboard participant tile is created for screenshare.
  232. (!isScreenShareParticipant(action.participant)
  233. && !isWhiteboardParticipant(action.participant)
  234. ) && _maybePlaySounds(store, action);
  235. return _participantJoinedOrUpdated(store, next, action);
  236. }
  237. case PARTICIPANT_LEFT: {
  238. // Do not play sounds when a tile for screenshare or whiteboard is removed.
  239. (!isScreenShareParticipant(action.participant)
  240. && !isWhiteboardParticipant(action.participant)
  241. ) && _maybePlaySounds(store, action);
  242. break;
  243. }
  244. case PARTICIPANT_UPDATED:
  245. return _participantJoinedOrUpdated(store, next, action);
  246. case OVERWRITE_PARTICIPANTS_NAMES: {
  247. const { participantList } = action;
  248. if (!Array.isArray(participantList)) {
  249. logger.error('Overwrite names failed. Argument is not an array.');
  250. return;
  251. }
  252. batch(() => {
  253. participantList.forEach(p => {
  254. store.dispatch(overwriteParticipantName(p.id, p.name));
  255. });
  256. });
  257. break;
  258. }
  259. case OVERWRITE_PARTICIPANT_NAME: {
  260. const { dispatch, getState } = store;
  261. const state = getState();
  262. const { id, name } = action;
  263. let breakoutRoom = false, identifier = id;
  264. if (id.indexOf('@') !== -1) {
  265. identifier = id.slice(id.indexOf('/') + 1);
  266. breakoutRoom = true;
  267. action.id = identifier;
  268. }
  269. if (breakoutRoom) {
  270. const rooms = getBreakoutRooms(state);
  271. const roomCounter = state['features/breakout-rooms'].roomCounter;
  272. const newRooms: any = {};
  273. Object.entries(rooms).forEach(([ key, r ]) => {
  274. const participants = r?.participants || {};
  275. const jid = Object.keys(participants).find(p =>
  276. p.slice(p.indexOf('/') + 1) === identifier);
  277. if (jid) {
  278. newRooms[key] = {
  279. ...r,
  280. participants: {
  281. ...participants,
  282. [jid]: {
  283. ...participants[jid],
  284. displayName: name
  285. }
  286. }
  287. };
  288. } else {
  289. newRooms[key] = r;
  290. }
  291. });
  292. dispatch({
  293. type: UPDATE_BREAKOUT_ROOMS,
  294. rooms,
  295. roomCounter,
  296. updatedNames: true
  297. });
  298. } else {
  299. dispatch(participantUpdated({
  300. id: identifier,
  301. name
  302. }));
  303. }
  304. break;
  305. }
  306. }
  307. return next(action);
  308. });
  309. /**
  310. * Syncs the redux state features/base/participants up with the redux state
  311. * features/base/conference by ensuring that the former does not contain remote
  312. * participants no longer relevant to the latter. Introduced to address an issue
  313. * with multiplying thumbnails in the filmstrip.
  314. */
  315. StateListenerRegistry.register(
  316. /* selector */ state => getCurrentConference(state),
  317. /* listener */ (conference, { dispatch, getState }) => {
  318. batch(() => {
  319. for (const [ id, p ] of getRemoteParticipants(getState())) {
  320. (!conference || p.conference !== conference)
  321. && dispatch(participantLeft(id, p.conference, {
  322. isReplaced: p.isReplaced
  323. }));
  324. }
  325. });
  326. });
  327. /**
  328. * Reset the ID of the local participant to
  329. * {@link LOCAL_PARTICIPANT_DEFAULT_ID}. Such a reset is deemed possible only if
  330. * the local participant and, respectively, her ID is not involved in a
  331. * conference which is still of interest to the user and, consequently, the app.
  332. * For example, a conference which is in the process of leaving is no longer of
  333. * interest the user, is unrecoverable from the perspective of the user and,
  334. * consequently, the app.
  335. */
  336. StateListenerRegistry.register(
  337. /* selector */ state => state['features/base/conference'],
  338. /* listener */ ({ leaving }, { dispatch, getState }) => {
  339. const state = getState();
  340. const localParticipant = getLocalParticipant(state);
  341. let id: string;
  342. if (!localParticipant
  343. || (id = localParticipant.id)
  344. === LOCAL_PARTICIPANT_DEFAULT_ID) {
  345. // The ID of the local participant has been reset already.
  346. return;
  347. }
  348. // The ID of the local may be reset only if it is not in use.
  349. const dispatchLocalParticipantIdChanged
  350. = forEachConference(
  351. state,
  352. conference =>
  353. conference === leaving || conference.myUserId() !== id);
  354. dispatchLocalParticipantIdChanged
  355. && dispatch(
  356. localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
  357. });
  358. /**
  359. * Registers listeners for participant change events.
  360. */
  361. StateListenerRegistry.register(
  362. state => state['features/base/conference'].conference,
  363. (conference, store) => {
  364. if (conference) {
  365. const propertyHandlers: {
  366. [key: string]: Function;
  367. } = {
  368. 'e2ee.enabled': (participant: IJitsiParticipant, value: string) =>
  369. _e2eeUpdated(store, conference, participant.getId(), value),
  370. 'features_e2ee': (participant: IJitsiParticipant, value: boolean) =>
  371. getParticipantById(store.getState(), participant.getId())?.e2eeSupported !== value
  372. && store.dispatch(participantUpdated({
  373. conference,
  374. id: participant.getId(),
  375. e2eeSupported: value
  376. })),
  377. 'features_jigasi': (participant: IJitsiParticipant, value: boolean) =>
  378. store.dispatch(participantUpdated({
  379. conference,
  380. id: participant.getId(),
  381. isJigasi: value
  382. })),
  383. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  384. 'features_screen-sharing': (participant: IJitsiParticipant, value: string) =>
  385. store.dispatch(participantUpdated({
  386. conference,
  387. id: participant.getId(),
  388. features: { 'screen-sharing': true }
  389. })),
  390. 'localRecording': (participant: IJitsiParticipant, value: string) =>
  391. _localRecordingUpdated(store, conference, participant.getId(), value),
  392. 'raisedHand': (participant: IJitsiParticipant, value: string) =>
  393. _raiseHandUpdated(store, conference, participant.getId(), value),
  394. 'region': (participant: IJitsiParticipant, value: string) =>
  395. store.dispatch(participantUpdated({
  396. conference,
  397. id: participant.getId(),
  398. region: value
  399. })),
  400. 'remoteControlSessionStatus': (participant: IJitsiParticipant, value: boolean) =>
  401. store.dispatch(participantUpdated({
  402. conference,
  403. id: participant.getId(),
  404. remoteControlSessionStatus: value
  405. }))
  406. };
  407. // update properties for the participants that are already in the conference
  408. conference.getParticipants().forEach((participant: any) => {
  409. Object.keys(propertyHandlers).forEach(propertyName => {
  410. const value = participant.getProperty(propertyName);
  411. if (value !== undefined) {
  412. propertyHandlers[propertyName as keyof typeof propertyHandlers](participant, value);
  413. }
  414. });
  415. });
  416. // We joined a conference
  417. conference.on(
  418. JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
  419. (participant: IJitsiParticipant, propertyName: string, oldValue: string, newValue: string) => {
  420. if (propertyHandlers.hasOwnProperty(propertyName)) {
  421. propertyHandlers[propertyName](participant, newValue);
  422. }
  423. });
  424. } else {
  425. const localParticipantId = getLocalParticipant(store.getState)?.id;
  426. // We left the conference, the local participant must be updated.
  427. _e2eeUpdated(store, conference, localParticipantId ?? '', false);
  428. _raiseHandUpdated(store, conference, localParticipantId ?? '', 0);
  429. }
  430. }
  431. );
  432. /**
  433. * Handles a E2EE enabled status update.
  434. *
  435. * @param {Store} store - The redux store.
  436. * @param {Object} conference - The conference for which we got an update.
  437. * @param {string} participantId - The ID of the participant from which we got an update.
  438. * @param {boolean} newValue - The new value of the E2EE enabled status.
  439. * @returns {void}
  440. */
  441. function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConference,
  442. participantId: string, newValue: string | boolean) {
  443. const e2eeEnabled = newValue === 'true';
  444. const state = getState();
  445. const { e2ee = {} } = state['features/base/config'];
  446. if (e2eeEnabled === getParticipantById(state, participantId)?.e2eeEnabled) {
  447. return;
  448. }
  449. dispatch(participantUpdated({
  450. conference,
  451. id: participantId,
  452. e2eeEnabled
  453. }));
  454. if (e2ee.externallyManagedKey) {
  455. return;
  456. }
  457. const { maxMode } = getState()['features/e2ee'] || {};
  458. if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) {
  459. dispatch(toggleE2EE(e2eeEnabled));
  460. }
  461. }
  462. /**
  463. * Initializes the local participant and signals that it joined.
  464. *
  465. * @private
  466. * @param {Store} store - The redux store.
  467. * @param {Dispatch} next - The redux dispatch function to dispatch the
  468. * specified action to the specified store.
  469. * @param {Action} action - The redux action which is being dispatched
  470. * in the specified store.
  471. * @private
  472. * @returns {Object} The value returned by {@code next(action)}.
  473. */
  474. function _localParticipantJoined({ getState, dispatch }: IStore, next: Function, action: AnyAction) {
  475. const result = next(action);
  476. const settings = getState()['features/base/settings'];
  477. dispatch(localParticipantJoined({
  478. avatarURL: settings.avatarURL,
  479. email: settings.email,
  480. name: settings.displayName,
  481. id: ''
  482. }));
  483. return result;
  484. }
  485. /**
  486. * Signals that the local participant has left.
  487. *
  488. * @param {Store} store - The redux store.
  489. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  490. * specified {@code action} into the specified {@code store}.
  491. * @param {Action} action - The redux action which is being dispatched in the
  492. * specified {@code store}.
  493. * @private
  494. * @returns {Object} The value returned by {@code next(action)}.
  495. */
  496. function _localParticipantLeft({ dispatch }: IStore, next: Function, action: AnyAction) {
  497. const result = next(action);
  498. dispatch(localParticipantLeft());
  499. return result;
  500. }
  501. /**
  502. * Plays sounds when participants join/leave conference.
  503. *
  504. * @param {Store} store - The redux store.
  505. * @param {Action} action - The redux action. Should be either
  506. * {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
  507. * @private
  508. * @returns {void}
  509. */
  510. function _maybePlaySounds({ getState, dispatch }: IStore, action: AnyAction) {
  511. const state = getState();
  512. const { startAudioMuted } = state['features/base/config'];
  513. const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
  514. // We're not playing sounds for local participant
  515. // nor when the user is joining past the "startAudioMuted" limit.
  516. // The intention there was to not play user joined notification in big
  517. // conferences where 100th person is joining.
  518. if (!action.participant.local
  519. && (!startAudioMuted
  520. || getParticipantCount(state) < startAudioMuted)) {
  521. const { isReplacing, isReplaced } = action.participant;
  522. if (action.type === PARTICIPANT_JOINED) {
  523. if (!joinSound) {
  524. return;
  525. }
  526. const { presence } = action.participant;
  527. // The sounds for the poltergeist are handled by features/invite.
  528. if (presence !== INVITED && presence !== CALLING && !isReplacing) {
  529. dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
  530. }
  531. } else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
  532. dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
  533. }
  534. }
  535. }
  536. /**
  537. * Notifies the feature base/participants that the action
  538. * {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} is being dispatched
  539. * within a specific redux store.
  540. *
  541. * @param {Store} store - The redux store in which the specified {@code action}
  542. * is being dispatched.
  543. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  544. * specified {@code action} in the specified {@code store}.
  545. * @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or
  546. * {@code PARTICIPANT_UPDATED} which is being dispatched in the specified
  547. * {@code store}.
  548. * @private
  549. * @returns {Object} The value returned by {@code next(action)}.
  550. */
  551. function _participantJoinedOrUpdated(store: IStore, next: Function, action: AnyAction) {
  552. const { dispatch, getState } = store;
  553. const { overwrittenNameList } = store.getState()['features/base/participants'];
  554. const { participant: {
  555. avatarURL,
  556. email,
  557. id,
  558. local,
  559. localRecording,
  560. name,
  561. raisedHandTimestamp
  562. } } = action;
  563. // Send an external update of the local participant's raised hand state
  564. // if a new raised hand state is defined in the action.
  565. if (typeof raisedHandTimestamp !== 'undefined') {
  566. if (local) {
  567. const { conference } = getState()['features/base/conference'];
  568. const rHand = parseInt(raisedHandTimestamp, 10);
  569. // Send raisedHand signalling only if there is a change
  570. if (conference && rHand !== getLocalParticipant(getState())?.raisedHandTimestamp) {
  571. conference.setLocalParticipantProperty('raisedHand', rHand);
  572. }
  573. }
  574. }
  575. if (overwrittenNameList[id]) {
  576. action.participant.name = overwrittenNameList[id];
  577. }
  578. // Send an external update of the local participant's local recording state
  579. // if a new local recording state is defined in the action.
  580. if (typeof localRecording !== 'undefined') {
  581. if (local) {
  582. const conference = getCurrentConference(getState);
  583. // Send localRecording signalling only if there is a change
  584. if (conference
  585. && localRecording !== getLocalParticipant(getState())?.localRecording) {
  586. conference.setLocalParticipantProperty('localRecording', localRecording);
  587. }
  588. }
  589. }
  590. // Allow the redux update to go through and compare the old avatar
  591. // to the new avatar and emit out change events if necessary.
  592. const result = next(action);
  593. // Only run this if the config is populated, otherwise we preload external resources
  594. // even if disableThirdPartyRequests is set to true in config
  595. if (Object.keys(getState()['features/base/config']).length) {
  596. const { disableThirdPartyRequests } = getState()['features/base/config'];
  597. if (!disableThirdPartyRequests && (avatarURL || email || id || name)) {
  598. const participantId = !id && local ? getLocalParticipant(getState())?.id : id;
  599. const updatedParticipant = getParticipantById(getState(), participantId);
  600. getFirstLoadableAvatarUrl(updatedParticipant ?? { id: '' }, store)
  601. .then((urlData?: { isUsingCORS: boolean; src: string; }) => {
  602. dispatch(setLoadableAvatarUrl(participantId, urlData?.src ?? '', Boolean(urlData?.isUsingCORS)));
  603. });
  604. }
  605. }
  606. return result;
  607. }
  608. /**
  609. * Handles a local recording status update.
  610. *
  611. * @param {Function} dispatch - The Redux dispatch function.
  612. * @param {Object} conference - The conference for which we got an update.
  613. * @param {string} participantId - The ID of the participant from which we got an update.
  614. * @param {boolean} newValue - The new value of the local recording status.
  615. * @returns {void}
  616. */
  617. function _localRecordingUpdated({ dispatch, getState }: IStore, conference: IJitsiConference,
  618. participantId: string, newValue: string) {
  619. const state = getState();
  620. dispatch(participantUpdated({
  621. conference,
  622. id: participantId,
  623. localRecording: newValue
  624. }));
  625. const participantName = getParticipantDisplayName(state, participantId);
  626. dispatch(showNotification({
  627. titleKey: 'notify.somebody',
  628. title: participantName,
  629. descriptionKey: newValue ? 'notify.localRecordingStarted' : 'notify.localRecordingStopped',
  630. uid: LOCAL_RECORDING_NOTIFICATION_ID
  631. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  632. dispatch(playSound(newValue ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
  633. }
  634. /**
  635. * Handles a raise hand status update.
  636. *
  637. * @param {Function} dispatch - The Redux dispatch function.
  638. * @param {Object} conference - The conference for which we got an update.
  639. * @param {string} participantId - The ID of the participant from which we got an update.
  640. * @param {boolean} newValue - The new value of the raise hand status.
  641. * @returns {void}
  642. */
  643. function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiConference,
  644. participantId: string, newValue: string | number) {
  645. let raisedHandTimestamp;
  646. switch (newValue) {
  647. case undefined:
  648. case 'false':
  649. raisedHandTimestamp = 0;
  650. break;
  651. case 'true':
  652. raisedHandTimestamp = Date.now();
  653. break;
  654. default:
  655. raisedHandTimestamp = parseInt(`${newValue}`, 10);
  656. }
  657. const state = getState();
  658. dispatch(participantUpdated({
  659. conference,
  660. id: participantId,
  661. raisedHandTimestamp
  662. }));
  663. dispatch(raiseHandUpdateQueue({
  664. id: participantId,
  665. raisedHandTimestamp
  666. }));
  667. if (typeof APP !== 'undefined') {
  668. APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
  669. }
  670. const isModerator = isLocalParticipantModerator(state);
  671. const participant = getParticipantById(state, participantId);
  672. let shouldDisplayAllowAction = false;
  673. if (isModerator) {
  674. shouldDisplayAllowAction = isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
  675. || isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
  676. }
  677. const action = shouldDisplayAllowAction ? {
  678. customActionNameKey: [ 'notify.allowAction' ],
  679. customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
  680. } : {};
  681. if (raisedHandTimestamp) {
  682. let notificationTitle;
  683. const participantName = getParticipantDisplayName(state, participantId);
  684. const { raisedHandsQueue } = state['features/base/participants'];
  685. if (raisedHandsQueue.length > 1) {
  686. const raisedHands = raisedHandsQueue.length - 1;
  687. notificationTitle = i18n.t('notify.raisedHands', {
  688. participantName,
  689. raisedHands
  690. });
  691. } else {
  692. notificationTitle = participantName;
  693. }
  694. dispatch(showNotification({
  695. titleKey: 'notify.somebody',
  696. title: notificationTitle,
  697. descriptionKey: 'notify.raisedHand',
  698. concatText: true,
  699. uid: RAISE_HAND_NOTIFICATION_ID,
  700. ...action
  701. }, shouldDisplayAllowAction ? NOTIFICATION_TIMEOUT_TYPE.MEDIUM : NOTIFICATION_TIMEOUT_TYPE.SHORT));
  702. dispatch(playSound(RAISE_HAND_SOUND_ID));
  703. }
  704. }
  705. /**
  706. * Registers sounds related with the participants feature.
  707. *
  708. * @param {Store} store - The redux store.
  709. * @private
  710. * @returns {void}
  711. */
  712. function _registerSounds({ dispatch }: IStore) {
  713. dispatch(
  714. registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
  715. dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
  716. }
  717. /**
  718. * Unregisters sounds related with the participants feature.
  719. *
  720. * @param {Store} store - The redux store.
  721. * @private
  722. * @returns {void}
  723. */
  724. function _unregisterSounds({ dispatch }: IStore) {
  725. dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
  726. dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
  727. }