/* eslint-disable lines-around-comment */ // @ts-ignore import Bourne from '@hapi/bourne'; // @ts-ignore import { jitsiLocalStorage } from '@jitsi/js-utils'; import _ from 'lodash'; import { IState } from '../../app/types'; import { browser } from '../lib-jitsi-meet'; import { parseURLParams } from '../util/parseURLParams'; import { IConfig } from './configType'; import CONFIG_WHITELIST from './configWhitelist'; import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants'; import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist'; import logger from './logger'; // XXX The function getRoomName is split out of // functions.any.js because it is bundled in both app.bundle and // do_external_connect, webpack 1 does not support tree shaking, and we don't // want all functions to be bundled in do_external_connect. export { default as getRoomName } from './getRoomName'; /** * Create a "fake" configuration object for the given base URL. This is used in case the config * couldn't be loaded in the welcome page, so at least we have something to try with. * * @param {string} baseURL - URL of the deployment for which we want the fake config. * @returns {Object} */ export function createFakeConfig(baseURL: string) { const url = new URL(baseURL); return { hosts: { domain: url.hostname, muc: `conference.${url.hostname}` }, bosh: `${baseURL}http-bind`, p2p: { enabled: true } }; } /** * Selector used to get the meeting region. * * @param {Object} state - The global state. * @returns {string} */ export function getMeetingRegion(state: IState) { return state['features/base/config']?.deploymentInfo?.region || ''; } /** * Selector for determining if receiving multiple stream support is enabled. * * @param {Object} state - The global state. * @returns {boolean} */ export function getMultipleVideoSupportFeatureFlag(state: IState) { return (getFeatureFlag(state, FEATURE_FLAGS.MULTIPLE_VIDEO_STREAMS_SUPPORT) && getSourceNameSignalingFeatureFlag(state)) ?? true; } /** * Selector for determining if sending multiple stream support is enabled. * * @param {Object} state - The global state. * @returns {boolean} */ export function getMultipleVideoSendingSupportFeatureFlag(state: IState) { return navigator.product !== 'ReactNative' && ((getMultipleVideoSupportFeatureFlag(state) ?? true) && isUnifiedPlanEnabled(state)); } /** * Selector used to get the sourceNameSignaling feature flag. * * @param {Object} state - The global state. * @returns {boolean} */ export function getSourceNameSignalingFeatureFlag(state: IState) { return getFeatureFlag(state, FEATURE_FLAGS.SOURCE_NAME_SIGNALING) ?? true; } /** * Selector used to get a feature flag. * * @param {Object} state - The global state. * @param {string} featureFlag - The name of the feature flag. * @returns {boolean} */ export function getFeatureFlag(state: IState, featureFlag: string) { const featureFlags = state['features/base/config']?.flags || {}; return featureFlags[featureFlag as keyof typeof featureFlags]; } /** * Selector used to get the disableRemoveRaisedHandOnFocus. * * @param {Object} state - The global state. * @returns {boolean} */ export function getDisableRemoveRaisedHandOnFocus(state: IState) { return state['features/base/config']?.disableRemoveRaisedHandOnFocus || false; } /** * Selector used to get the endpoint used for fetching the recording. * * @param {Object} state - The global state. * @returns {string} */ export function getRecordingSharingUrl(state: IState) { return state['features/base/config'].recordingSharingUrl; } /** * Overrides JSON properties in {@code config} and * {@code interfaceConfig} Objects with the values from {@code newConfig}. * Overrides only the whitelisted keys. * * @param {Object} config - The config Object in which we'll be overriding * properties. * @param {Object} interfaceConfig - The interfaceConfig Object in which we'll * be overriding properties. * @param {Object} json - Object containing configuration properties. * Destination object is selected based on root property name: * { * config: { * // config.js properties here * }, * interfaceConfig: { * // interface_config.js properties here * } * }. * @returns {void} */ export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json: any) { for (const configName of Object.keys(json)) { let configObj; if (configName === 'config') { configObj = config; } else if (configName === 'interfaceConfig') { configObj = interfaceConfig; } if (configObj) { const configJSON = getWhitelistedJSON(configName as 'interfaceConfig' | 'config', json[configName]); if (!_.isEmpty(configJSON)) { logger.info( `Extending ${configName} with: ${ JSON.stringify(configJSON)}`); // eslint-disable-next-line arrow-body-style _.mergeWith(configObj, configJSON, (oldValue, newValue) => { // XXX We don't want to merge the arrays, we want to // overwrite them. return Array.isArray(oldValue) ? newValue : undefined; }); } } } } /* eslint-enable max-params, no-shadow */ /** * Apply whitelist filtering for configs with whitelists. * Only extracts overridden values for keys we allow to be overridden. * * @param {string} configName - The config name, one of config or interfaceConfig. * @param {Object} configJSON - The object with keys and values to override. * @returns {Object} - The result object only with the keys * that are whitelisted. */ export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object { if (configName === 'interfaceConfig') { return _.pick(configJSON, INTERFACE_CONFIG_WHITELIST); } else if (configName === 'config') { return _.pick(configJSON, CONFIG_WHITELIST); } return configJSON; } /** * Selector for determining if the display name is read only. * * @param {Object} state - The state of the app. * @returns {boolean} */ export function isNameReadOnly(state: IState): boolean { return Boolean(state['features/base/config'].disableProfile || state['features/base/config'].readOnlyName); } /** * Selector for determining if the display name is visible. * * @param {Object} state - The state of the app. * @returns {boolean} */ export function isDisplayNameVisible(state: IState): boolean { return !state['features/base/config'].hideDisplayName; } /** * Selector for determining if Unified plan support is enabled. * * @param {Object} state - The state of the app. * @returns {boolean} */ export function isUnifiedPlanEnabled(state: IState): boolean { const { enableUnifiedOnChrome = true } = state['features/base/config']; return browser.supportsUnifiedPlan() && (!browser.isChromiumBased() || (browser.isChromiumBased() && enableUnifiedOnChrome)); } /** * Restores a Jitsi Meet config.js from {@code localStorage} if it was * previously downloaded from a specific {@code baseURL} and stored with * {@link storeConfig}. * * @param {string} baseURL - The base URL from which the config.js was * previously downloaded and stored with {@code storeConfig}. * @returns {?Object} The Jitsi Meet config.js which was previously downloaded * from {@code baseURL} and stored with {@code storeConfig} if it was restored; * otherwise, {@code undefined}. */ export function restoreConfig(baseURL: string) { const key = `${_CONFIG_STORE_PREFIX}/${baseURL}`; const config = jitsiLocalStorage.getItem(key); if (config) { try { return Bourne.parse(config) || undefined; } catch (e) { // Somehow incorrect data ended up in the storage. Clean it up. jitsiLocalStorage.removeItem(key); } } return undefined; } /* eslint-disable max-params */ /** * Inspects the hash part of the location URI and overrides values specified * there in the corresponding config objects given as the arguments. The syntax * is: {@code https://server.com/room#config.debug=true * &interfaceConfig.showButton=false}. * * In the hash part each parameter will be parsed to JSON and then the root * object will be matched with the corresponding config object given as the * argument to this function. * * @param {Object} config - This is the general config. * @param {Object} interfaceConfig - This is the interface config. * @param {URI} location - The new location to which the app is navigating to. * @returns {void} */ export function setConfigFromURLParams( config: IConfig, interfaceConfig: any, location: string | URL) { const params = parseURLParams(location); const json: any = {}; // At this point we have: // params = { // "config.disableAudioLevels": false, // "config.channelLastN": -1, // "interfaceConfig.APP_NAME": "Jitsi Meet" // } // We want to have: // json = { // config: { // "disableAudioLevels": false, // "channelLastN": -1 // }, // interfaceConfig: { // "APP_NAME": "Jitsi Meet" // } // } config && (json.config = {}); interfaceConfig && (json.interfaceConfig = {}); for (const param of Object.keys(params)) { let base = json; const names = param.split('.'); const last = names.pop() ?? ''; for (const name of names) { base = base[name] = base[name] || {}; } base[last] = params[param]; } overrideConfigJSON(config, interfaceConfig, json); } /* eslint-enable max-params */