diff --git a/react/features/app/actions.js b/react/features/app/actions.js index 255d21f4d..26193be1e 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -1,7 +1,13 @@ /* @flow */ import { setRoom } from '../base/conference'; -import { configWillLoad, loadConfigError, setConfig } from '../base/config'; +import { + configWillLoad, + loadConfigError, + restoreConfig, + setConfig, + storeConfig +} from '../base/config'; import { setLocationURL } from '../base/connection'; import { loadConfig } from '../base/lib-jitsi-meet'; import { parseURIString } from '../base/util'; @@ -24,48 +30,6 @@ export function appNavigate(uri: ?string) { _appNavigateToOptionalLocation(dispatch, getState, parseURIString(uri)); } -/** - * Redirects to another page generated by replacing the path in the original URL - * with the given path. - * - * @param {(string)} pathname - The path to navigate to. - * @returns {Function} - */ -export function redirectWithStoredParams(pathname: string) { - return (dispatch: Dispatch<*>, getState: Function) => { - const { locationURL } = getState()['features/base/connection']; - const newLocationURL = new URL(locationURL.href); - - newLocationURL.pathname = pathname; - window.location.assign(newLocationURL.toString()); - }; -} - -/** - * Reloads the page by restoring the original URL. - * - * @returns {Function} - */ -export function reloadWithStoredParams() { - return (dispatch: Dispatch<*>, getState: Function) => { - const { locationURL } = getState()['features/base/connection']; - const windowLocation = window.location; - const oldSearchString = windowLocation.search; - - windowLocation.replace(locationURL.toString()); - - if (window.self !== window.top - && locationURL.search === oldSearchString) { - // NOTE: Assuming that only the hash or search part of the URL will - // be changed! - // location.reload will not trigger redirect/reload for iframe when - // only the hash params are changed. That's why we need to call - // reload in addition to replace. - windowLocation.reload(); - } - }; -} - /** * Triggers an in-app navigation to a specific location URI. * @@ -89,7 +53,7 @@ function _appNavigateToMandatoryLocation( dispatch(configWillLoad(newLocation)); return ( - _loadConfig(newLocation) + _loadConfig(dispatch, getState, newLocation) .then( config => loadConfigSettled(/* error */ undefined, config), error => loadConfigSettled(error, /* config */ undefined)) @@ -214,12 +178,17 @@ export function appWillUnmount(app: Object) { /** * Loads config.js from a specific host. * + * @param {Dispatch} dispatch - The redux {@code dispatch} function. + * @param {Function} getState - The redux {@code getState} function. * @param {Object} location - The location URI which specifies the host to load * the config.js from. * @private * @returns {Promise} */ -function _loadConfig({ contextRoot, host, protocol, room }) { +function _loadConfig( + dispatch: Dispatch<*>, + getState: Function, + { contextRoot, host, protocol, room }) { // XXX As the mobile/React Native app does not employ config on the // WelcomePage, do not download config.js from the deployment when // navigating to the WelcomePage - the perceived/visible navigation will be @@ -246,21 +215,9 @@ function _loadConfig({ contextRoot, host, protocol, room }) { /* eslint-enable no-param-reassign */ - const key = `config.js/${baseURL}`; - return loadConfig(url).then( /* onFulfilled */ config => { - // Try to store the configuration in localStorage. If the deployment - // specified 'getroom' as a function, for example, it does not make - // sense to and it will not be stored. - try { - if (typeof window.config === 'undefined' - || window.config !== config) { - window.localStorage.setItem(key, JSON.stringify(config)); - } - } catch (e) { - // Ignore the error because the caching is optional. - } + dispatch(storeConfig(baseURL, config)); return config; }, @@ -268,23 +225,54 @@ function _loadConfig({ contextRoot, host, protocol, room }) { // XXX The (down)loading of config failed. Try to use the last // successfully fetched for that deployment. It may not match the // shard. - let storage; + const config = restoreConfig(baseURL); - try { - // XXX Even reading the property localStorage of window may - // throw an error (which is user agent-specific behavior). - storage = window.localStorage; - - const config = storage.getItem(key); - - if (config) { - return JSON.parse(config); - } - } catch (e) { - // Somehow incorrect data ended up in the storage. Clean it up. - storage && storage.removeItem(key); + if (config) { + return config; } throw error; }); } + +/** + * Redirects to another page generated by replacing the path in the original URL + * with the given path. + * + * @param {(string)} pathname - The path to navigate to. + * @returns {Function} + */ +export function redirectWithStoredParams(pathname: string) { + return (dispatch: Dispatch<*>, getState: Function) => { + const { locationURL } = getState()['features/base/connection']; + const newLocationURL = new URL(locationURL.href); + + newLocationURL.pathname = pathname; + window.location.assign(newLocationURL.toString()); + }; +} + +/** + * Reloads the page by restoring the original URL. + * + * @returns {Function} + */ +export function reloadWithStoredParams() { + return (dispatch: Dispatch<*>, getState: Function) => { + const { locationURL } = getState()['features/base/connection']; + const windowLocation = window.location; + const oldSearchString = windowLocation.search; + + windowLocation.replace(locationURL.toString()); + + if (window.self !== window.top + && locationURL.search === oldSearchString) { + // NOTE: Assuming that only the hash or search part of the URL will + // be changed! + // location.reload will not trigger redirect/reload for iframe when + // only the hash params are changed. That's why we need to call + // reload in addition to replace. + windowLocation.reload(); + } + }; +} diff --git a/react/features/base/config/actions.js b/react/features/base/config/actions.js index 432317a30..2b7c1fe0a 100644 --- a/react/features/base/config/actions.js +++ b/react/features/base/config/actions.js @@ -2,11 +2,15 @@ import type { Dispatch } from 'redux'; +import { addKnownDomains } from '../known-domains'; +import { parseURIString } from '../util'; + import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes'; +import { _CONFIG_STORE_PREFIX } from './constants'; import { setConfigFromURLParams } from './functions'; /** @@ -87,3 +91,44 @@ export function setConfig(config: Object = {}) { }); }; } + +/** + * Stores a specific Jitsi Meet config.js object into {@code localStorage}. + * + * @param {string} baseURL - The base URL from which the config.js was + * downloaded. + * @param {Object} config - The Jitsi Meet config.js to store. + * @returns {Function} + */ +export function storeConfig(baseURL: string, config: Object) { + return (dispatch: Dispatch<*>) => { + // Try to store the configuration in localStorage. If the deployment + // specified 'getroom' as a function, for example, it does not make + // sense to and it will not be stored. + let b = false; + + try { + if (typeof window.config === 'undefined' + || window.config !== config) { + window.localStorage.setItem( + `${_CONFIG_STORE_PREFIX}/${baseURL}`, + JSON.stringify(config)); + b = true; + } + } catch (e) { + // Ignore the error because the caching is optional. + } + + // If base/config knows a domain, then the app knows it. + if (b) { + try { + dispatch(addKnownDomains(parseURIString(baseURL).host)); + } catch (e) { + // Ignore the error because the fiddling with "known domains" is + // a side effect here. + } + } + + return b; + }; +} diff --git a/react/features/base/config/constants.js b/react/features/base/config/constants.js new file mode 100644 index 000000000..f86f3c59e --- /dev/null +++ b/react/features/base/config/constants.js @@ -0,0 +1,8 @@ +/** + * The prefix of the {@code localStorage} key into which {@link storeConfig} + * stores and from which {@link restoreConfig} restores. + * + * @protected + * @type string + */ +export const _CONFIG_STORE_PREFIX = 'config.js'; diff --git a/react/features/base/config/functions.js b/react/features/base/config/functions.js index c64f7f60c..12cfbe11b 100644 --- a/react/features/base/config/functions.js +++ b/react/features/base/config/functions.js @@ -2,6 +2,7 @@ import _ from 'lodash'; +import { _CONFIG_STORE_PREFIX } from './constants'; import parseURLParams from './parseURLParams'; declare var $: Object; @@ -238,6 +239,39 @@ function _getWhitelistedJSON(configName, configJSON) { return _.pick(configJSON, WHITELISTED_KEYS); } +/** + * 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): ?Object { + let storage; + const key = `${_CONFIG_STORE_PREFIX}/${baseURL}`; + + try { + // XXX Even reading the property localStorage of window may throw an + // error (which is user agent-specific behavior). + storage = window.localStorage; + + const config = storage.getItem(key); + + if (config) { + return JSON.parse(config) || undefined; + } + } catch (e) { + // Somehow incorrect data ended up in the storage. Clean it up. + storage && storage.removeItem(key); + } + + return undefined; +} + /* eslint-disable max-params */ /** diff --git a/react/features/base/config/middleware.js b/react/features/base/config/middleware.js index 25099c77c..7a8674a63 100644 --- a/react/features/base/config/middleware.js +++ b/react/features/base/config/middleware.js @@ -1,8 +1,12 @@ // @flow +import { APP_WILL_MOUNT } from '../../app'; +import { addKnownDomains } from '../known-domains'; import { MiddlewareRegistry } from '../redux'; +import { parseURIString } from '../util'; import { SET_CONFIG } from './actionTypes'; +import { _CONFIG_STORE_PREFIX } from './constants'; /** * The middleware of the feature {@code base/config}. @@ -13,6 +17,9 @@ import { SET_CONFIG } from './actionTypes'; */ MiddlewareRegistry.register(store => next => action => { switch (action.type) { + case APP_WILL_MOUNT: + return _appWillMount(store, next, action); + case SET_CONFIG: return _setConfig(store, next, action); } @@ -20,6 +27,59 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); +/** + * Notifies the feature {@code base/config} that the {@link APP_WILL_MOUNT} + * redux action is being {@code dispatch}ed in a specific redux store. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} in the specified {@code store}. + * @param {Action} action - The redux action which is being {@code dispatch}ed + * in the specified {@code store}. + * @private + * @returns {*} The return value of {@code next(action)}. + */ +function _appWillMount(store, next, action) { + const result = next(action); + + // It's an opportune time to transfer the feature base/config's knowledge + // about "known domains" (which is local to the feature) to the feature + // base/known-domains (which is global to the app). + // + // XXX Since the feature base/config predates the feature calendar-sync and, + // consequently, the feature known-domains, it's possible for the feature + // base/config to know of domains which the feature known-domains is yet to + // discover. + const { localStorage } = window; + + if (localStorage) { + const prefix = `${_CONFIG_STORE_PREFIX}/`; + const knownDomains = []; + + for (let i = 0; /* localStorage.key(i) */; ++i) { + const key = localStorage.key(i); + + if (key) { + let baseURL; + + if (key.startsWith(prefix) + && (baseURL = key.substring(prefix.length))) { + const uri = parseURIString(baseURL); + let host; + + uri && (host = uri.host) && knownDomains.push(host); + } + } else { + break; + } + } + knownDomains.length && store.dispatch(addKnownDomains(knownDomains)); + } + + return result; +} + /** * Notifies the feature {@code base/config} that the {@link SET_CONFIG} redux * action is being {@code dispatch}ed in a specific redux store.