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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import i18n from 'i18next';
  2. import { AnyAction } from 'redux';
  3. // @ts-ignore
  4. import { MIN_ASSUMED_BANDWIDTH_BPS } from '../../../../modules/API/constants';
  5. import {
  6. ACTION_PINNED,
  7. ACTION_UNPINNED,
  8. createNotAllowedErrorEvent,
  9. createOfferAnswerFailedEvent,
  10. createPinnedEvent
  11. } from '../../analytics/AnalyticsEvents';
  12. import { sendAnalytics } from '../../analytics/functions';
  13. import { reloadNow } from '../../app/actions';
  14. import { IStore } from '../../app/types';
  15. import { removeLobbyChatParticipant } from '../../chat/actions.any';
  16. import { openDisplayNamePrompt } from '../../display-name/actions';
  17. import { isVpaasMeeting } from '../../jaas/functions';
  18. import { showErrorNotification, showNotification } from '../../notifications/actions';
  19. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  20. import { INotificationProps } from '../../notifications/types';
  21. import { hasDisplayName } from '../../prejoin/utils';
  22. import { stopLocalVideoRecording } from '../../recording/actions.any';
  23. import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
  24. import { iAmVisitor } from '../../visitors/functions';
  25. import { overwriteConfig } from '../config/actions';
  26. import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
  27. import { connectionDisconnected, disconnect } from '../connection/actions';
  28. import { validateJwt } from '../jwt/functions';
  29. import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
  30. import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
  31. import { PARTICIPANT_ROLE } from '../participants/constants';
  32. import {
  33. getLocalParticipant,
  34. getParticipantById,
  35. getPinnedParticipant
  36. } from '../participants/functions';
  37. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  38. import StateListenerRegistry from '../redux/StateListenerRegistry';
  39. import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
  40. import { parseURIString } from '../util/uri';
  41. import {
  42. CONFERENCE_FAILED,
  43. CONFERENCE_JOINED,
  44. CONFERENCE_SUBJECT_CHANGED,
  45. CONFERENCE_WILL_LEAVE,
  46. P2P_STATUS_CHANGED,
  47. SEND_TONES,
  48. SET_ASSUMED_BANDWIDTH_BPS,
  49. SET_PENDING_SUBJECT_CHANGE,
  50. SET_ROOM
  51. } from './actionTypes';
  52. import {
  53. authStatusChanged,
  54. conferenceFailed,
  55. conferenceWillLeave,
  56. createConference,
  57. setLocalSubject,
  58. setSubject,
  59. updateConferenceMetadata
  60. } from './actions';
  61. import { CONFERENCE_LEAVE_REASONS } from './constants';
  62. import {
  63. _addLocalTracksToConference,
  64. _removeLocalTracksFromConference,
  65. forEachConference,
  66. getCurrentConference,
  67. restoreConferenceOptions
  68. } from './functions';
  69. import logger from './logger';
  70. import { IConferenceMetadata } from './reducer';
  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. * Set up state change listener to perform maintenance tasks when the conference
  116. * is left or failed.
  117. */
  118. StateListenerRegistry.register(
  119. state => getCurrentConference(state),
  120. (conference, { dispatch }, previousConference): void => {
  121. if (conference && !previousConference) {
  122. conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: IConferenceMetadata) => {
  123. dispatch(updateConferenceMetadata(metadata));
  124. });
  125. }
  126. if (conference !== previousConference) {
  127. dispatch(updateConferenceMetadata(null));
  128. }
  129. });
  130. /**
  131. * Makes sure to leave a failed conference in order to release any allocated
  132. * resources like peer connections, emit participant left events, etc.
  133. *
  134. * @param {Store} store - The redux store in which the specified {@code action}
  135. * is being dispatched.
  136. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  137. * specified {@code action} to the specified {@code store}.
  138. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  139. * being dispatched in the specified {@code store}.
  140. * @private
  141. * @returns {Object} The value returned by {@code next(action)}.
  142. */
  143. function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  144. const { conference, error } = action;
  145. const result = next(action);
  146. const { enableForcedReload } = getState()['features/base/config'];
  147. if (LocalRecordingManager.isRecordingLocally()) {
  148. dispatch(stopLocalVideoRecording());
  149. }
  150. // Handle specific failure reasons.
  151. switch (error.name) {
  152. case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
  153. if (enableForcedReload) {
  154. dispatch(showErrorNotification({
  155. description: 'Restart initiated because of a bridge failure',
  156. titleKey: 'dialog.sessionRestarted'
  157. }));
  158. }
  159. break;
  160. }
  161. case JitsiConferenceErrors.CONNECTION_ERROR: {
  162. const [ msg ] = error.params;
  163. dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
  164. dispatch(showErrorNotification({
  165. descriptionArguments: { msg },
  166. descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
  167. titleKey: 'connection.CONNFAIL'
  168. }));
  169. break;
  170. }
  171. case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
  172. dispatch(showErrorNotification({
  173. hideErrorSupportLink: true,
  174. descriptionKey: 'dialog.maxUsersLimitReached',
  175. titleKey: 'dialog.maxUsersLimitReachedTitle'
  176. }));
  177. // In case of max users(it can be from a visitor node), let's restore
  178. // oldConfig if any as we will be back to the main prosody.
  179. const newConfig = restoreConferenceOptions(getState);
  180. if (newConfig) {
  181. dispatch(overwriteConfig(newConfig));
  182. dispatch(conferenceWillLeave(conference));
  183. conference.leave()
  184. .then(() => dispatch(disconnect()));
  185. }
  186. break;
  187. }
  188. case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
  189. const [ type, msg ] = error.params;
  190. let descriptionKey;
  191. let titleKey = 'dialog.tokenAuthFailed';
  192. if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_MAIN_PARTICIPANTS) {
  193. descriptionKey = 'visitors.notification.noMainParticipantsDescription';
  194. titleKey = 'visitors.notification.noMainParticipantsTitle';
  195. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_VISITORS_LOBBY) {
  196. descriptionKey = 'visitors.notification.noVisitorLobby';
  197. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.PROMOTION_NOT_ALLOWED) {
  198. descriptionKey = 'visitors.notification.notAllowedPromotion';
  199. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_CREATION_RESTRICTION) {
  200. descriptionKey = 'dialog.errorRoomCreationRestriction';
  201. }
  202. dispatch(showErrorNotification({
  203. descriptionKey,
  204. hideErrorSupportLink: true,
  205. titleKey
  206. }));
  207. sendAnalytics(createNotAllowedErrorEvent(type, msg));
  208. break;
  209. }
  210. case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
  211. sendAnalytics(createOfferAnswerFailedEvent());
  212. break;
  213. }
  214. !error.recoverable
  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. // FIXME: Workaround for the web version. Currently, the creation of the
  221. // conference is handled by /conference.js and appropriate failure handlers
  222. // are set there.
  223. if (typeof APP !== 'undefined') {
  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({
  280. validateInput: hasDisplayName
  281. }));
  282. }
  283. return result;
  284. }
  285. /**
  286. * Notifies the feature base/conference that the action
  287. * {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
  288. * store.
  289. *
  290. * @param {Store} store - The redux store in which the specified {@code action}
  291. * is being dispatched.
  292. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  293. * specified {@code action} to the specified {@code store}.
  294. * @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
  295. * which is being dispatched in the specified {@code store}.
  296. * @private
  297. * @returns {Object} The value returned by {@code next(action)}.
  298. */
  299. async function _connectionEstablished({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  300. const result = next(action);
  301. const { tokenAuthUrl = false } = getState()['features/base/config'];
  302. // if there is token auth URL defined and local participant is using jwt
  303. // this means it is logged in when connection is established, so we can change the state
  304. if (tokenAuthUrl && !isVpaasMeeting(getState())) {
  305. let email;
  306. if (getState()['features/base/jwt'].jwt) {
  307. email = getLocalParticipant(getState())?.email;
  308. }
  309. dispatch(authStatusChanged(true, email || ''));
  310. }
  311. // FIXME: Workaround for the web version. Currently, the creation of the
  312. // conference is handled by /conference.js.
  313. if (typeof APP === 'undefined') {
  314. dispatch(createConference());
  315. return result;
  316. }
  317. return result;
  318. }
  319. /**
  320. * Logs jwt validation errors from xmpp and from the client-side validator.
  321. *
  322. * @param {string} message - The error message from xmpp.
  323. * @param {string} errors - The detailed errors.
  324. * @returns {void}
  325. */
  326. function _logJwtErrors(message: string, errors: string) {
  327. message && logger.error(`JWT error: ${message}`);
  328. errors && logger.error('JWT parsing errors:', errors);
  329. }
  330. /**
  331. * Notifies the feature base/conference that the action
  332. * {@code CONNECTION_FAILED} is being dispatched within a specific redux
  333. * store.
  334. *
  335. * @param {Store} store - The redux store in which the specified {@code action}
  336. * is being dispatched.
  337. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  338. * specified {@code action} to the specified {@code store}.
  339. * @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
  340. * being dispatched in the specified {@code store}.
  341. * @private
  342. * @returns {Object} The value returned by {@code next(action)}.
  343. */
  344. function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  345. const { connection, error } = action;
  346. const { jwt } = getState()['features/base/jwt'];
  347. if (jwt) {
  348. const errors: string = validateJwt(jwt).map((err: any) =>
  349. i18n.t(`dialog.tokenAuthFailedReason.${err.key}`, err.args))
  350. .join(' ');
  351. _logJwtErrors(error.message, errors);
  352. // do not show the notification when we will prompt the user
  353. // for username and password
  354. if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
  355. dispatch(showErrorNotification({
  356. descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
  357. descriptionArguments: { reason: errors },
  358. titleKey: 'dialog.tokenAuthFailedTitle'
  359. }));
  360. }
  361. }
  362. if (error.name === JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED) {
  363. let notificationAction: Function = showNotification;
  364. const notificationProps = {
  365. customActionNameKey: [ 'dialog.rejoinNow' ],
  366. customActionHandler: [ () => dispatch(reloadNow()) ],
  367. descriptionKey: 'notify.connectionFailed'
  368. } as INotificationProps;
  369. const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
  370. const { tenant = '' } = parseURIString(locationURL.href) || {};
  371. if (tenant.startsWith('-') || tenant.endsWith('-')) {
  372. notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';
  373. notificationProps.titleKey = 'notify.invalidTenant';
  374. notificationAction = showErrorNotification;
  375. } else if (tenant.length > 63) {
  376. notificationProps.descriptionKey = 'notify.invalidTenantLengthDescription';
  377. notificationProps.titleKey = 'notify.invalidTenant';
  378. notificationAction = showErrorNotification;
  379. }
  380. dispatch(notificationAction(notificationProps, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  381. }
  382. const result = next(action);
  383. _removeUnloadHandler(getState);
  384. forEachConference(getState, conference => {
  385. // TODO: revisit this
  386. // It feels that it would make things easier if JitsiConference
  387. // in lib-jitsi-meet would monitor it's connection and emit
  388. // CONFERENCE_FAILED when it's dropped. It has more knowledge on
  389. // whether it can recover or not. But because the reload screen
  390. // and the retry logic is implemented in the app maybe it can be
  391. // left this way for now.
  392. if (conference.getConnection() === connection) {
  393. // XXX Note that on mobile the error type passed to
  394. // connectionFailed is always an object with .name property.
  395. // This fact needs to be checked prior to enabling this logic on
  396. // web.
  397. const conferenceAction = conferenceFailed(conference, error.name);
  398. // Copy the recoverable flag if set on the CONNECTION_FAILED
  399. // action to not emit recoverable action caused by
  400. // a non-recoverable one.
  401. if (typeof error.recoverable !== 'undefined') {
  402. conferenceAction.error.recoverable = error.recoverable;
  403. }
  404. dispatch(conferenceAction);
  405. }
  406. return true;
  407. });
  408. return result;
  409. }
  410. /**
  411. * Notifies the feature base/conference that the action
  412. * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
  413. * redux store.
  414. *
  415. * @param {Store} store - The redux store in which the specified {@code action}
  416. * is being dispatched.
  417. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  418. * specified {@code action} to the specified {@code store}.
  419. * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
  420. * which is being dispatched in the specified {@code store}.
  421. * @private
  422. * @returns {Object} The value returned by {@code next(action)}.
  423. */
  424. function _conferenceSubjectChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  425. const result = next(action);
  426. const { subject } = getState()['features/base/conference'];
  427. if (subject) {
  428. dispatch({
  429. type: SET_PENDING_SUBJECT_CHANGE,
  430. subject: undefined
  431. });
  432. }
  433. typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
  434. return result;
  435. }
  436. /**
  437. * Notifies the feature base/conference that the action
  438. * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
  439. * store.
  440. *
  441. * @private
  442. * @param {Object} store - The redux store.
  443. * @returns {void}
  444. */
  445. function _conferenceWillLeave({ getState }: IStore) {
  446. _removeUnloadHandler(getState);
  447. }
  448. /**
  449. * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
  450. * is being dispatched within a specific redux store. Pins the specified remote
  451. * participant in the associated conference, ignores the local participant.
  452. *
  453. * @param {Store} store - The redux store in which the specified {@code action}
  454. * is being dispatched.
  455. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  456. * specified {@code action} to the specified {@code store}.
  457. * @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
  458. * being dispatched in the specified {@code store}.
  459. * @private
  460. * @returns {Object} The value returned by {@code next(action)}.
  461. */
  462. function _pinParticipant({ getState }: IStore, next: Function, action: AnyAction) {
  463. const state = getState();
  464. const { conference } = state['features/base/conference'];
  465. if (!conference) {
  466. return next(action);
  467. }
  468. const id = action.participant.id;
  469. const participantById = getParticipantById(state, id);
  470. const pinnedParticipant = getPinnedParticipant(state);
  471. const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
  472. const local
  473. = participantById?.local
  474. || (!id && pinnedParticipant?.local);
  475. let participantIdForEvent;
  476. if (local) {
  477. participantIdForEvent = local;
  478. } else {
  479. participantIdForEvent
  480. = actionName === ACTION_PINNED ? id : pinnedParticipant?.id;
  481. }
  482. sendAnalytics(createPinnedEvent(
  483. actionName,
  484. participantIdForEvent,
  485. {
  486. local,
  487. 'participant_count': conference.getParticipantCount()
  488. }));
  489. return next(action);
  490. }
  491. /**
  492. * Removes the unload handler.
  493. *
  494. * @param {Function} getState - The redux getState function.
  495. * @returns {void}
  496. */
  497. function _removeUnloadHandler(getState: IStore['getState']) {
  498. if (typeof beforeUnloadHandler !== 'undefined') {
  499. const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
  500. window.removeEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  501. beforeUnloadHandler = undefined;
  502. }
  503. }
  504. /**
  505. * Requests the specified tones to be played.
  506. *
  507. * @param {Store} store - The redux store in which the specified {@code action}
  508. * is being dispatched.
  509. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  510. * specified {@code action} to the specified {@code store}.
  511. * @param {Action} action - The redux action {@code SEND_TONES} which is
  512. * being dispatched in the specified {@code store}.
  513. * @private
  514. * @returns {Object} The value returned by {@code next(action)}.
  515. */
  516. function _sendTones({ getState }: IStore, next: Function, action: AnyAction) {
  517. const state = getState();
  518. const { conference } = state['features/base/conference'];
  519. if (conference) {
  520. const { duration, tones, pause } = action;
  521. conference.sendTones(tones, duration, pause);
  522. }
  523. return next(action);
  524. }
  525. /**
  526. * Notifies the feature base/conference that the action
  527. * {@code SET_ROOM} is being dispatched within a specific
  528. * redux store.
  529. *
  530. * @param {Store} store - The redux store in which the specified {@code action}
  531. * is being dispatched.
  532. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  533. * specified {@code action} to the specified {@code store}.
  534. * @param {Action} action - The redux action {@code SET_ROOM}
  535. * which is being dispatched in the specified {@code store}.
  536. * @private
  537. * @returns {Object} The value returned by {@code next(action)}.
  538. */
  539. function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  540. const state = getState();
  541. const { localSubject, subject } = state['features/base/config'];
  542. const { room } = action;
  543. if (room) {
  544. // Set the stored subject.
  545. localSubject && dispatch(setLocalSubject(localSubject));
  546. subject && dispatch(setSubject(subject));
  547. }
  548. return next(action);
  549. }
  550. /**
  551. * Notifies the feature base/conference that the action {@code TRACK_ADDED}
  552. * or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
  553. *
  554. * @param {Store} store - The redux store in which the specified {@code action}
  555. * is being dispatched.
  556. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  557. * specified {@code action} to the specified {@code store}.
  558. * @param {Action} action - The redux action {@code TRACK_ADDED} or
  559. * {@code TRACK_REMOVED} which is being dispatched in the specified
  560. * {@code store}.
  561. * @private
  562. * @returns {Object} The value returned by {@code next(action)}.
  563. */
  564. function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
  565. const track = action.track;
  566. // TODO All track swapping should happen here instead of conference.js.
  567. if (track?.local) {
  568. const { getState } = store;
  569. const state = getState();
  570. const conference = getCurrentConference(state);
  571. let promise;
  572. if (conference) {
  573. const jitsiTrack = action.track.jitsiTrack;
  574. if (action.type === TRACK_ADDED) {
  575. // If gUM is slow and tracks are created after the user has already joined the conference, avoid
  576. // adding the tracks to the conference if the user is a visitor.
  577. if (!iAmVisitor(state)) {
  578. promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
  579. }
  580. } else {
  581. promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
  582. }
  583. if (promise) {
  584. return promise.then(() => next(action));
  585. }
  586. }
  587. }
  588. return next(action);
  589. }
  590. /**
  591. * Updates the conference object when the local participant is updated.
  592. *
  593. * @param {Store} store - The redux store in which the specified {@code action}
  594. * is being dispatched.
  595. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  596. * specified {@code action} to the specified {@code store}.
  597. * @param {Action} action - The redux action which is being dispatched in the
  598. * specified {@code store}.
  599. * @private
  600. * @returns {Object} The value returned by {@code next(action)}.
  601. */
  602. function _updateLocalParticipantInConference({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  603. const { conference } = getState()['features/base/conference'];
  604. const { participant } = action;
  605. const result = next(action);
  606. const localParticipant = getLocalParticipant(getState);
  607. if (conference && participant.id === localParticipant?.id) {
  608. if ('name' in participant) {
  609. conference.setDisplayName(participant.name);
  610. }
  611. if ('isSilent' in participant) {
  612. conference.setIsSilent(participant.isSilent);
  613. }
  614. if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
  615. const { pendingSubjectChange, subject } = getState()['features/base/conference'];
  616. // When the local user role is updated to moderator and we have a pending subject change
  617. // which was not reflected we need to set it (the first time we tried was before becoming moderator).
  618. if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
  619. dispatch(setSubject(pendingSubjectChange));
  620. }
  621. }
  622. }
  623. return result;
  624. }
  625. /**
  626. * Notifies the external API that the action {@code P2P_STATUS_CHANGED}
  627. * is being dispatched within a specific redux store.
  628. *
  629. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  630. * specified {@code action} to the specified {@code store}.
  631. * @param {Action} action - The redux action {@code P2P_STATUS_CHANGED}
  632. * which is being dispatched in the specified {@code store}.
  633. * @private
  634. * @returns {Object} The value returned by {@code next(action)}.
  635. */
  636. function _p2pStatusChanged(next: Function, action: AnyAction) {
  637. const result = next(action);
  638. if (typeof APP !== 'undefined') {
  639. APP.API.notifyP2pStatusChanged(action.p2p);
  640. }
  641. return result;
  642. }
  643. /**
  644. * Notifies the feature base/conference that the action
  645. * {@code SET_ASSUMED_BANDWIDTH_BPS} is being dispatched within a specific
  646. * redux store.
  647. *
  648. * @param {Store} store - The redux store in which the specified {@code action}
  649. * is being dispatched.
  650. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  651. * specified {@code action} to the specified {@code store}.
  652. * @param {Action} action - The redux action {@code SET_ASSUMED_BANDWIDTH_BPS}
  653. * which is being dispatched in the specified {@code store}.
  654. * @private
  655. * @returns {Object} The value returned by {@code next(action)}.
  656. */
  657. function _setAssumedBandwidthBps({ getState }: IStore, next: Function, action: AnyAction) {
  658. const state = getState();
  659. const conference = getCurrentConference(state);
  660. const payload = Number(action.assumedBandwidthBps);
  661. const assumedBandwidthBps = isNaN(payload) || payload < MIN_ASSUMED_BANDWIDTH_BPS
  662. ? MIN_ASSUMED_BANDWIDTH_BPS
  663. : payload;
  664. if (conference) {
  665. conference.setAssumedBandwidthBps(assumedBandwidthBps);
  666. }
  667. return next(action);
  668. }