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

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