Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

reducer.ts 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import _ from 'lodash';
  2. import { CONFERENCE_INFO } from '../../conference/components/constants';
  3. import ReducerRegistry from '../redux/ReducerRegistry';
  4. import { equals } from '../redux/functions';
  5. import {
  6. CONFIG_WILL_LOAD,
  7. LOAD_CONFIG_ERROR,
  8. OVERWRITE_CONFIG,
  9. SET_CONFIG,
  10. UPDATE_CONFIG
  11. } from './actionTypes';
  12. import {
  13. IConfig,
  14. IDeeplinkingConfig,
  15. IDeeplinkingMobileConfig,
  16. IDeeplinkingPlatformConfig,
  17. IMobileDynamicLink
  18. } from './configType';
  19. import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
  20. /**
  21. * The initial state of the feature base/config when executing in a
  22. * non-React Native environment. The mandatory configuration to be passed to
  23. * JitsiMeetJS#init(). The app will download config.js from the Jitsi Meet
  24. * deployment and take its values into account but the values below will be
  25. * enforced (because they are essential to the correct execution of the
  26. * application).
  27. *
  28. * @type {Object}
  29. */
  30. const INITIAL_NON_RN_STATE: IConfig = {
  31. };
  32. /**
  33. * The initial state of the feature base/config when executing in a React Native
  34. * environment. The mandatory configuration to be passed to JitsiMeetJS#init().
  35. * The app will download config.js from the Jitsi Meet deployment and take its
  36. * values into account but the values below will be enforced (because they are
  37. * essential to the correct execution of the application).
  38. *
  39. * @type {Object}
  40. */
  41. const INITIAL_RN_STATE: IConfig = {
  42. };
  43. /**
  44. * Mapping between old configs controlling the conference info headers visibility and the
  45. * new configs. Needed in order to keep backwards compatibility.
  46. */
  47. const CONFERENCE_HEADER_MAPPING = {
  48. hideConferenceTimer: [ 'conference-timer' ],
  49. hideConferenceSubject: [ 'subject' ],
  50. hideParticipantsStats: [ 'participants-count' ],
  51. hideRecordingLabel: [ 'recording' ]
  52. };
  53. export interface IConfigState extends IConfig {
  54. analysis?: {
  55. obfuscateRoomName?: boolean;
  56. };
  57. disableRemoteControl?: boolean;
  58. error?: Error;
  59. oldConfig?: {
  60. bosh?: string;
  61. focusUserJid?: string;
  62. hosts: {
  63. domain: string;
  64. muc: string;
  65. };
  66. p2p?: object;
  67. websocket?: string;
  68. };
  69. }
  70. ReducerRegistry.register<IConfigState>('features/base/config', (state = _getInitialState(), action): IConfigState => {
  71. switch (action.type) {
  72. case UPDATE_CONFIG:
  73. return _updateConfig(state, action);
  74. case CONFIG_WILL_LOAD:
  75. return {
  76. error: undefined,
  77. /**
  78. * The URL of the location associated with/configured by this
  79. * configuration.
  80. *
  81. * @type URL
  82. */
  83. locationURL: action.locationURL
  84. };
  85. case LOAD_CONFIG_ERROR:
  86. // XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
  87. // the asynchronous "loadConfig procedure/process" started with
  88. // CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
  89. // is settling the process needs to provide proof that they have
  90. // started it and that the iteration of the process being completed
  91. // now is still of interest to the app.
  92. if (state.locationURL === action.locationURL) {
  93. return {
  94. /**
  95. * The {@link Error} which prevented the loading of the
  96. * configuration of the associated {@code locationURL}.
  97. *
  98. * @type Error
  99. */
  100. error: action.error
  101. };
  102. }
  103. break;
  104. case SET_CONFIG:
  105. return _setConfig(state, action);
  106. case OVERWRITE_CONFIG:
  107. return {
  108. ...state,
  109. ...action.config
  110. };
  111. }
  112. return state;
  113. });
  114. /**
  115. * Gets the initial state of the feature base/config. The mandatory
  116. * configuration to be passed to JitsiMeetJS#init(). The app will download
  117. * config.js from the Jitsi Meet deployment and take its values into account but
  118. * the values below will be enforced (because they are essential to the correct
  119. * execution of the application).
  120. *
  121. * @returns {Object}
  122. */
  123. function _getInitialState() {
  124. return (
  125. navigator.product === 'ReactNative'
  126. ? INITIAL_RN_STATE
  127. : INITIAL_NON_RN_STATE);
  128. }
  129. /**
  130. * Reduces a specific Redux action SET_CONFIG of the feature
  131. * base/lib-jitsi-meet.
  132. *
  133. * @param {IConfig} state - The Redux state of the feature base/config.
  134. * @param {Action} action - The Redux action SET_CONFIG to reduce.
  135. * @private
  136. * @returns {Object} The new state after the reduction of the specified action.
  137. */
  138. function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
  139. // eslint-disable-next-line no-param-reassign
  140. config = _translateLegacyConfig(config);
  141. const { audioQuality } = config;
  142. const hdAudioOptions = {};
  143. if (audioQuality?.stereo) {
  144. Object.assign(hdAudioOptions, {
  145. disableAP: true,
  146. enableNoAudioDetection: false,
  147. enableNoisyMicDetection: false,
  148. enableTalkWhileMuted: false
  149. });
  150. }
  151. const newState = _.merge(
  152. {},
  153. config,
  154. hdAudioOptions,
  155. { error: undefined },
  156. // The config of _getInitialState() is meant to override the config
  157. // downloaded from the Jitsi Meet deployment because the former contains
  158. // values that are mandatory.
  159. _getInitialState()
  160. );
  161. _cleanupConfig(newState);
  162. return equals(state, newState) ? state : newState;
  163. }
  164. /**
  165. * Processes the conferenceInfo object against the defaults.
  166. *
  167. * @param {IConfig} config - The old config.
  168. * @returns {Object} The processed conferenceInfo object.
  169. */
  170. function _getConferenceInfo(config: IConfig) {
  171. const { conferenceInfo } = config;
  172. if (conferenceInfo) {
  173. return {
  174. alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
  175. autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
  176. };
  177. }
  178. return {
  179. ...CONFERENCE_INFO
  180. };
  181. }
  182. /**
  183. * Constructs a new config {@code Object}, if necessary, out of a specific
  184. * interface_config {@code Object} which is in the latest format supported by jitsi-meet.
  185. *
  186. * @param {Object} oldValue - The config {@code Object} which may or may not be
  187. * in the latest form supported by jitsi-meet and from which a new config
  188. * {@code Object} is to be constructed if necessary.
  189. * @returns {Object} A config {@code Object} which is in the latest format
  190. * supported by jitsi-meet.
  191. */
  192. function _translateInterfaceConfig(oldValue: IConfig) {
  193. const newValue = oldValue;
  194. if (!Array.isArray(oldValue.toolbarButtons)
  195. && typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
  196. newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
  197. }
  198. if (!oldValue.toolbarConfig) {
  199. oldValue.toolbarConfig = {};
  200. }
  201. newValue.toolbarConfig = oldValue.toolbarConfig || {};
  202. if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean'
  203. && typeof interfaceConfig === 'object'
  204. && typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') {
  205. newValue.toolbarConfig.alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE;
  206. }
  207. if (typeof oldValue.toolbarConfig.initialTimeout !== 'number'
  208. && typeof interfaceConfig === 'object'
  209. && typeof interfaceConfig.INITIAL_TOOLBAR_TIMEOUT === 'number') {
  210. newValue.toolbarConfig.initialTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
  211. }
  212. if (typeof oldValue.toolbarConfig.timeout !== 'number'
  213. && typeof interfaceConfig === 'object'
  214. && typeof interfaceConfig.TOOLBAR_TIMEOUT === 'number') {
  215. newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT;
  216. }
  217. if (!oldValue.connectionIndicators
  218. && typeof interfaceConfig === 'object'
  219. && (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
  220. || interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
  221. || interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
  222. newValue.connectionIndicators = {
  223. disabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  224. autoHide: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  225. autoHideTimeout: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT
  226. };
  227. }
  228. if (oldValue.disableModeratorIndicator === undefined
  229. && typeof interfaceConfig === 'object'
  230. && interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
  231. newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
  232. }
  233. if (oldValue.defaultLocalDisplayName === undefined
  234. && typeof interfaceConfig === 'object'
  235. && interfaceConfig.hasOwnProperty('DEFAULT_LOCAL_DISPLAY_NAME')) {
  236. newValue.defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
  237. }
  238. if (oldValue.defaultRemoteDisplayName === undefined
  239. && typeof interfaceConfig === 'object'
  240. && interfaceConfig.hasOwnProperty('DEFAULT_REMOTE_DISPLAY_NAME')) {
  241. newValue.defaultRemoteDisplayName = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
  242. }
  243. if (oldValue.defaultLogoUrl === undefined) {
  244. if (typeof interfaceConfig === 'object'
  245. && interfaceConfig.hasOwnProperty('DEFAULT_LOGO_URL')) {
  246. newValue.defaultLogoUrl = interfaceConfig.DEFAULT_LOGO_URL;
  247. } else {
  248. newValue.defaultLogoUrl = 'images/watermark.svg';
  249. }
  250. }
  251. // if we have `deeplinking` defined, ignore deprecated values, except `disableDeepLinking`.
  252. // Otherwise, compose the config.
  253. if (oldValue.deeplinking && newValue.deeplinking) { // make TS happy
  254. newValue.deeplinking.disabled = oldValue.deeplinking.hasOwnProperty('disabled')
  255. ? oldValue.deeplinking.disabled
  256. : Boolean(oldValue.disableDeepLinking);
  257. } else {
  258. const disabled = Boolean(oldValue.disableDeepLinking);
  259. const deeplinking: IDeeplinkingConfig = {
  260. desktop: {} as IDeeplinkingPlatformConfig,
  261. hideLogo: false,
  262. disabled,
  263. android: {} as IDeeplinkingMobileConfig,
  264. ios: {} as IDeeplinkingMobileConfig
  265. };
  266. if (typeof interfaceConfig === 'object') {
  267. const mobileDynamicLink = interfaceConfig.MOBILE_DYNAMIC_LINK;
  268. const dynamicLink: IMobileDynamicLink | undefined = mobileDynamicLink ? {
  269. apn: mobileDynamicLink.APN,
  270. appCode: mobileDynamicLink.APP_CODE,
  271. ibi: mobileDynamicLink.IBI,
  272. isi: mobileDynamicLink.ISI,
  273. customDomain: mobileDynamicLink.CUSTOM_DOMAIN
  274. } : undefined;
  275. if (deeplinking.desktop) {
  276. deeplinking.desktop.appName = interfaceConfig.NATIVE_APP_NAME;
  277. }
  278. deeplinking.hideLogo = Boolean(interfaceConfig.HIDE_DEEP_LINKING_LOGO);
  279. deeplinking.android = {
  280. appName: interfaceConfig.NATIVE_APP_NAME,
  281. appScheme: interfaceConfig.APP_SCHEME,
  282. downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_ANDROID,
  283. appPackage: interfaceConfig.ANDROID_APP_PACKAGE,
  284. fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID,
  285. dynamicLink
  286. };
  287. deeplinking.ios = {
  288. appName: interfaceConfig.NATIVE_APP_NAME,
  289. appScheme: interfaceConfig.APP_SCHEME,
  290. downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS,
  291. dynamicLink
  292. };
  293. }
  294. newValue.deeplinking = deeplinking;
  295. }
  296. return newValue;
  297. }
  298. /**
  299. * Constructs a new config {@code Object}, if necessary, out of a specific
  300. * config {@code Object} which is in the latest format supported by jitsi-meet.
  301. * Such a translation from an old config format to a new/the latest config
  302. * format is necessary because the mobile app bundles jitsi-meet and
  303. * lib-jitsi-meet at build time and does not download them at runtime from the
  304. * deployment on which it will join a conference.
  305. *
  306. * @param {Object} oldValue - The config {@code Object} which may or may not be
  307. * in the latest form supported by jitsi-meet and from which a new config
  308. * {@code Object} is to be constructed if necessary.
  309. * @returns {Object} A config {@code Object} which is in the latest format
  310. * supported by jitsi-meet.
  311. */
  312. function _translateLegacyConfig(oldValue: IConfig) {
  313. const newValue = _translateInterfaceConfig(oldValue);
  314. // Translate deprecated config values to new config values.
  315. const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key as keyof IConfig]);
  316. if (filteredConferenceInfo.length) {
  317. newValue.conferenceInfo = _getConferenceInfo(oldValue);
  318. filteredConferenceInfo.forEach(key => {
  319. newValue.conferenceInfo = oldValue.conferenceInfo ?? {};
  320. // hideRecordingLabel does not mean not render it at all, but autoHide it
  321. if (key === 'hideRecordingLabel') {
  322. newValue.conferenceInfo.alwaysVisible
  323. = (newValue.conferenceInfo?.alwaysVisible ?? [])
  324. .filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
  325. newValue.conferenceInfo.autoHide
  326. = _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
  327. } else {
  328. newValue.conferenceInfo.alwaysVisible
  329. = (newValue.conferenceInfo.alwaysVisible ?? [])
  330. .filter(c => !CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
  331. newValue.conferenceInfo.autoHide
  332. = (newValue.conferenceInfo.autoHide ?? []).filter(c =>
  333. !CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
  334. }
  335. });
  336. }
  337. newValue.welcomePage = oldValue.welcomePage || {};
  338. if (oldValue.hasOwnProperty('enableWelcomePage')
  339. && !newValue.welcomePage.hasOwnProperty('disabled')
  340. ) {
  341. newValue.welcomePage.disabled = !oldValue.enableWelcomePage;
  342. }
  343. newValue.prejoinConfig = oldValue.prejoinConfig || {};
  344. if (oldValue.hasOwnProperty('prejoinPageEnabled')
  345. && !newValue.prejoinConfig.hasOwnProperty('enabled')
  346. ) {
  347. newValue.prejoinConfig.enabled = oldValue.prejoinPageEnabled;
  348. }
  349. newValue.disabledSounds = newValue.disabledSounds || [];
  350. if (oldValue.disableJoinLeaveSounds) {
  351. newValue.disabledSounds.unshift('PARTICIPANT_LEFT_SOUND', 'PARTICIPANT_JOINED_SOUND');
  352. }
  353. if (oldValue.disableRecordAudioNotification) {
  354. newValue.disabledSounds.unshift(
  355. 'RECORDING_ON_SOUND',
  356. 'RECORDING_OFF_SOUND',
  357. 'LIVE_STREAMING_ON_SOUND',
  358. 'LIVE_STREAMING_OFF_SOUND'
  359. );
  360. }
  361. if (oldValue.disableIncomingMessageSound) {
  362. newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
  363. }
  364. if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
  365. newValue.audioQuality = {
  366. opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,
  367. stereo: oldValue.audioQuality?.stereo ?? oldValue.stereo
  368. };
  369. }
  370. newValue.e2ee = newValue.e2ee || {};
  371. if (oldValue.e2eeLabels) {
  372. newValue.e2ee.labels = oldValue.e2eeLabels;
  373. }
  374. newValue.defaultLocalDisplayName
  375. = newValue.defaultLocalDisplayName || 'me';
  376. if (oldValue.hideAddRoomButton) {
  377. newValue.breakoutRooms = {
  378. /* eslint-disable-next-line no-extra-parens */
  379. ...(newValue.breakoutRooms || {}),
  380. hideAddRoomButton: oldValue.hideAddRoomButton
  381. };
  382. }
  383. newValue.defaultRemoteDisplayName
  384. = newValue.defaultRemoteDisplayName || 'Fellow Jitster';
  385. newValue.transcription = newValue.transcription || {};
  386. if (oldValue.transcribingEnabled !== undefined) {
  387. newValue.transcription = {
  388. ...newValue.transcription,
  389. enabled: oldValue.transcribingEnabled
  390. };
  391. }
  392. if (oldValue.transcribeWithAppLanguage !== undefined) {
  393. newValue.transcription = {
  394. ...newValue.transcription,
  395. useAppLanguage: oldValue.transcribeWithAppLanguage
  396. };
  397. }
  398. if (oldValue.preferredTranscribeLanguage !== undefined) {
  399. newValue.transcription = {
  400. ...newValue.transcription,
  401. preferredLanguage: oldValue.preferredTranscribeLanguage
  402. };
  403. }
  404. if (oldValue.autoCaptionOnRecord !== undefined) {
  405. newValue.transcription = {
  406. ...newValue.transcription,
  407. autoCaptionOnRecord: oldValue.autoCaptionOnRecord
  408. };
  409. }
  410. newValue.recordingService = newValue.recordingService || {};
  411. if (oldValue.fileRecordingsServiceEnabled !== undefined
  412. && newValue.recordingService.enabled === undefined) {
  413. newValue.recordingService = {
  414. ...newValue.recordingService,
  415. enabled: oldValue.fileRecordingsServiceEnabled
  416. };
  417. }
  418. if (oldValue.fileRecordingsServiceSharingEnabled !== undefined
  419. && newValue.recordingService.sharingEnabled === undefined) {
  420. newValue.recordingService = {
  421. ...newValue.recordingService,
  422. sharingEnabled: oldValue.fileRecordingsServiceSharingEnabled
  423. };
  424. }
  425. newValue.liveStreaming = newValue.liveStreaming || {};
  426. // Migrate config.liveStreamingEnabled
  427. if (oldValue.liveStreamingEnabled !== undefined) {
  428. newValue.liveStreaming = {
  429. ...newValue.liveStreaming,
  430. enabled: oldValue.liveStreamingEnabled
  431. };
  432. }
  433. // Migrate interfaceConfig.LIVE_STREAMING_HELP_LINK
  434. if (oldValue.liveStreaming === undefined
  435. && typeof interfaceConfig === 'object'
  436. && interfaceConfig.hasOwnProperty('LIVE_STREAMING_HELP_LINK')) {
  437. newValue.liveStreaming = {
  438. ...newValue.liveStreaming,
  439. helpLink: interfaceConfig.LIVE_STREAMING_HELP_LINK
  440. };
  441. }
  442. newValue.speakerStats = newValue.speakerStats || {};
  443. if (oldValue.disableSpeakerStatsSearch !== undefined
  444. && newValue.speakerStats.disableSearch === undefined
  445. ) {
  446. newValue.speakerStats = {
  447. ...newValue.speakerStats,
  448. disableSearch: oldValue.disableSpeakerStatsSearch
  449. };
  450. }
  451. if (oldValue.speakerStatsOrder !== undefined
  452. && newValue.speakerStats.order === undefined) {
  453. newValue.speakerStats = {
  454. ...newValue.speakerStats,
  455. order: oldValue.speakerStatsOrder
  456. };
  457. }
  458. if (oldValue.autoKnockLobby !== undefined
  459. && newValue.lobby?.autoKnock === undefined) {
  460. newValue.lobby = {
  461. ...newValue.lobby || {},
  462. autoKnock: oldValue.autoKnockLobby
  463. };
  464. }
  465. if (oldValue.enableLobbyChat !== undefined
  466. && newValue.lobby?.enableChat === undefined) {
  467. newValue.lobby = {
  468. ...newValue.lobby || {},
  469. enableChat: oldValue.enableLobbyChat
  470. };
  471. }
  472. if (oldValue.hideLobbyButton !== undefined
  473. && newValue.securityUi?.hideLobbyButton === undefined) {
  474. newValue.securityUi = {
  475. ...newValue.securityUi || {},
  476. hideLobbyButton: oldValue.hideLobbyButton
  477. };
  478. }
  479. _setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
  480. return newValue;
  481. }
  482. /**
  483. * Updates the stored configuration with the given extra options.
  484. *
  485. * @param {Object} state - The Redux state of the feature base/config.
  486. * @param {Action} action - The Redux action to reduce.
  487. * @private
  488. * @returns {Object} The new state after the reduction of the specified action.
  489. */
  490. function _updateConfig(state: IConfig, { config }: { config: IConfig; }) {
  491. const newState = _.merge({}, state, config);
  492. _cleanupConfig(newState);
  493. return equals(state, newState) ? state : newState;
  494. }