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.any.ts 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import { AnyAction } from 'redux';
  2. // @ts-ignore
  3. import { MIN_ASSUMED_BANDWIDTH_BPS } from '../../../../modules/API/constants';
  4. import {
  5. ACTION_PINNED,
  6. ACTION_UNPINNED,
  7. createOfferAnswerFailedEvent,
  8. createPinnedEvent
  9. } from '../../analytics/AnalyticsEvents';
  10. import { sendAnalytics } from '../../analytics/functions';
  11. import { reloadNow } from '../../app/actions';
  12. import { IReduxState, IStore } from '../../app/types';
  13. import { removeLobbyChatParticipant } from '../../chat/actions.any';
  14. import { openDisplayNamePrompt } from '../../display-name/actions';
  15. import { readyToClose } from '../../mobile/external-api/actions';
  16. import { showErrorNotification, showWarningNotification } from '../../notifications/actions';
  17. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  18. import { stopLocalVideoRecording } from '../../recording/actions.any';
  19. import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
  20. import { setIAmVisitor } from '../../visitors/actions';
  21. import { iAmVisitor } from '../../visitors/functions';
  22. import { overwriteConfig } from '../config/actions';
  23. import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
  24. import { connect, connectionDisconnected, disconnect } from '../connection/actions';
  25. import { validateJwt } from '../jwt/functions';
  26. import { JitsiConferenceErrors } from '../lib-jitsi-meet';
  27. import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
  28. import { PARTICIPANT_ROLE } from '../participants/constants';
  29. import {
  30. getLocalParticipant,
  31. getParticipantById,
  32. getPinnedParticipant
  33. } from '../participants/functions';
  34. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  35. import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
  36. import { destroyLocalTracks } from '../tracks/actions.any';
  37. import {
  38. CONFERENCE_FAILED,
  39. CONFERENCE_JOINED,
  40. CONFERENCE_SUBJECT_CHANGED,
  41. CONFERENCE_WILL_LEAVE,
  42. P2P_STATUS_CHANGED,
  43. SEND_TONES,
  44. SET_ASSUMED_BANDWIDTH_BPS,
  45. SET_PENDING_SUBJECT_CHANGE,
  46. SET_ROOM
  47. } from './actionTypes';
  48. import {
  49. conferenceFailed,
  50. conferenceWillLeave,
  51. createConference,
  52. leaveConference,
  53. setLocalSubject,
  54. setSubject
  55. } from './actions';
  56. import {
  57. CONFERENCE_DESTROYED_LEAVE_TIMEOUT,
  58. CONFERENCE_LEAVE_REASONS,
  59. TRIGGER_READY_TO_CLOSE_REASONS
  60. } from './constants';
  61. import {
  62. _addLocalTracksToConference,
  63. _removeLocalTracksFromConference,
  64. forEachConference,
  65. getCurrentConference,
  66. getVisitorOptions,
  67. restoreConferenceOptions
  68. } from './functions';
  69. import logger from './logger';
  70. /**
  71. * Handler for before unload event.
  72. */
  73. let beforeUnloadHandler: ((e?: any) => void) | undefined;
  74. /**
  75. * Implements the middleware of the feature base/conference.
  76. *
  77. * @param {Store} store - The redux store.
  78. * @returns {Function}
  79. */
  80. MiddlewareRegistry.register(store => next => action => {
  81. switch (action.type) {
  82. case CONFERENCE_FAILED:
  83. return _conferenceFailed(store, next, action);
  84. case CONFERENCE_JOINED:
  85. return _conferenceJoined(store, next, action);
  86. case CONNECTION_ESTABLISHED:
  87. return _connectionEstablished(store, next, action);
  88. case CONNECTION_FAILED:
  89. return _connectionFailed(store, next, action);
  90. case CONFERENCE_SUBJECT_CHANGED:
  91. return _conferenceSubjectChanged(store, next, action);
  92. case CONFERENCE_WILL_LEAVE:
  93. _conferenceWillLeave(store);
  94. break;
  95. case P2P_STATUS_CHANGED:
  96. return _p2pStatusChanged(next, action);
  97. case PARTICIPANT_UPDATED:
  98. return _updateLocalParticipantInConference(store, next, action);
  99. case PIN_PARTICIPANT:
  100. return _pinParticipant(store, next, action);
  101. case SEND_TONES:
  102. return _sendTones(store, next, action);
  103. case SET_ROOM:
  104. return _setRoom(store, next, action);
  105. case TRACK_ADDED:
  106. case TRACK_REMOVED:
  107. return _trackAddedOrRemoved(store, next, action);
  108. case SET_ASSUMED_BANDWIDTH_BPS:
  109. return _setAssumedBandwidthBps(store, next, action);
  110. }
  111. return next(action);
  112. });
  113. /**
  114. * Makes sure to leave a failed conference in order to release any allocated
  115. * resources like peer connections, emit participant left events, etc.
  116. *
  117. * @param {Store} store - The redux store in which the specified {@code action}
  118. * is being dispatched.
  119. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  120. * specified {@code action} to the specified {@code store}.
  121. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  122. * being dispatched in the specified {@code store}.
  123. * @private
  124. * @returns {Object} The value returned by {@code next(action)}.
  125. */
  126. function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  127. const { conference, error } = action;
  128. if (error.name === JitsiConferenceErrors.REDIRECTED) {
  129. if (typeof error.recoverable === 'undefined') {
  130. error.recoverable = true;
  131. }
  132. }
  133. const result = next(action);
  134. const { enableForcedReload } = getState()['features/base/config'];
  135. // Handle specific failure reasons.
  136. switch (error.name) {
  137. case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
  138. const [ reason ] = error.params;
  139. dispatch(showWarningNotification({
  140. description: reason,
  141. titleKey: 'dialog.sessTerminated'
  142. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  143. if (TRIGGER_READY_TO_CLOSE_REASONS.includes(reason)) {
  144. if (typeof APP === 'undefined') {
  145. dispatch(readyToClose());
  146. } else {
  147. APP.API.notifyReadyToClose();
  148. }
  149. setTimeout(() => dispatch(leaveConference()), CONFERENCE_DESTROYED_LEAVE_TIMEOUT);
  150. }
  151. break;
  152. }
  153. case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
  154. if (enableForcedReload) {
  155. dispatch(showErrorNotification({
  156. description: 'Restart initiated because of a bridge failure',
  157. titleKey: 'dialog.sessionRestarted'
  158. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  159. }
  160. break;
  161. }
  162. case JitsiConferenceErrors.CONNECTION_ERROR: {
  163. const [ msg ] = error.params;
  164. dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
  165. dispatch(showErrorNotification({
  166. descriptionArguments: { msg },
  167. descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
  168. titleKey: 'connection.CONNFAIL'
  169. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  170. break;
  171. }
  172. case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
  173. if (typeof APP === 'undefined') {
  174. // in case of max users(it can be from a visitor node), let's restore
  175. // oldConfig if any as we will be back to the main prosody
  176. const newConfig = restoreConferenceOptions(getState);
  177. if (newConfig) {
  178. dispatch(overwriteConfig(newConfig)) // @ts-ignore
  179. .then(dispatch(conferenceWillLeave(conference)))
  180. .then(conference.leave())
  181. .then(dispatch(disconnect()))
  182. .then(dispatch(connect()));
  183. }
  184. }
  185. break;
  186. }
  187. case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
  188. sendAnalytics(createOfferAnswerFailedEvent());
  189. break;
  190. case JitsiConferenceErrors.REDIRECTED: {
  191. // once conference.js is gone this can be removed and both
  192. // redirect logics to be merged
  193. if (typeof APP === 'undefined') {
  194. const newConfig = getVisitorOptions(getState, error.params);
  195. if (!newConfig) {
  196. logger.warn('Not redirected missing params');
  197. break;
  198. }
  199. const [ vnode ] = error.params;
  200. dispatch(overwriteConfig(newConfig)) // @ts-ignore
  201. .then(dispatch(conferenceWillLeave(conference)))
  202. .then(conference.leave())
  203. .then(dispatch(disconnect()))
  204. .then(dispatch(setIAmVisitor(Boolean(vnode))))
  205. // we do not clear local tracks on error, so we need to manually clear them
  206. .then(dispatch(destroyLocalTracks()))
  207. .then(dispatch(connect()));
  208. }
  209. break;
  210. }
  211. }
  212. if (typeof APP === 'undefined') {
  213. !error.recoverable
  214. && conference
  215. && conference.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
  216. // Even though we don't care too much about the failure, it may be
  217. // good to know that it happen, so log it (on the info level).
  218. logger.info('JitsiConference.leave() rejected with:', reason);
  219. });
  220. } else {
  221. // FIXME: Workaround for the web version. Currently, the creation of the
  222. // conference is handled by /conference.js and appropriate failure handlers
  223. // are set there.
  224. _removeUnloadHandler(getState);
  225. }
  226. if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
  227. dispatch(conferenceWillLeave(conference));
  228. dispatch(reloadNow());
  229. }
  230. return result;
  231. }
  232. /**
  233. * Does extra sync up on properties that may need to be updated after the
  234. * conference was joined.
  235. *
  236. * @param {Store} store - The redux store in which the specified {@code action}
  237. * is being dispatched.
  238. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  239. * specified {@code action} to the specified {@code store}.
  240. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  241. * being dispatched in the specified {@code store}.
  242. * @private
  243. * @returns {Object} The value returned by {@code next(action)}.
  244. */
  245. function _conferenceJoined({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  246. const result = next(action);
  247. const { conference } = action;
  248. const { pendingSubjectChange } = getState()['features/base/conference'];
  249. const {
  250. disableBeforeUnloadHandlers = false,
  251. requireDisplayName
  252. } = getState()['features/base/config'];
  253. dispatch(removeLobbyChatParticipant(true));
  254. pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
  255. // FIXME: Very dirty solution. This will work on web only.
  256. // When the user closes the window or quits the browser, lib-jitsi-meet
  257. // handles the process of leaving the conference. This is temporary solution
  258. // that should cover the described use case as part of the effort to
  259. // implement the conferenceWillLeave action for web.
  260. beforeUnloadHandler = (e?: any) => {
  261. if (LocalRecordingManager.isRecordingLocally()) {
  262. dispatch(stopLocalVideoRecording());
  263. if (e) {
  264. e.preventDefault();
  265. e.returnValue = null;
  266. }
  267. }
  268. dispatch(conferenceWillLeave(conference));
  269. };
  270. if (!iAmVisitor(getState())) {
  271. // if a visitor is promoted back to main room and want to join an empty breakout room
  272. // we need to send iq to jicofo, so it can join/create the breakout room
  273. dispatch(overwriteConfig({ disableFocus: false }));
  274. }
  275. window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  276. if (requireDisplayName
  277. && !getLocalParticipant(getState)?.name
  278. && !conference.isHidden()) {
  279. dispatch(openDisplayNamePrompt(undefined));
  280. }
  281. return result;
  282. }
  283. /**
  284. * Notifies the feature base/conference that the action
  285. * {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
  286. * store.
  287. *
  288. * @param {Store} store - The redux store in which the specified {@code action}
  289. * is being dispatched.
  290. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  291. * specified {@code action} to the specified {@code store}.
  292. * @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
  293. * which is being dispatched in the specified {@code store}.
  294. * @private
  295. * @returns {Object} The value returned by {@code next(action)}.
  296. */
  297. function _connectionEstablished({ dispatch }: IStore, next: Function, action: AnyAction) {
  298. const result = next(action);
  299. // FIXME: Workaround for the web version. Currently, the creation of the
  300. // conference is handled by /conference.js.
  301. typeof APP === 'undefined' && dispatch(createConference());
  302. return result;
  303. }
  304. /**
  305. * Logs jwt validation errors from xmpp and from the client-side validator.
  306. *
  307. * @param {string} message -The error message from xmpp.
  308. * @param {Object} state - The redux state.
  309. * @returns {void}
  310. */
  311. function _logJwtErrors(message: string, state: IReduxState) {
  312. const { jwt } = state['features/base/jwt'];
  313. if (!jwt) {
  314. return;
  315. }
  316. const errorKeys = validateJwt(jwt);
  317. message && logger.error(`JWT error: ${message}`);
  318. errorKeys.length && logger.error('JWT parsing error:', errorKeys);
  319. }
  320. /**
  321. * Notifies the feature base/conference that the action
  322. * {@code CONNECTION_FAILED} is being dispatched within a specific redux
  323. * store.
  324. *
  325. * @param {Store} store - The redux store in which the specified {@code action}
  326. * is being dispatched.
  327. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  328. * specified {@code action} to the specified {@code store}.
  329. * @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
  330. * being dispatched in the specified {@code store}.
  331. * @private
  332. * @returns {Object} The value returned by {@code next(action)}.
  333. */
  334. function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  335. _logJwtErrors(action.error.message, getState());
  336. const result = next(action);
  337. _removeUnloadHandler(getState);
  338. // FIXME: Workaround for the web version. Currently, the creation of the
  339. // conference is handled by /conference.js and appropriate failure handlers
  340. // are set there.
  341. if (typeof APP === 'undefined') {
  342. const { connection } = action;
  343. const { error } = action;
  344. forEachConference(getState, conference => {
  345. // It feels that it would make things easier if JitsiConference
  346. // in lib-jitsi-meet would monitor it's connection and emit
  347. // CONFERENCE_FAILED when it's dropped. It has more knowledge on
  348. // whether it can recover or not. But because the reload screen
  349. // and the retry logic is implemented in the app maybe it can be
  350. // left this way for now.
  351. if (conference.getConnection() === connection) {
  352. // XXX Note that on mobile the error type passed to
  353. // connectionFailed is always an object with .name property.
  354. // This fact needs to be checked prior to enabling this logic on
  355. // web.
  356. const conferenceAction
  357. = conferenceFailed(conference, error.name);
  358. // Copy the recoverable flag if set on the CONNECTION_FAILED
  359. // action to not emit recoverable action caused by
  360. // a non-recoverable one.
  361. if (typeof error.recoverable !== 'undefined') {
  362. conferenceAction.error.recoverable = error.recoverable;
  363. }
  364. dispatch(conferenceAction);
  365. }
  366. return true;
  367. });
  368. }
  369. return result;
  370. }
  371. /**
  372. * Notifies the feature base/conference that the action
  373. * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
  374. * redux store.
  375. *
  376. * @param {Store} store - The redux store in which the specified {@code action}
  377. * is being dispatched.
  378. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  379. * specified {@code action} to the specified {@code store}.
  380. * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
  381. * which is being dispatched in the specified {@code store}.
  382. * @private
  383. * @returns {Object} The value returned by {@code next(action)}.
  384. */
  385. function _conferenceSubjectChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  386. const result = next(action);
  387. const { subject } = getState()['features/base/conference'];
  388. if (subject) {
  389. dispatch({
  390. type: SET_PENDING_SUBJECT_CHANGE,
  391. subject: undefined
  392. });
  393. }
  394. typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
  395. return result;
  396. }
  397. /**
  398. * Notifies the feature base/conference that the action
  399. * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
  400. * store.
  401. *
  402. * @private
  403. * @param {Object} store - The redux store.
  404. * @returns {void}
  405. */
  406. function _conferenceWillLeave({ getState }: IStore) {
  407. _removeUnloadHandler(getState);
  408. }
  409. /**
  410. * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
  411. * is being dispatched within a specific redux store. Pins the specified remote
  412. * participant in the associated conference, ignores the local participant.
  413. *
  414. * @param {Store} store - The redux store in which the specified {@code action}
  415. * is being dispatched.
  416. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  417. * specified {@code action} to the specified {@code store}.
  418. * @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
  419. * being dispatched in the specified {@code store}.
  420. * @private
  421. * @returns {Object} The value returned by {@code next(action)}.
  422. */
  423. function _pinParticipant({ getState }: IStore, next: Function, action: AnyAction) {
  424. const state = getState();
  425. const { conference } = state['features/base/conference'];
  426. if (!conference) {
  427. return next(action);
  428. }
  429. const id = action.participant.id;
  430. const participantById = getParticipantById(state, id);
  431. const pinnedParticipant = getPinnedParticipant(state);
  432. const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
  433. const local
  434. = participantById?.local
  435. || (!id && pinnedParticipant && pinnedParticipant.local);
  436. let participantIdForEvent;
  437. if (local) {
  438. participantIdForEvent = local;
  439. } else {
  440. participantIdForEvent
  441. = actionName === ACTION_PINNED ? id : pinnedParticipant?.id;
  442. }
  443. sendAnalytics(createPinnedEvent(
  444. actionName,
  445. participantIdForEvent,
  446. {
  447. local,
  448. 'participant_count': conference.getParticipantCount()
  449. }));
  450. return next(action);
  451. }
  452. /**
  453. * Removes the unload handler.
  454. *
  455. * @param {Function} getState - The redux getState function.
  456. * @returns {void}
  457. */
  458. function _removeUnloadHandler(getState: IStore['getState']) {
  459. if (typeof beforeUnloadHandler !== 'undefined') {
  460. const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
  461. window.removeEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  462. beforeUnloadHandler = undefined;
  463. }
  464. }
  465. /**
  466. * Requests the specified tones to be played.
  467. *
  468. * @param {Store} store - The redux store in which the specified {@code action}
  469. * is being dispatched.
  470. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  471. * specified {@code action} to the specified {@code store}.
  472. * @param {Action} action - The redux action {@code SEND_TONES} which is
  473. * being dispatched in the specified {@code store}.
  474. * @private
  475. * @returns {Object} The value returned by {@code next(action)}.
  476. */
  477. function _sendTones({ getState }: IStore, next: Function, action: AnyAction) {
  478. const state = getState();
  479. const { conference } = state['features/base/conference'];
  480. if (conference) {
  481. const { duration, tones, pause } = action;
  482. conference.sendTones(tones, duration, pause);
  483. }
  484. return next(action);
  485. }
  486. /**
  487. * Notifies the feature base/conference that the action
  488. * {@code SET_ROOM} is being dispatched within a specific
  489. * redux store.
  490. *
  491. * @param {Store} store - The redux store in which the specified {@code action}
  492. * is being dispatched.
  493. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  494. * specified {@code action} to the specified {@code store}.
  495. * @param {Action} action - The redux action {@code SET_ROOM}
  496. * which is being dispatched in the specified {@code store}.
  497. * @private
  498. * @returns {Object} The value returned by {@code next(action)}.
  499. */
  500. function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  501. const state = getState();
  502. const { localSubject, subject } = state['features/base/config'];
  503. const { room } = action;
  504. if (room) {
  505. // Set the stored subject.
  506. dispatch(setLocalSubject(localSubject ?? ''));
  507. dispatch(setSubject(subject ?? ''));
  508. }
  509. return next(action);
  510. }
  511. /**
  512. * Synchronizes local tracks from state with local tracks in JitsiConference
  513. * instance.
  514. *
  515. * @param {Store} store - The redux store.
  516. * @param {Object} action - Action object.
  517. * @private
  518. * @returns {Promise}
  519. */
  520. function _syncConferenceLocalTracksWithState({ getState }: IStore, action: AnyAction) {
  521. const state = getState();
  522. const conference = getCurrentConference(state);
  523. let promise;
  524. if (conference) {
  525. const track = action.track.jitsiTrack;
  526. if (action.type === TRACK_ADDED) {
  527. // If gUM is slow and tracks are created after the user has already joined the conference, avoid
  528. // adding the tracks to the conference if the user is a visitor.
  529. if (!iAmVisitor(state)) {
  530. promise = _addLocalTracksToConference(conference, [ track ]);
  531. }
  532. } else {
  533. promise = _removeLocalTracksFromConference(conference, [ track ]);
  534. }
  535. }
  536. return promise || Promise.resolve();
  537. }
  538. /**
  539. * Notifies the feature base/conference that the action {@code TRACK_ADDED}
  540. * or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
  541. *
  542. * @param {Store} store - The redux store in which the specified {@code action}
  543. * is being dispatched.
  544. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  545. * specified {@code action} to the specified {@code store}.
  546. * @param {Action} action - The redux action {@code TRACK_ADDED} or
  547. * {@code TRACK_REMOVED} which is being dispatched in the specified
  548. * {@code store}.
  549. * @private
  550. * @returns {Object} The value returned by {@code next(action)}.
  551. */
  552. function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
  553. const track = action.track;
  554. // TODO All track swapping should happen here instead of conference.js.
  555. if (track?.local) {
  556. return (
  557. _syncConferenceLocalTracksWithState(store, action)
  558. .then(() => next(action)));
  559. }
  560. return next(action);
  561. }
  562. /**
  563. * Updates the conference object when the local participant is updated.
  564. *
  565. * @param {Store} store - The redux store in which the specified {@code action}
  566. * is being dispatched.
  567. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  568. * specified {@code action} to the specified {@code store}.
  569. * @param {Action} action - The redux action which is being dispatched in the
  570. * specified {@code store}.
  571. * @private
  572. * @returns {Object} The value returned by {@code next(action)}.
  573. */
  574. function _updateLocalParticipantInConference({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  575. const { conference } = getState()['features/base/conference'];
  576. const { participant } = action;
  577. const result = next(action);
  578. const localParticipant = getLocalParticipant(getState);
  579. if (conference && participant.id === localParticipant?.id) {
  580. if ('name' in participant) {
  581. conference.setDisplayName(participant.name);
  582. }
  583. if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
  584. const { pendingSubjectChange, subject } = getState()['features/base/conference'];
  585. // When the local user role is updated to moderator and we have a pending subject change
  586. // which was not reflected we need to set it (the first time we tried was before becoming moderator).
  587. if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
  588. dispatch(setSubject(pendingSubjectChange));
  589. }
  590. }
  591. }
  592. return result;
  593. }
  594. /**
  595. * Notifies the external API that the action {@code P2P_STATUS_CHANGED}
  596. * is being dispatched within a specific redux store.
  597. *
  598. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  599. * specified {@code action} to the specified {@code store}.
  600. * @param {Action} action - The redux action {@code P2P_STATUS_CHANGED}
  601. * which is being dispatched in the specified {@code store}.
  602. * @private
  603. * @returns {Object} The value returned by {@code next(action)}.
  604. */
  605. function _p2pStatusChanged(next: Function, action: AnyAction) {
  606. const result = next(action);
  607. if (typeof APP !== 'undefined') {
  608. APP.API.notifyP2pStatusChanged(action.p2p);
  609. }
  610. return result;
  611. }
  612. /**
  613. * Notifies the feature base/conference that the action
  614. * {@code SET_ASSUMED_BANDWIDTH_BPS} is being dispatched within a specific
  615. * redux store.
  616. *
  617. * @param {Store} store - The redux store in which the specified {@code action}
  618. * is being dispatched.
  619. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  620. * specified {@code action} to the specified {@code store}.
  621. * @param {Action} action - The redux action {@code SET_ASSUMED_BANDWIDTH_BPS}
  622. * which is being dispatched in the specified {@code store}.
  623. * @private
  624. * @returns {Object} The value returned by {@code next(action)}.
  625. */
  626. function _setAssumedBandwidthBps({ getState }: IStore, next: Function, action: AnyAction) {
  627. const state = getState();
  628. const conference = getCurrentConference(state);
  629. const payload = Number(action.assumedBandwidthBps);
  630. const assumedBandwidthBps = isNaN(payload) || payload < MIN_ASSUMED_BANDWIDTH_BPS
  631. ? MIN_ASSUMED_BANDWIDTH_BPS
  632. : payload;
  633. if (conference) {
  634. conference.setAssumedBandwidthBps(assumedBandwidthBps);
  635. }
  636. return next(action);
  637. }