From f53fb3d8140d080657776d4ef09a0840ef324a2a Mon Sep 17 00:00:00 2001 From: Ilya Daynatovich Date: Wed, 14 Dec 2016 14:09:29 +0200 Subject: [PATCH] Introduced new actions and functions for app initialization --- .../app/{actions.js => actions.native.js} | 0 react/features/app/actions.web.js | 173 ++++++++++++++ react/features/app/components/App.web.js | 84 +------ .../app/{functions.js => functions.native.js} | 0 react/features/app/functions.web.js | 214 ++++++++++++++++++ 5 files changed, 391 insertions(+), 80 deletions(-) rename react/features/app/{actions.js => actions.native.js} (100%) create mode 100644 react/features/app/actions.web.js rename react/features/app/{functions.js => functions.native.js} (100%) create mode 100644 react/features/app/functions.web.js diff --git a/react/features/app/actions.js b/react/features/app/actions.native.js similarity index 100% rename from react/features/app/actions.js rename to react/features/app/actions.native.js diff --git a/react/features/app/actions.web.js b/react/features/app/actions.web.js new file mode 100644 index 000000000..2ec370379 --- /dev/null +++ b/react/features/app/actions.web.js @@ -0,0 +1,173 @@ +import { setRoom } from '../base/conference'; +import { + getDomain, + setDomain +} from '../base/connection'; +import { + loadConfig, + setConfig +} from '../base/lib-jitsi-meet'; + +import { + APP_WILL_MOUNT, + APP_WILL_UNMOUNT +} from './actionTypes'; +import { + _getRoomAndDomainFromUrlString, + _getRouteToRender, + init +} from './functions'; +import './reducer'; + + +/** + * Triggers an in-app navigation to a different route. Allows navigation to be + * abstracted between the mobile and web versions. + * + * @param {(string|undefined)} urlOrRoom - The URL or room name to which to + * navigate. + * @returns {Function} + */ +export function appNavigate(urlOrRoom) { + return (dispatch, getState) => { + const oldDomain = getDomain(getState()); + + const { domain, room } = _getRoomAndDomainFromUrlString(urlOrRoom); + + // TODO Kostiantyn Tsaregradskyi: We should probably detect if user is + // currently in a conference and ask her if she wants to close the + // current conference and start a new one with the new room name or + // domain. + + if (typeof domain === 'undefined' || oldDomain === domain) { + // If both domain and room vars became undefined, that means we're + // actually dealing with just room name and not with URL. + dispatch( + _setRoomAndNavigate( + typeof room === 'undefined' && typeof domain === 'undefined' + ? urlOrRoom + : room)); + } else if (oldDomain !== domain) { + // Update domain without waiting for config to be loaded to prevent + // race conditions when we will start to load config multiple times. + dispatch(setDomain(domain)); + + // If domain has changed, we need to load the config of the new + // domain and set it, and only after that we can navigate to + // different route. + loadConfig(`https://${domain}`) + .then( + config => configLoaded(/* err */ undefined, config), + err => configLoaded(err, /* config */ undefined)); + } + + /** + * Notifies that an attempt to load the config(uration) of domain has + * completed. + * + * @param {string|undefined} err - If the loading has failed, the error + * detailing the cause of the failure. + * @param {Object|undefined} config - If the loading has succeeded, the + * loaded config(uration). + * @returns {void} + */ + function configLoaded(err, config) { + if (err) { + // XXX The failure could be, for example, because of a + // certificate-related error. In which case the connection will + // fail later in Strophe anyway even if we use the default + // config here. + + // The function loadConfig will log the err. + return; + } + + // We set room name only here to prevent race conditions on app + // start to not make app re-render conference page for two times. + dispatch(setRoom(room)); + dispatch(setConfig(config)); + _navigate(getState()); + } + }; +} + +/** + * Temporary solution. Should dispatch actions related to + * initial settings of the app like setting log levels, + * reading the config parameters from query string etc. + * + * @returns {Function} + */ +export function appInit() { + return () => { + init(); + }; +} + +/** + * Signals that a specific App will mount (in the terms of React). + * + * @param {App} app - The App which will mount. + * @returns {{ + * type: APP_WILL_MOUNT, + * app: App + * }} + */ +export function appWillMount(app) { + return { + type: APP_WILL_MOUNT, + app + }; +} + +/** + * Signals that a specific App will unmount (in the terms of React). + * + * @param {App} app - The App which will unmount. + * @returns {{ + * type: APP_WILL_UNMOUNT, + * app: App + * }} + */ +export function appWillUnmount(app) { + return { + type: APP_WILL_UNMOUNT, + app + }; +} + +/** + * Navigates to route corresponding to current room name. + * + * @param {Object} state - Redux state. + * @private + * @returns {void} + */ +function _navigate(state) { + const app = state['features/app'].app; + const routeToRender = _getRouteToRender(state); + + app._navigate(routeToRender); +} + +/** + * Sets room and navigates to new route if needed. + * + * @param {string} newRoom - New room name. + * @private + * @returns {Function} + */ +function _setRoomAndNavigate(newRoom) { + return (dispatch, getState) => { + const oldRoom = getState()['features/base/conference'].room; + + dispatch(setRoom(newRoom)); + + const state = getState(); + const room = state['features/base/conference'].room; + + if (room !== oldRoom) { + _navigate(state); + } + }; +} diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index 34cb7c242..252812444 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -1,4 +1,4 @@ -/* global APP, JitsiMeetJS, loggingConfig $ */ +/* global $ */ import React from 'react'; import { Provider } from 'react-redux'; import { compose } from 'redux'; @@ -13,18 +13,7 @@ import { getDomain } from '../../base/connection'; import { RouteRegistry } from '../../base/navigator'; import { AbstractApp } from './AbstractApp'; -import settings from '../../../../modules/settings/Settings'; - - -import URLProcessor from '../../../../modules/config/URLProcessor'; -import getTokenData from '../../../../modules/tokendata/TokenData'; -import JitsiMeetLogStorage from '../../../../modules/util/JitsiMeetLogStorage'; - -// eslint-disable-next-line max-len -import KeyboardShortcut from '../../../../modules/keyboardshortcut/keyboardshortcut'; - -const Logger = require('jitsi-meet-logger'); -const LogCollector = Logger.LogCollector; +import { appInit } from '../actions'; /** @@ -64,78 +53,13 @@ export class App extends AbstractApp { } /** - * Init translation from old app. + * Inits the app before component will mount. * * @inheritdoc */ componentWillMount(...args) { super.componentWillMount(...args); - - URLProcessor.setConfigParametersFromUrl(); - - /* APP.init BEGIN */ - - /* Init logging BEGIN */ - - // Adjust logging level - configureLoggingLevels(); - - // Create the LogCollector and register it as the global log transport. - // It is done early to capture as much logs as possible. Captured logs - // will be cached, before the JitsiMeetLogStorage gets ready (statistics - // module is initialized). - if (!APP.logCollector && !loggingConfig.disableLogCollector) { - APP.logCollector = new LogCollector(new JitsiMeetLogStorage()); - Logger.addGlobalTransport(APP.logCollector); - JitsiMeetJS.addGlobalLogTransport(APP.logCollector); - } - - /* Init logging BEGIN */ - - APP.keyboardshortcut = KeyboardShortcut; - APP.tokenData = getTokenData(); - - /* APP.init END */ - - APP.API.init(APP.tokenData.externalAPISettings); - - /** - * Adjusts the logging levels. - * - * @private - * @returns {void} - */ - function configureLoggingLevels() { - // NOTE The library Logger is separated from - // the app loggers, so the levels - // have to be set in two places - - // Set default logging level - const defaultLogLevel - = loggingConfig.defaultLogLevel || JitsiMeetJS.logLevels.TRACE; - - Logger.setLogLevel(defaultLogLevel); - JitsiMeetJS.setLogLevel(defaultLogLevel); - - // NOTE console was used on purpose here to go around the logging - // and always print the default logging level to the console - console.info(`Default logging level set to: ${defaultLogLevel}`); - - // Set log level for each logger - if (loggingConfig) { - Object.keys(loggingConfig).forEach(loggerName => { - if (loggerName !== 'defaultLogLevel') { - const level = loggingConfig[loggerName]; - - Logger.setLogLevelById(level, loggerName); - JitsiMeetJS.setLogLevelById(level, loggerName); - } - }); - } - } - - - APP.translation.init(settings.getLanguage()); + this.props.store.dispatch(appInit()); } /** diff --git a/react/features/app/functions.js b/react/features/app/functions.native.js similarity index 100% rename from react/features/app/functions.js rename to react/features/app/functions.native.js diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js new file mode 100644 index 000000000..5f541c74a --- /dev/null +++ b/react/features/app/functions.web.js @@ -0,0 +1,214 @@ +/* global APP, JitsiMeetJS, loggingConfig */ +import { isRoomValid } from '../base/conference'; +import { RouteRegistry } from '../base/navigator'; +import { Conference } from '../conference'; +import { WelcomePage } from '../welcome'; + +import getTokenData from '../../../modules/tokendata/TokenData'; +import settings from '../../../modules/settings/Settings'; + +import URLProcessor from '../../../modules/config/URLProcessor'; +import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage'; + +// eslint-disable-next-line max-len +import KeyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut'; + +const Logger = require('jitsi-meet-logger'); +const LogCollector = Logger.LogCollector; + + +/** + * Gets room name and domain from URL object. + * + * @param {URL} url - URL object. + * @private + * @returns {{ + * domain: (string|undefined), + * room: (string|undefined) + * }} + */ +function _getRoomAndDomainFromUrlObject(url) { + let domain; + let room; + + if (url) { + domain = url.hostname; + room = url.pathname.substr(1); + + // Convert empty string to undefined to simplify checks. + if (room === '') { + room = undefined; + } + if (domain === '') { + domain = undefined; + } + } + + return { + domain, + room + }; +} + +/** + * Gets conference room name and connection domain from URL. + * + * @param {(string|undefined)} url - URL. + * @returns {{ + * domain: (string|undefined), + * room: (string|undefined) + * }} + */ +export function _getRoomAndDomainFromUrlString(url) { + // Rewrite the specified URL in order to handle special cases such as + // hipchat.com and enso.me which do not follow the common pattern of most + // Jitsi Meet deployments. + if (typeof url === 'string') { + // hipchat.com + let regex = /^(https?):\/\/hipchat.com\/video\/call\//gi; + let match = regex.exec(url); + + if (!match) { + // enso.me + regex = /^(https?):\/\/enso\.me\/(?:call|meeting)\//gi; + match = regex.exec(url); + } + if (match && match.length > 1) { + /* eslint-disable no-param-reassign, prefer-template */ + + url + = match[1] /* URL protocol */ + + '://enso.hipchat.me/' + + url.substring(regex.lastIndex); + + /* eslint-enable no-param-reassign, prefer-template */ + } + } + + return _getRoomAndDomainFromUrlObject(_urlStringToObject(url)); +} + +/** + * Determines which route is to be rendered in order to depict a specific Redux + * store. + * + * @param {(Object|Function)} stateOrGetState - Redux state or Regux getState() + * method. + * @returns {Route} + */ +export function _getRouteToRender(stateOrGetState) { + const state + = typeof stateOrGetState === 'function' + ? stateOrGetState() + : stateOrGetState; + const room = state['features/base/conference'].room; + const component = isRoomValid(room) ? Conference : WelcomePage; + + return RouteRegistry.getRouteByComponent(component); +} + +/** + * Parses a string into a URL (object). + * + * @param {(string|undefined)} url - The URL to parse. + * @private + * @returns {URL} + */ +function _urlStringToObject(url) { + let urlObj; + + if (url) { + try { + urlObj = new URL(url); + } catch (ex) { + // The return value will signal the failure & the logged + // exception will provide the details to the developers. + console.log(`${url} seems to be not a valid URL, but it's OK`, ex); + } + } + + return urlObj; +} + +/** + * Temporary solution. Later we'll get rid of global APP + * and set its properties in redux store. + * + * @returns {void} + */ +export function init() { + _setConfigParametersFromUrl(); + _initLogging(); + + APP.keyboardshortcut = KeyboardShortcut; + APP.tokenData = getTokenData(); + APP.API.init(APP.tokenData.externalAPISettings); + + APP.translation.init(settings.getLanguage()); +} + +/** + * Initializes logging in the app. + * + * @private + * @returns {void} + */ +function _initLogging() { + // Adjust logging level + configureLoggingLevels(); + + // Create the LogCollector and register it as the global log transport. + // It is done early to capture as much logs as possible. Captured logs + // will be cached, before the JitsiMeetLogStorage gets ready (statistics + // module is initialized). + if (!APP.logCollector && !loggingConfig.disableLogCollector) { + APP.logCollector = new LogCollector(new JitsiMeetLogStorage()); + Logger.addGlobalTransport(APP.logCollector); + JitsiMeetJS.addGlobalLogTransport(APP.logCollector); + } +} + +/** + * Adjusts the logging levels. + * + * @private + * @returns {void} + */ +function configureLoggingLevels() { + // NOTE The library Logger is separated from + // the app loggers, so the levels + // have to be set in two places + + // Set default logging level + const defaultLogLevel + = loggingConfig.defaultLogLevel || JitsiMeetJS.logLevels.TRACE; + + Logger.setLogLevel(defaultLogLevel); + JitsiMeetJS.setLogLevel(defaultLogLevel); + + // NOTE console was used on purpose here to go around the logging + // and always print the default logging level to the console + console.info(`Default logging level set to: ${defaultLogLevel}`); + + // Set log level for each logger + if (loggingConfig) { + Object.keys(loggingConfig).forEach(loggerName => { + if (loggerName !== 'defaultLogLevel') { + const level = loggingConfig[loggerName]; + + Logger.setLogLevelById(level, loggerName); + JitsiMeetJS.setLogLevelById(level, loggerName); + } + }); + } +} + +/** + * Sets config parameters from query string. + * + * @private + * @returns {void} + */ +function _setConfigParametersFromUrl() { + URLProcessor.setConfigParametersFromUrl(); +}