選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

middleware.any.ts 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  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. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  169. break;
  170. }
  171. case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
  172. dispatch(showErrorNotification({
  173. hideErrorSupportLink: true,
  174. descriptionKey: 'dialog.maxUsersLimitReached',
  175. titleKey: 'dialog.maxUsersLimitReachedTitle'
  176. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  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. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  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
  216. && conference.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
  217. // Even though we don't care too much about the failure, it may be
  218. // good to know that it happen, so log it (on the info level).
  219. logger.info('JitsiConference.leave() rejected with:', reason);
  220. });
  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. if (typeof APP !== 'undefined') {
  225. _removeUnloadHandler(getState);
  226. }
  227. if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
  228. dispatch(conferenceWillLeave(conference));
  229. dispatch(reloadNow());
  230. }
  231. return result;
  232. }
  233. /**
  234. * Does extra sync up on properties that may need to be updated after the
  235. * conference was joined.
  236. *
  237. * @param {Store} store - The redux store in which the specified {@code action}
  238. * is being dispatched.
  239. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  240. * specified {@code action} to the specified {@code store}.
  241. * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
  242. * being dispatched in the specified {@code store}.
  243. * @private
  244. * @returns {Object} The value returned by {@code next(action)}.
  245. */
  246. function _conferenceJoined({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  247. const result = next(action);
  248. const { conference } = action;
  249. const { pendingSubjectChange } = getState()['features/base/conference'];
  250. const {
  251. disableBeforeUnloadHandlers = false,
  252. requireDisplayName
  253. } = getState()['features/base/config'];
  254. dispatch(removeLobbyChatParticipant(true));
  255. pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
  256. // FIXME: Very dirty solution. This will work on web only.
  257. // When the user closes the window or quits the browser, lib-jitsi-meet
  258. // handles the process of leaving the conference. This is temporary solution
  259. // that should cover the described use case as part of the effort to
  260. // implement the conferenceWillLeave action for web.
  261. beforeUnloadHandler = (e?: any) => {
  262. if (LocalRecordingManager.isRecordingLocally()) {
  263. dispatch(stopLocalVideoRecording());
  264. if (e) {
  265. e.preventDefault();
  266. e.returnValue = null;
  267. }
  268. }
  269. dispatch(conferenceWillLeave(conference));
  270. };
  271. if (!iAmVisitor(getState())) {
  272. // if a visitor is promoted back to main room and want to join an empty breakout room
  273. // we need to send iq to jicofo, so it can join/create the breakout room
  274. dispatch(overwriteConfig({ disableFocus: false }));
  275. }
  276. window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  277. if (requireDisplayName
  278. && !getLocalParticipant(getState)?.name
  279. && !conference.isHidden()) {
  280. dispatch(openDisplayNamePrompt({
  281. validateInput: hasDisplayName
  282. }));
  283. }
  284. return result;
  285. }
  286. /**
  287. * Notifies the feature base/conference that the action
  288. * {@code CONNECTION_ESTABLISHED} is being dispatched within a specific redux
  289. * store.
  290. *
  291. * @param {Store} store - The redux store in which the specified {@code action}
  292. * is being dispatched.
  293. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  294. * specified {@code action} to the specified {@code store}.
  295. * @param {Action} action - The redux action {@code CONNECTION_ESTABLISHED}
  296. * which is being dispatched in the specified {@code store}.
  297. * @private
  298. * @returns {Object} The value returned by {@code next(action)}.
  299. */
  300. async function _connectionEstablished({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  301. const result = next(action);
  302. const { tokenAuthUrl = false } = getState()['features/base/config'];
  303. // if there is token auth URL defined and local participant is using jwt
  304. // this means it is logged in when connection is established, so we can change the state
  305. if (tokenAuthUrl && !isVpaasMeeting(getState())) {
  306. let email;
  307. if (getState()['features/base/jwt'].jwt) {
  308. email = getLocalParticipant(getState())?.email;
  309. }
  310. dispatch(authStatusChanged(true, email || ''));
  311. }
  312. // FIXME: Workaround for the web version. Currently, the creation of the
  313. // conference is handled by /conference.js.
  314. if (typeof APP === 'undefined') {
  315. dispatch(createConference());
  316. return result;
  317. }
  318. return result;
  319. }
  320. /**
  321. * Logs jwt validation errors from xmpp and from the client-side validator.
  322. *
  323. * @param {string} message - The error message from xmpp.
  324. * @param {string} errors - The detailed errors.
  325. * @returns {void}
  326. */
  327. function _logJwtErrors(message: string, errors: string) {
  328. message && logger.error(`JWT error: ${message}`);
  329. errors && logger.error('JWT parsing errors:', errors);
  330. }
  331. /**
  332. * Notifies the feature base/conference that the action
  333. * {@code CONNECTION_FAILED} is being dispatched within a specific redux
  334. * store.
  335. *
  336. * @param {Store} store - The redux store in which the specified {@code action}
  337. * is being dispatched.
  338. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  339. * specified {@code action} to the specified {@code store}.
  340. * @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
  341. * being dispatched in the specified {@code store}.
  342. * @private
  343. * @returns {Object} The value returned by {@code next(action)}.
  344. */
  345. function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  346. const { connection, error } = action;
  347. const { jwt } = getState()['features/base/jwt'];
  348. if (jwt) {
  349. const errors: string = validateJwt(jwt).map((err: any) =>
  350. i18n.t(`dialog.tokenAuthFailedReason.${err.key}`, err.args))
  351. .join(' ');
  352. _logJwtErrors(error.message, errors);
  353. // do not show the notification when we will prompt the user
  354. // for username and password
  355. if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
  356. dispatch(showErrorNotification({
  357. descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
  358. descriptionArguments: { reason: errors },
  359. titleKey: 'dialog.tokenAuthFailedTitle'
  360. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  361. }
  362. }
  363. if (error.name === JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED) {
  364. let notificationAction: Function = showNotification;
  365. const notificationProps = {
  366. customActionNameKey: [ 'dialog.rejoinNow' ],
  367. customActionHandler: [ () => dispatch(reloadNow()) ],
  368. descriptionKey: 'notify.connectionFailed'
  369. } as INotificationProps;
  370. const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
  371. const { tenant } = parseURIString(locationURL.href) || {};
  372. if (tenant.startsWith('-') || tenant.endsWith('-')) {
  373. notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';
  374. notificationProps.titleKey = 'notify.invalidTenant';
  375. notificationAction = showErrorNotification;
  376. } else if (tenant.length > 63) {
  377. notificationProps.descriptionKey = 'notify.invalidTenantLengthDescription';
  378. notificationProps.titleKey = 'notify.invalidTenant';
  379. notificationAction = showErrorNotification;
  380. }
  381. dispatch(notificationAction(notificationProps, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  382. }
  383. const result = next(action);
  384. _removeUnloadHandler(getState);
  385. forEachConference(getState, conference => {
  386. // TODO: revisit this
  387. // It feels that it would make things easier if JitsiConference
  388. // in lib-jitsi-meet would monitor it's connection and emit
  389. // CONFERENCE_FAILED when it's dropped. It has more knowledge on
  390. // whether it can recover or not. But because the reload screen
  391. // and the retry logic is implemented in the app maybe it can be
  392. // left this way for now.
  393. if (conference.getConnection() === connection) {
  394. // XXX Note that on mobile the error type passed to
  395. // connectionFailed is always an object with .name property.
  396. // This fact needs to be checked prior to enabling this logic on
  397. // web.
  398. const conferenceAction = conferenceFailed(conference, error.name);
  399. // Copy the recoverable flag if set on the CONNECTION_FAILED
  400. // action to not emit recoverable action caused by
  401. // a non-recoverable one.
  402. if (typeof error.recoverable !== 'undefined') {
  403. conferenceAction.error.recoverable = error.recoverable;
  404. }
  405. dispatch(conferenceAction);
  406. }
  407. return true;
  408. });
  409. return result;
  410. }
  411. /**
  412. * Notifies the feature base/conference that the action
  413. * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
  414. * redux store.
  415. *
  416. * @param {Store} store - The redux store in which the specified {@code action}
  417. * is being dispatched.
  418. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  419. * specified {@code action} to the specified {@code store}.
  420. * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
  421. * which is being dispatched in the specified {@code store}.
  422. * @private
  423. * @returns {Object} The value returned by {@code next(action)}.
  424. */
  425. function _conferenceSubjectChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  426. const result = next(action);
  427. const { subject } = getState()['features/base/conference'];
  428. if (subject) {
  429. dispatch({
  430. type: SET_PENDING_SUBJECT_CHANGE,
  431. subject: undefined
  432. });
  433. }
  434. typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
  435. return result;
  436. }
  437. /**
  438. * Notifies the feature base/conference that the action
  439. * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
  440. * store.
  441. *
  442. * @private
  443. * @param {Object} store - The redux store.
  444. * @returns {void}
  445. */
  446. function _conferenceWillLeave({ getState }: IStore) {
  447. _removeUnloadHandler(getState);
  448. }
  449. /**
  450. * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
  451. * is being dispatched within a specific redux store. Pins the specified remote
  452. * participant in the associated conference, ignores the local participant.
  453. *
  454. * @param {Store} store - The redux store in which the specified {@code action}
  455. * is being dispatched.
  456. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  457. * specified {@code action} to the specified {@code store}.
  458. * @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
  459. * being dispatched in the specified {@code store}.
  460. * @private
  461. * @returns {Object} The value returned by {@code next(action)}.
  462. */
  463. function _pinParticipant({ getState }: IStore, next: Function, action: AnyAction) {
  464. const state = getState();
  465. const { conference } = state['features/base/conference'];
  466. if (!conference) {
  467. return next(action);
  468. }
  469. const id = action.participant.id;
  470. const participantById = getParticipantById(state, id);
  471. const pinnedParticipant = getPinnedParticipant(state);
  472. const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
  473. const local
  474. = participantById?.local
  475. || (!id && pinnedParticipant && pinnedParticipant.local);
  476. let participantIdForEvent;
  477. if (local) {
  478. participantIdForEvent = local;
  479. } else {
  480. participantIdForEvent
  481. = actionName === ACTION_PINNED ? id : pinnedParticipant?.id;
  482. }
  483. sendAnalytics(createPinnedEvent(
  484. actionName,
  485. participantIdForEvent,
  486. {
  487. local,
  488. 'participant_count': conference.getParticipantCount()
  489. }));
  490. return next(action);
  491. }
  492. /**
  493. * Removes the unload handler.
  494. *
  495. * @param {Function} getState - The redux getState function.
  496. * @returns {void}
  497. */
  498. function _removeUnloadHandler(getState: IStore['getState']) {
  499. if (typeof beforeUnloadHandler !== 'undefined') {
  500. const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
  501. window.removeEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  502. beforeUnloadHandler = undefined;
  503. }
  504. }
  505. /**
  506. * Requests the specified tones to be played.
  507. *
  508. * @param {Store} store - The redux store in which the specified {@code action}
  509. * is being dispatched.
  510. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  511. * specified {@code action} to the specified {@code store}.
  512. * @param {Action} action - The redux action {@code SEND_TONES} which is
  513. * being dispatched in the specified {@code store}.
  514. * @private
  515. * @returns {Object} The value returned by {@code next(action)}.
  516. */
  517. function _sendTones({ getState }: IStore, next: Function, action: AnyAction) {
  518. const state = getState();
  519. const { conference } = state['features/base/conference'];
  520. if (conference) {
  521. const { duration, tones, pause } = action;
  522. conference.sendTones(tones, duration, pause);
  523. }
  524. return next(action);
  525. }
  526. /**
  527. * Notifies the feature base/conference that the action
  528. * {@code SET_ROOM} is being dispatched within a specific
  529. * redux store.
  530. *
  531. * @param {Store} store - The redux store in which the specified {@code action}
  532. * is being dispatched.
  533. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  534. * specified {@code action} to the specified {@code store}.
  535. * @param {Action} action - The redux action {@code SET_ROOM}
  536. * which is being dispatched in the specified {@code store}.
  537. * @private
  538. * @returns {Object} The value returned by {@code next(action)}.
  539. */
  540. function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  541. const state = getState();
  542. const { localSubject, subject } = state['features/base/config'];
  543. const { room } = action;
  544. if (room) {
  545. // Set the stored subject.
  546. localSubject && dispatch(setLocalSubject(localSubject));
  547. subject && dispatch(setSubject(subject));
  548. }
  549. return next(action);
  550. }
  551. /**
  552. * Notifies the feature base/conference that the action {@code TRACK_ADDED}
  553. * or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
  554. *
  555. * @param {Store} store - The redux store in which the specified {@code action}
  556. * is being dispatched.
  557. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  558. * specified {@code action} to the specified {@code store}.
  559. * @param {Action} action - The redux action {@code TRACK_ADDED} or
  560. * {@code TRACK_REMOVED} which is being dispatched in the specified
  561. * {@code store}.
  562. * @private
  563. * @returns {Object} The value returned by {@code next(action)}.
  564. */
  565. function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
  566. const track = action.track;
  567. // TODO All track swapping should happen here instead of conference.js.
  568. if (track?.local) {
  569. const { getState } = store;
  570. const state = getState();
  571. const conference = getCurrentConference(state);
  572. let promise;
  573. if (conference) {
  574. const jitsiTrack = action.track.jitsiTrack;
  575. if (action.type === TRACK_ADDED) {
  576. // If gUM is slow and tracks are created after the user has already joined the conference, avoid
  577. // adding the tracks to the conference if the user is a visitor.
  578. if (!iAmVisitor(state)) {
  579. promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
  580. }
  581. } else {
  582. promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
  583. }
  584. if (promise) {
  585. return promise.then(() => next(action));
  586. }
  587. }
  588. }
  589. return next(action);
  590. }
  591. /**
  592. * Updates the conference object when the local participant is updated.
  593. *
  594. * @param {Store} store - The redux store in which the specified {@code action}
  595. * is being dispatched.
  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 which is being dispatched in the
  599. * specified {@code store}.
  600. * @private
  601. * @returns {Object} The value returned by {@code next(action)}.
  602. */
  603. function _updateLocalParticipantInConference({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  604. const { conference } = getState()['features/base/conference'];
  605. const { participant } = action;
  606. const result = next(action);
  607. const localParticipant = getLocalParticipant(getState);
  608. if (conference && participant.id === localParticipant?.id) {
  609. if ('name' in participant) {
  610. conference.setDisplayName(participant.name);
  611. }
  612. if ('isSilent' in participant) {
  613. conference.setIsSilent(participant.isSilent);
  614. }
  615. if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
  616. const { pendingSubjectChange, subject } = getState()['features/base/conference'];
  617. // When the local user role is updated to moderator and we have a pending subject change
  618. // which was not reflected we need to set it (the first time we tried was before becoming moderator).
  619. if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
  620. dispatch(setSubject(pendingSubjectChange));
  621. }
  622. }
  623. }
  624. return result;
  625. }
  626. /**
  627. * Notifies the external API that the action {@code P2P_STATUS_CHANGED}
  628. * is being dispatched within a specific redux store.
  629. *
  630. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  631. * specified {@code action} to the specified {@code store}.
  632. * @param {Action} action - The redux action {@code P2P_STATUS_CHANGED}
  633. * which is being dispatched in the specified {@code store}.
  634. * @private
  635. * @returns {Object} The value returned by {@code next(action)}.
  636. */
  637. function _p2pStatusChanged(next: Function, action: AnyAction) {
  638. const result = next(action);
  639. if (typeof APP !== 'undefined') {
  640. APP.API.notifyP2pStatusChanged(action.p2p);
  641. }
  642. return result;
  643. }
  644. /**
  645. * Notifies the feature base/conference that the action
  646. * {@code SET_ASSUMED_BANDWIDTH_BPS} is being dispatched within a specific
  647. * redux store.
  648. *
  649. * @param {Store} store - The redux store in which the specified {@code action}
  650. * is being dispatched.
  651. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  652. * specified {@code action} to the specified {@code store}.
  653. * @param {Action} action - The redux action {@code SET_ASSUMED_BANDWIDTH_BPS}
  654. * which is being dispatched in the specified {@code store}.
  655. * @private
  656. * @returns {Object} The value returned by {@code next(action)}.
  657. */
  658. function _setAssumedBandwidthBps({ getState }: IStore, next: Function, action: AnyAction) {
  659. const state = getState();
  660. const conference = getCurrentConference(state);
  661. const payload = Number(action.assumedBandwidthBps);
  662. const assumedBandwidthBps = isNaN(payload) || payload < MIN_ASSUMED_BANDWIDTH_BPS
  663. ? MIN_ASSUMED_BANDWIDTH_BPS
  664. : payload;
  665. if (conference) {
  666. conference.setAssumedBandwidthBps(assumedBandwidthBps);
  667. }
  668. return next(action);
  669. }