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 20KB

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