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

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