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.

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