Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

middleware.any.ts 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  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 {
  41. CONFERENCE_FAILED,
  42. CONFERENCE_JOINED,
  43. CONFERENCE_SUBJECT_CHANGED,
  44. CONFERENCE_WILL_LEAVE,
  45. P2P_STATUS_CHANGED,
  46. SEND_TONES,
  47. SET_ASSUMED_BANDWIDTH_BPS,
  48. SET_PENDING_SUBJECT_CHANGE,
  49. SET_ROOM
  50. } from './actionTypes';
  51. import {
  52. authStatusChanged,
  53. conferenceFailed,
  54. conferenceWillLeave,
  55. createConference,
  56. setLocalSubject,
  57. setSubject,
  58. updateConferenceMetadata
  59. } from './actions';
  60. import { CONFERENCE_LEAVE_REASONS } from './constants';
  61. import {
  62. _addLocalTracksToConference,
  63. _removeLocalTracksFromConference,
  64. forEachConference,
  65. getCurrentConference,
  66. restoreConferenceOptions
  67. } from './functions';
  68. import logger from './logger';
  69. import { IConferenceMetadata } from './reducer';
  70. /**
  71. * Handler for before unload event.
  72. */
  73. let beforeUnloadHandler: ((e?: any) => void) | undefined;
  74. /**
  75. * Implements the middleware of the feature base/conference.
  76. *
  77. * @param {Store} store - The redux store.
  78. * @returns {Function}
  79. */
  80. MiddlewareRegistry.register(store => next => action => {
  81. switch (action.type) {
  82. case CONFERENCE_FAILED:
  83. return _conferenceFailed(store, next, action);
  84. case CONFERENCE_JOINED:
  85. return _conferenceJoined(store, next, action);
  86. case CONNECTION_ESTABLISHED:
  87. return _connectionEstablished(store, next, action);
  88. case CONNECTION_FAILED:
  89. return _connectionFailed(store, next, action);
  90. case CONFERENCE_SUBJECT_CHANGED:
  91. return _conferenceSubjectChanged(store, next, action);
  92. case CONFERENCE_WILL_LEAVE:
  93. _conferenceWillLeave(store);
  94. break;
  95. case P2P_STATUS_CHANGED:
  96. return _p2pStatusChanged(next, action);
  97. case PARTICIPANT_UPDATED:
  98. return _updateLocalParticipantInConference(store, next, action);
  99. case PIN_PARTICIPANT:
  100. return _pinParticipant(store, next, action);
  101. case SEND_TONES:
  102. return _sendTones(store, next, action);
  103. case SET_ROOM:
  104. return _setRoom(store, next, action);
  105. case TRACK_ADDED:
  106. case TRACK_REMOVED:
  107. return _trackAddedOrRemoved(store, next, action);
  108. case SET_ASSUMED_BANDWIDTH_BPS:
  109. return _setAssumedBandwidthBps(store, next, action);
  110. }
  111. return next(action);
  112. });
  113. /**
  114. * Set up state change listener to perform maintenance tasks when the conference
  115. * is left or failed.
  116. */
  117. StateListenerRegistry.register(
  118. state => getCurrentConference(state),
  119. (conference, { dispatch }, previousConference): void => {
  120. if (conference && !previousConference) {
  121. conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: IConferenceMetadata) => {
  122. dispatch(updateConferenceMetadata(metadata));
  123. });
  124. }
  125. if (conference !== previousConference) {
  126. dispatch(updateConferenceMetadata(null));
  127. }
  128. });
  129. /**
  130. * Makes sure to leave a failed conference in order to release any allocated
  131. * resources like peer connections, emit participant left events, etc.
  132. *
  133. * @param {Store} store - The redux store in which the specified {@code action}
  134. * is being dispatched.
  135. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  136. * specified {@code action} to the specified {@code store}.
  137. * @param {Action} action - The redux action {@code CONFERENCE_FAILED} which is
  138. * being dispatched in the specified {@code store}.
  139. * @private
  140. * @returns {Object} The value returned by {@code next(action)}.
  141. */
  142. function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  143. const { conference, error } = action;
  144. const result = next(action);
  145. const { enableForcedReload } = getState()['features/base/config'];
  146. if (LocalRecordingManager.isRecordingLocally()) {
  147. dispatch(stopLocalVideoRecording());
  148. }
  149. // Handle specific failure reasons.
  150. switch (error.name) {
  151. case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
  152. if (enableForcedReload) {
  153. dispatch(showErrorNotification({
  154. description: 'Restart initiated because of a bridge failure',
  155. titleKey: 'dialog.sessionRestarted'
  156. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  157. }
  158. break;
  159. }
  160. case JitsiConferenceErrors.CONNECTION_ERROR: {
  161. const [ msg ] = error.params;
  162. dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
  163. dispatch(showErrorNotification({
  164. descriptionArguments: { msg },
  165. descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
  166. titleKey: 'connection.CONNFAIL'
  167. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  168. break;
  169. }
  170. case JitsiConferenceErrors.CONFERENCE_MAX_USERS: {
  171. dispatch(showErrorNotification({
  172. hideErrorSupportLink: true,
  173. descriptionKey: 'dialog.maxUsersLimitReached',
  174. titleKey: 'dialog.maxUsersLimitReachedTitle'
  175. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  176. // In case of max users(it can be from a visitor node), let's restore
  177. // oldConfig if any as we will be back to the main prosody.
  178. const newConfig = restoreConferenceOptions(getState);
  179. if (newConfig) {
  180. dispatch(overwriteConfig(newConfig));
  181. dispatch(conferenceWillLeave(conference));
  182. conference.leave()
  183. .then(() => dispatch(disconnect()));
  184. }
  185. break;
  186. }
  187. case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
  188. const [ type, msg ] = error.params;
  189. let descriptionKey;
  190. let titleKey = 'dialog.tokenAuthFailed';
  191. if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_MAIN_PARTICIPANTS) {
  192. descriptionKey = 'visitors.notification.noMainParticipantsDescription';
  193. titleKey = 'visitors.notification.noMainParticipantsTitle';
  194. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.NO_VISITORS_LOBBY) {
  195. descriptionKey = 'visitors.notification.noVisitorLobby';
  196. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.PROMOTION_NOT_ALLOWED) {
  197. descriptionKey = 'visitors.notification.notAllowedPromotion';
  198. } else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_CREATION_RESTRICTION) {
  199. descriptionKey = 'dialog.errorRoomCreationRestriction';
  200. }
  201. dispatch(showErrorNotification({
  202. descriptionKey,
  203. hideErrorSupportLink: true,
  204. titleKey
  205. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  206. sendAnalytics(createNotAllowedErrorEvent(type, msg));
  207. break;
  208. }
  209. case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
  210. sendAnalytics(createOfferAnswerFailedEvent());
  211. break;
  212. }
  213. !error.recoverable
  214. && conference
  215. && conference.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).catch((reason: Error) => {
  216. // Even though we don't care too much about the failure, it may be
  217. // good to know that it happen, so log it (on the info level).
  218. logger.info('JitsiConference.leave() rejected with:', reason);
  219. });
  220. // 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. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  360. }
  361. }
  362. if (error.name === JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED) {
  363. const notificationProps = {
  364. customActionNameKey: [ 'dialog.rejoinNow' ],
  365. customActionHandler: [ () => dispatch(reloadNow()) ],
  366. descriptionKey: 'notify.connectionFailed'
  367. } as INotificationProps;
  368. dispatch(showNotification(notificationProps, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  369. }
  370. const result = next(action);
  371. _removeUnloadHandler(getState);
  372. forEachConference(getState, conference => {
  373. // TODO: revisit this
  374. // It feels that it would make things easier if JitsiConference
  375. // in lib-jitsi-meet would monitor it's connection and emit
  376. // CONFERENCE_FAILED when it's dropped. It has more knowledge on
  377. // whether it can recover or not. But because the reload screen
  378. // and the retry logic is implemented in the app maybe it can be
  379. // left this way for now.
  380. if (conference.getConnection() === connection) {
  381. // XXX Note that on mobile the error type passed to
  382. // connectionFailed is always an object with .name property.
  383. // This fact needs to be checked prior to enabling this logic on
  384. // web.
  385. const conferenceAction = conferenceFailed(conference, error.name);
  386. // Copy the recoverable flag if set on the CONNECTION_FAILED
  387. // action to not emit recoverable action caused by
  388. // a non-recoverable one.
  389. if (typeof error.recoverable !== 'undefined') {
  390. conferenceAction.error.recoverable = error.recoverable;
  391. }
  392. dispatch(conferenceAction);
  393. }
  394. return true;
  395. });
  396. return result;
  397. }
  398. /**
  399. * Notifies the feature base/conference that the action
  400. * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific
  401. * redux store.
  402. *
  403. * @param {Store} store - The redux store in which the specified {@code action}
  404. * is being dispatched.
  405. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  406. * specified {@code action} to the specified {@code store}.
  407. * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED}
  408. * which is being dispatched in the specified {@code store}.
  409. * @private
  410. * @returns {Object} The value returned by {@code next(action)}.
  411. */
  412. function _conferenceSubjectChanged({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  413. const result = next(action);
  414. const { subject } = getState()['features/base/conference'];
  415. if (subject) {
  416. dispatch({
  417. type: SET_PENDING_SUBJECT_CHANGE,
  418. subject: undefined
  419. });
  420. }
  421. typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
  422. return result;
  423. }
  424. /**
  425. * Notifies the feature base/conference that the action
  426. * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
  427. * store.
  428. *
  429. * @private
  430. * @param {Object} store - The redux store.
  431. * @returns {void}
  432. */
  433. function _conferenceWillLeave({ getState }: IStore) {
  434. _removeUnloadHandler(getState);
  435. }
  436. /**
  437. * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
  438. * is being dispatched within a specific redux store. Pins the specified remote
  439. * participant in the associated conference, ignores the local participant.
  440. *
  441. * @param {Store} store - The redux store in which the specified {@code action}
  442. * is being dispatched.
  443. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  444. * specified {@code action} to the specified {@code store}.
  445. * @param {Action} action - The redux action {@code PIN_PARTICIPANT} which is
  446. * being dispatched in the specified {@code store}.
  447. * @private
  448. * @returns {Object} The value returned by {@code next(action)}.
  449. */
  450. function _pinParticipant({ getState }: IStore, next: Function, action: AnyAction) {
  451. const state = getState();
  452. const { conference } = state['features/base/conference'];
  453. if (!conference) {
  454. return next(action);
  455. }
  456. const id = action.participant.id;
  457. const participantById = getParticipantById(state, id);
  458. const pinnedParticipant = getPinnedParticipant(state);
  459. const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
  460. const local
  461. = participantById?.local
  462. || (!id && pinnedParticipant && pinnedParticipant.local);
  463. let participantIdForEvent;
  464. if (local) {
  465. participantIdForEvent = local;
  466. } else {
  467. participantIdForEvent
  468. = actionName === ACTION_PINNED ? id : pinnedParticipant?.id;
  469. }
  470. sendAnalytics(createPinnedEvent(
  471. actionName,
  472. participantIdForEvent,
  473. {
  474. local,
  475. 'participant_count': conference.getParticipantCount()
  476. }));
  477. return next(action);
  478. }
  479. /**
  480. * Removes the unload handler.
  481. *
  482. * @param {Function} getState - The redux getState function.
  483. * @returns {void}
  484. */
  485. function _removeUnloadHandler(getState: IStore['getState']) {
  486. if (typeof beforeUnloadHandler !== 'undefined') {
  487. const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
  488. window.removeEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
  489. beforeUnloadHandler = undefined;
  490. }
  491. }
  492. /**
  493. * Requests the specified tones to be played.
  494. *
  495. * @param {Store} store - The redux store in which the specified {@code action}
  496. * is being dispatched.
  497. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  498. * specified {@code action} to the specified {@code store}.
  499. * @param {Action} action - The redux action {@code SEND_TONES} which is
  500. * being dispatched in the specified {@code store}.
  501. * @private
  502. * @returns {Object} The value returned by {@code next(action)}.
  503. */
  504. function _sendTones({ getState }: IStore, next: Function, action: AnyAction) {
  505. const state = getState();
  506. const { conference } = state['features/base/conference'];
  507. if (conference) {
  508. const { duration, tones, pause } = action;
  509. conference.sendTones(tones, duration, pause);
  510. }
  511. return next(action);
  512. }
  513. /**
  514. * Notifies the feature base/conference that the action
  515. * {@code SET_ROOM} is being dispatched within a specific
  516. * redux store.
  517. *
  518. * @param {Store} store - The redux store in which the specified {@code action}
  519. * is being dispatched.
  520. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  521. * specified {@code action} to the specified {@code store}.
  522. * @param {Action} action - The redux action {@code SET_ROOM}
  523. * which is being dispatched in the specified {@code store}.
  524. * @private
  525. * @returns {Object} The value returned by {@code next(action)}.
  526. */
  527. function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  528. const state = getState();
  529. const { localSubject, subject } = state['features/base/config'];
  530. const { room } = action;
  531. if (room) {
  532. // Set the stored subject.
  533. localSubject && dispatch(setLocalSubject(localSubject));
  534. subject && dispatch(setSubject(subject));
  535. }
  536. return next(action);
  537. }
  538. /**
  539. * Notifies the feature base/conference that the action {@code TRACK_ADDED}
  540. * or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
  541. *
  542. * @param {Store} store - The redux store in which the specified {@code action}
  543. * is being dispatched.
  544. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  545. * specified {@code action} to the specified {@code store}.
  546. * @param {Action} action - The redux action {@code TRACK_ADDED} or
  547. * {@code TRACK_REMOVED} which is being dispatched in the specified
  548. * {@code store}.
  549. * @private
  550. * @returns {Object} The value returned by {@code next(action)}.
  551. */
  552. function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
  553. const track = action.track;
  554. // TODO All track swapping should happen here instead of conference.js.
  555. if (track?.local) {
  556. const { getState } = store;
  557. const state = getState();
  558. const conference = getCurrentConference(state);
  559. let promise;
  560. if (conference) {
  561. const jitsiTrack = action.track.jitsiTrack;
  562. if (action.type === TRACK_ADDED) {
  563. // If gUM is slow and tracks are created after the user has already joined the conference, avoid
  564. // adding the tracks to the conference if the user is a visitor.
  565. if (!iAmVisitor(state)) {
  566. promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
  567. }
  568. } else {
  569. promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
  570. }
  571. if (promise) {
  572. return promise.then(() => next(action));
  573. }
  574. }
  575. }
  576. return next(action);
  577. }
  578. /**
  579. * Updates the conference object when the local participant is updated.
  580. *
  581. * @param {Store} store - The redux store in which the specified {@code action}
  582. * is being dispatched.
  583. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  584. * specified {@code action} to the specified {@code store}.
  585. * @param {Action} action - The redux action which is being dispatched in the
  586. * specified {@code store}.
  587. * @private
  588. * @returns {Object} The value returned by {@code next(action)}.
  589. */
  590. function _updateLocalParticipantInConference({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  591. const { conference } = getState()['features/base/conference'];
  592. const { participant } = action;
  593. const result = next(action);
  594. const localParticipant = getLocalParticipant(getState);
  595. if (conference && participant.id === localParticipant?.id) {
  596. if ('name' in participant) {
  597. conference.setDisplayName(participant.name);
  598. }
  599. if ('isSilent' in participant) {
  600. conference.setIsSilent(participant.isSilent);
  601. }
  602. if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
  603. const { pendingSubjectChange, subject } = getState()['features/base/conference'];
  604. // When the local user role is updated to moderator and we have a pending subject change
  605. // which was not reflected we need to set it (the first time we tried was before becoming moderator).
  606. if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
  607. dispatch(setSubject(pendingSubjectChange));
  608. }
  609. }
  610. }
  611. return result;
  612. }
  613. /**
  614. * Notifies the external API that the action {@code P2P_STATUS_CHANGED}
  615. * is being dispatched within a specific redux store.
  616. *
  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 P2P_STATUS_CHANGED}
  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 _p2pStatusChanged(next: Function, action: AnyAction) {
  625. const result = next(action);
  626. if (typeof APP !== 'undefined') {
  627. APP.API.notifyP2pStatusChanged(action.p2p);
  628. }
  629. return result;
  630. }
  631. /**
  632. * Notifies the feature base/conference that the action
  633. * {@code SET_ASSUMED_BANDWIDTH_BPS} is being dispatched within a specific
  634. * redux store.
  635. *
  636. * @param {Store} store - The redux store in which the specified {@code action}
  637. * is being dispatched.
  638. * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
  639. * specified {@code action} to the specified {@code store}.
  640. * @param {Action} action - The redux action {@code SET_ASSUMED_BANDWIDTH_BPS}
  641. * which is being dispatched in the specified {@code store}.
  642. * @private
  643. * @returns {Object} The value returned by {@code next(action)}.
  644. */
  645. function _setAssumedBandwidthBps({ getState }: IStore, next: Function, action: AnyAction) {
  646. const state = getState();
  647. const conference = getCurrentConference(state);
  648. const payload = Number(action.assumedBandwidthBps);
  649. const assumedBandwidthBps = isNaN(payload) || payload < MIN_ASSUMED_BANDWIDTH_BPS
  650. ? MIN_ASSUMED_BANDWIDTH_BPS
  651. : payload;
  652. if (conference) {
  653. conference.setAssumedBandwidthBps(assumedBandwidthBps);
  654. }
  655. return next(action);
  656. }