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.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // @flow
  2. import _ from 'lodash';
  3. import { CONFERENCE_INFO } from '../../conference/components/constants';
  4. import { equals, ReducerRegistry } from '../redux';
  5. import {
  6. UPDATE_CONFIG,
  7. CONFIG_WILL_LOAD,
  8. LOAD_CONFIG_ERROR,
  9. SET_CONFIG,
  10. OVERWRITE_CONFIG
  11. } from './actionTypes';
  12. import { _cleanupConfig } from './functions';
  13. declare var interfaceConfig: Object;
  14. /**
  15. * The initial state of the feature base/config when executing in a
  16. * non-React Native environment. The mandatory configuration to be passed to
  17. * JitsiMeetJS#init(). The app will download config.js from the Jitsi Meet
  18. * deployment and take its values into account but the values below will be
  19. * enforced (because they are essential to the correct execution of the
  20. * application).
  21. *
  22. * @type {Object}
  23. */
  24. const INITIAL_NON_RN_STATE = {
  25. };
  26. /**
  27. * The initial state of the feature base/config when executing in a React Native
  28. * environment. The mandatory configuration to be passed to JitsiMeetJS#init().
  29. * The app will download config.js from the Jitsi Meet deployment and take its
  30. * values into account but the values below will be enforced (because they are
  31. * essential to the correct execution of the application).
  32. *
  33. * @type {Object}
  34. */
  35. const INITIAL_RN_STATE = {
  36. analytics: {},
  37. // FIXME The support for audio levels in lib-jitsi-meet polls the statistics
  38. // of WebRTC at a short interval multiple times a second. Unfortunately,
  39. // React Native is slow to fetch these statistics from the native WebRTC
  40. // API, through the React Native bridge and eventually to JavaScript.
  41. // Because the audio levels are of no interest to the mobile app, it is
  42. // fastest to merely disable them.
  43. disableAudioLevels: true,
  44. p2p: {
  45. disabledCodec: '',
  46. disableH264: false, // deprecated
  47. preferredCodec: 'H264',
  48. preferH264: true // deprecated
  49. }
  50. };
  51. /**
  52. * Mapping between old configs controlling the conference info headers visibility and the
  53. * new configs. Needed in order to keep backwards compatibility.
  54. */
  55. const CONFERENCE_HEADER_MAPPING = {
  56. hideConferenceTimer: [ 'conference-timer' ],
  57. hideConferenceSubject: [ 'subject' ],
  58. hideParticipantsStats: [ 'participants-count' ],
  59. hideRecordingLabel: [ 'recording', 'local-recording' ]
  60. };
  61. ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
  62. switch (action.type) {
  63. case UPDATE_CONFIG:
  64. return _updateConfig(state, action);
  65. case CONFIG_WILL_LOAD:
  66. return {
  67. error: undefined,
  68. /**
  69. * The URL of the location associated with/configured by this
  70. * configuration.
  71. *
  72. * @type URL
  73. */
  74. locationURL: action.locationURL
  75. };
  76. case LOAD_CONFIG_ERROR:
  77. // XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
  78. // the asynchronous "loadConfig procedure/process" started with
  79. // CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
  80. // is settling the process needs to provide proof that they have
  81. // started it and that the iteration of the process being completed
  82. // now is still of interest to the app.
  83. if (state.locationURL === action.locationURL) {
  84. return {
  85. /**
  86. * The {@link Error} which prevented the loading of the
  87. * configuration of the associated {@code locationURL}.
  88. *
  89. * @type Error
  90. */
  91. error: action.error
  92. };
  93. }
  94. break;
  95. case SET_CONFIG:
  96. return _setConfig(state, action);
  97. case OVERWRITE_CONFIG:
  98. return {
  99. ...state,
  100. ...action.config
  101. };
  102. }
  103. return state;
  104. });
  105. /**
  106. * Gets the initial state of the feature base/config. The mandatory
  107. * configuration to be passed to JitsiMeetJS#init(). The app will download
  108. * config.js from the Jitsi Meet deployment and take its values into account but
  109. * the values below will be enforced (because they are essential to the correct
  110. * execution of the application).
  111. *
  112. * @returns {Object}
  113. */
  114. function _getInitialState() {
  115. return (
  116. navigator.product === 'ReactNative'
  117. ? INITIAL_RN_STATE
  118. : INITIAL_NON_RN_STATE);
  119. }
  120. /**
  121. * Reduces a specific Redux action SET_CONFIG of the feature
  122. * base/lib-jitsi-meet.
  123. *
  124. * @param {Object} state - The Redux state of the feature base/config.
  125. * @param {Action} action - The Redux action SET_CONFIG to reduce.
  126. * @private
  127. * @returns {Object} The new state after the reduction of the specified action.
  128. */
  129. function _setConfig(state, { config }) {
  130. // The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
  131. // does not download them at runtime from the deployment on which it will
  132. // join a conference. The downloading is planned for implementation in the
  133. // future (later rather than sooner) but is not implemented yet at the time
  134. // of this writing and, consequently, we must provide legacy support in the
  135. // meantime.
  136. // eslint-disable-next-line no-param-reassign
  137. config = _translateLegacyConfig(config);
  138. const { audioQuality } = config;
  139. const hdAudioOptions = {};
  140. if (audioQuality?.stereo) {
  141. Object.assign(hdAudioOptions, {
  142. disableAP: true,
  143. enableNoAudioDetection: false,
  144. enableNoisyMicDetection: false,
  145. enableTalkWhileMuted: false
  146. });
  147. }
  148. const newState = _.merge(
  149. {},
  150. config,
  151. hdAudioOptions,
  152. { error: undefined },
  153. // The config of _getInitialState() is meant to override the config
  154. // downloaded from the Jitsi Meet deployment because the former contains
  155. // values that are mandatory.
  156. _getInitialState()
  157. );
  158. _cleanupConfig(newState);
  159. return equals(state, newState) ? state : newState;
  160. }
  161. /**
  162. * Processes the conferenceInfo object against the defaults.
  163. *
  164. * @param {Object} config - The old config.
  165. * @returns {Object} The processed conferenceInfo object.
  166. */
  167. function _getConferenceInfo(config) {
  168. const { conferenceInfo } = config;
  169. if (conferenceInfo) {
  170. return {
  171. alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
  172. autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
  173. };
  174. }
  175. return {
  176. ...CONFERENCE_INFO
  177. };
  178. }
  179. /**
  180. * Constructs a new config {@code Object}, if necessary, out of a specific
  181. * config {@code Object} which is in the latest format supported by jitsi-meet.
  182. * Such a translation from an old config format to a new/the latest config
  183. * format is necessary because the mobile app bundles jitsi-meet and
  184. * lib-jitsi-meet at build time and does not download them at runtime from the
  185. * deployment on which it will join a conference.
  186. *
  187. * @param {Object} oldValue - The config {@code Object} which may or may not be
  188. * in the latest form supported by jitsi-meet and from which a new config
  189. * {@code Object} is to be constructed if necessary.
  190. * @returns {Object} A config {@code Object} which is in the latest format
  191. * supported by jitsi-meet.
  192. */
  193. function _translateLegacyConfig(oldValue: Object) {
  194. const newValue = oldValue;
  195. if (!Array.isArray(oldValue.toolbarButtons)
  196. && typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
  197. newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
  198. }
  199. if (!oldValue.toolbarConfig) {
  200. oldValue.toolbarConfig = {};
  201. }
  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. const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]);
  218. if (filteredConferenceInfo.length) {
  219. newValue.conferenceInfo = _getConferenceInfo(oldValue);
  220. filteredConferenceInfo.forEach(key => {
  221. // hideRecordingLabel does not mean not render it at all, but autoHide it
  222. if (key === 'hideRecordingLabel') {
  223. newValue.conferenceInfo.alwaysVisible
  224. = newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
  225. newValue.conferenceInfo.autoHide
  226. = _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
  227. } else {
  228. newValue.conferenceInfo.alwaysVisible
  229. = newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
  230. newValue.conferenceInfo.autoHide
  231. = newValue.conferenceInfo.autoHide.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
  232. }
  233. });
  234. }
  235. if (!oldValue.connectionIndicators
  236. && typeof interfaceConfig === 'object'
  237. && (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
  238. || interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
  239. || interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
  240. newValue.connectionIndicators = {
  241. disabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  242. autoHide: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  243. autoHideTimeout: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT
  244. };
  245. }
  246. newValue.disabledSounds = newValue.disabledSounds || [];
  247. if (oldValue.disableJoinLeaveSounds) {
  248. newValue.disabledSounds.unshift('PARTICIPANT_LEFT_SOUND', 'PARTICIPANT_JOINED_SOUND');
  249. }
  250. if (oldValue.disableRecordAudioNotification) {
  251. newValue.disabledSounds.unshift(
  252. 'RECORDING_ON_SOUND',
  253. 'RECORDING_OFF_SOUND',
  254. 'LIVE_STREAMING_ON_SOUND',
  255. 'LIVE_STREAMING_OFF_SOUND'
  256. );
  257. }
  258. if (oldValue.disableIncomingMessageSound) {
  259. newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
  260. }
  261. if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
  262. newValue.audioQuality = {
  263. opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,
  264. stereo: oldValue.audioQuality?.stereo ?? oldValue.stereo
  265. };
  266. }
  267. if (oldValue.disableModeratorIndicator === undefined
  268. && typeof interfaceConfig === 'object'
  269. && interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
  270. newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
  271. }
  272. return newValue;
  273. }
  274. /**
  275. * Updates the stored configuration with the given extra options.
  276. *
  277. * @param {Object} state - The Redux state of the feature base/config.
  278. * @param {Action} action - The Redux action to reduce.
  279. * @private
  280. * @returns {Object} The new state after the reduction of the specified action.
  281. */
  282. function _updateConfig(state, { config }) {
  283. const newState = _.merge({}, state, config);
  284. _cleanupConfig(newState);
  285. return equals(state, newState) ? state : newState;
  286. }