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.

reducer.ts 21KB

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