diff --git a/app.js b/app.js index 978671546..044c8c0fc 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,4 @@ -/* global $, config, getRoomName, loggingConfig, JitsiMeetJS */ /* application specific logic */ -const logger = require("jitsi-meet-logger").getLogger(__filename); import "babel-polyfill"; import "jquery"; @@ -18,104 +16,13 @@ import 'aui-experimental-css'; window.toastr = require("toastr"); -const Logger = require("jitsi-meet-logger"); -const LogCollector = Logger.LogCollector; -import JitsiMeetLogStorage from "./modules/util/JitsiMeetLogStorage"; - -import URLProcessor from "./modules/config/URLProcessor"; -import { - generateRoomWithoutSeparator -} from './react/features/base/util/roomnameGenerator'; - import UI from "./modules/UI/UI"; import settings from "./modules/settings/Settings"; import conference from './conference'; -import ConferenceUrl from './modules/URL/ConferenceUrl'; import API from './modules/API/API'; -import UIEvents from './service/UI/UIEvents'; -import getTokenData from "./modules/tokendata/TokenData"; import translation from "./modules/translation/translation"; -const ConferenceEvents = JitsiMeetJS.events.conference; - -/** - * Tries to push history state with the following parameters: - * 'VideoChat', `Room: ${roomName}`, URL. If fail, prints the error and returns - * it. - */ -function pushHistoryState(roomName, URL) { - try { - window.history.pushState( - 'VideoChat', `Room: ${roomName}`, URL - ); - } catch (e) { - logger.warn("Push history state failed with parameters:", - 'VideoChat', `Room: ${roomName}`, URL, e); - return e; - } - return null; -} - -/** - * Replaces current history state(replaces the URL displayed by the browser). - * @param {string} newUrl the URL string which is to be displayed by the browser - * to the user. - */ -function replaceHistoryState (newUrl) { - if (window.history - && typeof window.history.replaceState === 'function') { - window.history.replaceState({}, document.title, newUrl); - } -} - -/** - * Builds and returns the room name. - */ -function buildRoomName () { - let roomName = getRoomName(); - - if(!roomName) { - let word = generateRoomWithoutSeparator(); - roomName = word.toLowerCase(); - let historyURL = window.location.href + word; - //Trying to push state with current URL + roomName - pushHistoryState(word, historyURL); - } - - return roomName; -} - -/** - * Adjusts the logging levels. - * @private - */ -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(function(loggerName) { - if ('defaultLogLevel' !== loggerName) { - const level = loggingConfig[loggerName]; - Logger.setLogLevelById(level, loggerName); - JitsiMeetJS.setLogLevelById(level, loggerName); - } - }); - } -} - const APP = { // Used by do_external_connect.js if we receive the attach data after // connect was already executed. status property can be "initialized", @@ -152,164 +59,15 @@ const APP = { */ ConferenceUrl : null, connection: null, - API, - init () { - this.initLogging(); - this.keyboardshortcut = - require("./modules/keyboardshortcut/keyboardshortcut"); - this.configFetch = require("./modules/config/HttpConfigFetch"); - this.tokenData = getTokenData(); - }, - 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 (!this.logCollector && !loggingConfig.disableLogCollector) { - this.logCollector = new LogCollector(new JitsiMeetLogStorage()); - Logger.addGlobalTransport(this.logCollector); - JitsiMeetJS.addGlobalLogTransport(this.logCollector); - } - } + API }; -/** - * If JWT token data it will be used for local user settings - */ -function setTokenData() { - let localUser = APP.tokenData.caller; - if(localUser) { - APP.settings.setEmail((localUser.getEmail() || "").trim(), true); - APP.settings.setAvatarUrl((localUser.getAvatarUrl() || "").trim()); - APP.settings.setDisplayName((localUser.getName() || "").trim(), true); - } -} - -function init() { - setTokenData(); - // Initialize the conference URL handler - APP.ConferenceUrl = new ConferenceUrl(window.location); - // Clean up the URL displayed by the browser - replaceHistoryState(APP.ConferenceUrl.getInviteUrl()); - - // TODO The execution of the mobile app starts from react/index.native.js. - // Similarly, the execution of the Web app should start from - // react/index.web.js for the sake of consistency and ease of understanding. - // Temporarily though because we are at the beginning of introducing React - // into the Web app, allow the execution of the Web app to start from app.js - // in order to reduce the complexity of the beginning step. - require('./react'); - - const isUIReady = APP.UI.start(); - if (isUIReady) { - APP.conference.init({roomName: buildRoomName()}).then(() => { - - if (APP.logCollector) { - // Start the LogCollector's periodic "store logs" task only if - // we're in the conference and not on the welcome page. This is - // determined by the value of "isUIReady" const above. - APP.logCollector.start(); - APP.logCollectorStarted = true; - // Make an attempt to flush in case a lot of logs have been - // cached, before the collector was started. - APP.logCollector.flush(); - - // This event listener will flush the logs, before - // the statistics module (CallStats) is stopped. - // - // NOTE The LogCollector is not stopped, because this event can - // be triggered multiple times during single conference - // (whenever statistics module is stopped). That includes - // the case when Jicofo terminates the single person left in the - // room. It will then restart the media session when someone - // eventually join the room which will start the stats again. - APP.conference.addConferenceListener( - ConferenceEvents.BEFORE_STATISTICS_DISPOSED, - () => { - if (APP.logCollector) { - APP.logCollector.flush(); - } - } - ); - } - - APP.UI.initConference(); - - APP.UI.addListener(UIEvents.LANG_CHANGED, language => { - APP.translation.setLanguage(language); - APP.settings.setLanguage(language); - }); - - APP.keyboardshortcut.init(); - }).catch(err => { - APP.UI.hideRingOverLay(); - APP.API.notifyConferenceLeft(APP.conference.roomName); - logger.error(err); - }); - } -} - -/** - * If we have an HTTP endpoint for getting config.json configured we're going to - * read it and override properties from config.js and interfaceConfig.js. - * If there is no endpoint we'll just continue with initialization. - * Keep in mind that if the endpoint has been configured and we fail to obtain - * the config for any reason then the conference won't start and error message - * will be displayed to the user. - */ -function obtainConfigAndInit() { - let roomName = APP.conference.roomName; - - if (config.configLocation) { - APP.configFetch.obtainConfig( - config.configLocation, roomName, - // Get config result callback - function(success, error) { - if (success) { - var now = APP.connectionTimes["configuration.fetched"] = - window.performance.now(); - logger.log("(TIME) configuration fetched:\t", now); - init(); - } else { - // Show obtain config error, - // pass the error object for report - APP.UI.messageHandler.openReportDialog( - null, "dialog.connectError", error); - } - }); - } else { - require("./modules/config/BoshAddressChoice").chooseAddress( - config, roomName); - - init(); - } -} - - -$(document).ready(function () { - var now = APP.connectionTimes["document.ready"] = window.performance.now(); - logger.log("(TIME) document ready:\t", now); - - URLProcessor.setConfigParametersFromUrl(); - - APP.init(); - - APP.translation.init(settings.getLanguage()); - - APP.API.init(APP.tokenData.externalAPISettings); - - obtainConfigAndInit(); -}); - -$(window).bind('beforeunload', function () { - // Stop the LogCollector - if (APP.logCollectorStarted) { - APP.logCollector.stop(); - APP.logCollectorStarted = false; - } - APP.API.dispose(); -}); +// TODO The execution of the mobile app starts from react/index.native.js. +// Similarly, the execution of the Web app should start from react/index.web.js +// for the sake of consistency and ease of understanding. Temporarily though +// because we are at the beginning of introducing React into the Web app, allow +// the execution of the Web app to start from app.js in order to reduce the +// complexity of the beginning step. +require('./react'); module.exports = APP; diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 10a502cc2..75752268f 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1,4 +1,5 @@ /* global APP, JitsiMeetJS, $, config, interfaceConfig, toastr */ + const logger = require("jitsi-meet-logger").getLogger(__filename); var UI = {}; @@ -396,20 +397,6 @@ UI.getSharedVideoManager = function () { */ UI.start = function () { document.title = interfaceConfig.APP_NAME; - var setupWelcomePage = null; - if(config.enableWelcomePage && window.location.pathname == "/" && - Settings.isWelcomePageEnabled()) { - $("#videoconference_page").hide(); - if (!setupWelcomePage) - setupWelcomePage = require("./welcome_page/WelcomePage"); - setupWelcomePage(); - - // Return false to indicate that the UI hasn't been fully started and - // conference ready. We're still waiting for input from the user. - return false; - } - - $("#welcome_page").hide(); // Set the defaults for prompt dialogs. $.prompt.setDefaults({persistent: false}); diff --git a/modules/UI/welcome_page/WelcomePage.js b/modules/UI/welcome_page/WelcomePage.js deleted file mode 100644 index 6777ff23a..000000000 --- a/modules/UI/welcome_page/WelcomePage.js +++ /dev/null @@ -1,30 +0,0 @@ -/* global $ */ - -function enterRoom() { - const $enterRoomField = $("#enter_room_field"); - - var val = $enterRoomField.val(); - if(!val) { - val = $enterRoomField.data("room-name"); - } - if (val) { - window.location.pathname = "/" + val; - } -} - -function setupWelcomePage() { - // XXX: We left only going to conference page here because transitions via - // React Router isn't implemented yet. - - $("#enter_room_button").click(function() { - enterRoom(); - }); - - $("#enter_room_field").keydown(function (event) { - if (event.keyCode === 13 /* enter */) { - enterRoom(); - } - }); -} - -module.exports = setupWelcomePage; diff --git a/package.json b/package.json index 48a1d9be5..e2024d222 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ "jws": "*", "lib-jitsi-meet": "jitsi/lib-jitsi-meet", "postis": "^2.2.0", - "react": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-dom": "15.4.2", "react-native": "0.39.2", "react-native-prompt": "^1.0.0", "react-native-vector-icons": "^3.0.0", "react-native-webrtc": "jitsi/react-native-webrtc", - "react-redux": "^5.0.1", + "react-redux": "^5.0.2", "react-router": "^3.0.0", "react-router-redux": "^4.0.7", "redux": "^3.5.2", @@ -60,7 +60,7 @@ "babel-preset-stage-1": "^6.16.0", "clean-css": "*", "css-loader": "*", - "eslint": "^3.12.2", + "eslint": "^3.13.1", "eslint-plugin-jsdoc": "*", "eslint-plugin-react": "*", "eslint-plugin-react-native": "^2.2.1", diff --git a/react/features/app/actions.js b/react/features/app/actions.js index ef0566ddd..efbbe6cad 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -1,23 +1,26 @@ import { setRoom } from '../base/conference'; -import { - getDomain, - setDomain -} from '../base/connection'; -import { - loadConfig, - setConfig -} from '../base/lib-jitsi-meet'; +import { getDomain, setDomain } from '../base/connection'; +import { loadConfig, setConfig } from '../base/lib-jitsi-meet'; -import { - APP_WILL_MOUNT, - APP_WILL_UNMOUNT -} from './actionTypes'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; import { _getRoomAndDomainFromUrlString, - _getRouteToRender + _getRouteToRender, + init } from './functions'; import './reducer'; +/** + * 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(); +} + /** * Triggers an in-app navigation to a different route. Allows navigation to be * abstracted between the mobile and web versions. diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index 7afd2e46a..942c4884a 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -1,15 +1,12 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { - browserHistory, - Route, - Router -} from 'react-router'; +import { browserHistory, Route, Router } from 'react-router'; import { push, syncHistoryWithStore } from 'react-router-redux'; import { getDomain } from '../../base/connection'; import { RouteRegistry } from '../../base/navigator'; +import { appInit } from '../actions'; import { AbstractApp } from './AbstractApp'; /** @@ -42,30 +39,18 @@ export class App extends AbstractApp { this.history = syncHistoryWithStore(browserHistory, props.store); // Bind event handlers so they are only bound once for every instance. - this._onRouteEnter = this._onRouteEnter.bind(this); this._routerCreateElement = this._routerCreateElement.bind(this); } /** - * Temporarily, prevents the super from dispatching Redux actions until they - * are integrated into the Web App. + * Inits the app before component will mount. * - * @returns {void} + * @inheritdoc */ - componentWillMount() { - // FIXME Do not override the super once the dispatching of Redux actions - // is integrated into the Web App. - } + componentWillMount(...args) { + super.componentWillMount(...args); - /** - * Temporarily, prevents the super from dispatching Redux actions until they - * are integrated into the Web App. - * - * @returns {void} - */ - componentWillUnmount() { - // FIXME Do not override the super once the dispatching of Redux actions - // is integrated into the Web App. + this.props.store.dispatch(appInit()); } /** @@ -75,19 +60,14 @@ export class App extends AbstractApp { * @returns {ReactElement} */ render() { - const routes = RouteRegistry.getRoutes(); - return ( - { routes.map(r => - - ) } + { + this._renderRoutes() + } ); @@ -118,10 +98,18 @@ export class App extends AbstractApp { * Invoked by react-router to notify this App that a Route is about to be * rendered. * + * @param {Route} route - The Route that is about to be rendered. * @private * @returns {void} */ - _onRouteEnter() { + _onRouteEnter(route, ...args) { + // Notify the route that it is about to be entered. + const onEnter = route.onEnter; + + if (typeof onEnter === 'function') { + onEnter(...args); + } + // XXX The following is mandatory. Otherwise, moving back & forward // through the browser's history could leave this App on the Conference // page without a room name. @@ -140,6 +128,37 @@ export class App extends AbstractApp { this._openURL(url); } + /** + * Renders a specific Route (for the purposes of the Router of this App). + * + * @param {Object} route - The Route to render. + * @returns {ReactElement} + * @private + */ + _renderRoute(route) { + const onEnter = (...args) => { + this._onRouteEnter(route, ...args); + }; + + return ( + + ); + } + + /** + * Renders the Routes of the Router of this App. + * + * @returns {Array.} + * @private + */ + _renderRoutes() { + return RouteRegistry.getRoutes().map(this._renderRoute, this); + } + /** * Create a ReactElement from the specified component and props on behalf of * the associated Router. diff --git a/react/features/app/functions.js b/react/features/app/functions.native.js similarity index 96% rename from react/features/app/functions.js rename to react/features/app/functions.native.js index 1c5a6f230..fb67dd1e1 100644 --- a/react/features/app/functions.js +++ b/react/features/app/functions.native.js @@ -64,8 +64,8 @@ export function _getRoomAndDomainFromUrlString(url) { url = match[1] /* URL protocol */ - + '://enso.hipchat.me/' - + url.substring(regex.lastIndex); + + '://enso.hipchat.me/' + + url.substring(regex.lastIndex); /* eslint-enable no-param-reassign, prefer-template */ } diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js new file mode 100644 index 000000000..ec4bbee3a --- /dev/null +++ b/react/features/app/functions.web.js @@ -0,0 +1,84 @@ +/* global APP, JitsiMeetJS, loggingConfig */ + +import URLProcessor from '../../../modules/config/URLProcessor'; +import KeyboardShortcut + from '../../../modules/keyboardshortcut/keyboardshortcut'; +import settings from '../../../modules/settings/Settings'; +import getTokenData from '../../../modules/tokendata/TokenData'; +import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage'; + +const Logger = require('jitsi-meet-logger'); + +export * from './functions.native'; + +/** + * Temporary solution. Later we'll get rid of global APP and set its properties + * in redux store. + * + * @returns {void} + */ +export function init() { + URLProcessor.setConfigParametersFromUrl(); + _initLogging(); + + APP.keyboardshortcut = KeyboardShortcut; + APP.tokenData = getTokenData(); + APP.API.init(APP.tokenData.externalAPISettings); + + APP.translation.init(settings.getLanguage()); +} + +/** + * 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); + } + }); + } +} + +/** + * 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 Logger.LogCollector(new JitsiMeetLogStorage()); + Logger.addGlobalTransport(APP.logCollector); + JitsiMeetJS.addGlobalLogTransport(APP.logCollector); + } +} diff --git a/react/features/base/connection/actions.js b/react/features/base/connection/actions.native.js similarity index 100% rename from react/features/base/connection/actions.js rename to react/features/base/connection/actions.native.js diff --git a/react/features/base/connection/actions.web.js b/react/features/base/connection/actions.web.js new file mode 100644 index 000000000..e8a65f638 --- /dev/null +++ b/react/features/base/connection/actions.web.js @@ -0,0 +1,94 @@ +/* global APP, JitsiMeetJS */ + +import UIEvents from '../../../../service/UI/UIEvents'; + +import { SET_DOMAIN } from './actionTypes'; +import './reducer'; + +const JitsiConferenceEvents = JitsiMeetJS.events.conference; +const logger = require('jitsi-meet-logger').getLogger(__filename); + +/** + * Opens new connection. + * + * @returns {Promise} + */ +export function connect() { + return (dispatch, getState) => { + const state = getState(); + const room = state['features/base/conference'].room; + + // XXX For web based version we use conference initialization logic + // from the old app (at the moment of writing). + return APP.conference.init({ roomName: room }).then(() => { + if (APP.logCollector) { + // Start the LogCollector's periodic "store logs" task + APP.logCollector.start(); + APP.logCollectorStarted = true; + + // Make an attempt to flush in case a lot of logs have been + // cached, before the collector was started. + APP.logCollector.flush(); + + // This event listener will flush the logs, before + // the statistics module (CallStats) is stopped. + // + // NOTE The LogCollector is not stopped, because this event can + // be triggered multiple times during single conference + // (whenever statistics module is stopped). That includes + // the case when Jicofo terminates the single person left in the + // room. It will then restart the media session when someone + // eventually join the room which will start the stats again. + APP.conference.addConferenceListener( + JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED, + () => { + if (APP.logCollector) { + APP.logCollector.flush(); + } + } + ); + } + + APP.UI.initConference(); + + APP.UI.addListener(UIEvents.LANG_CHANGED, language => { + APP.translation.setLanguage(language); + APP.settings.setLanguage(language); + }); + + APP.keyboardshortcut.init(); + }) + .catch(err => { + APP.UI.hideRingOverLay(); + APP.API.notifyConferenceLeft(APP.conference.roomName); + logger.error(err); + }); + }; +} + +/** + * Closes connection. + * + * @returns {Function} + */ +export function disconnect() { + // XXX For web based version we use conference hanging up logic from the old + // app. + return () => APP.conference.hangup(); +} + +/** + * Sets connection domain. + * + * @param {string} domain - Domain name. + * @returns {{ + * type: SET_DOMAIN, + * domain: string + * }} + */ +export function setDomain(domain) { + return { + type: SET_DOMAIN, + domain + }; +} diff --git a/react/features/base/lib-jitsi-meet/actions.js b/react/features/base/lib-jitsi-meet/actions.js index aa97ae36d..ef9bb8105 100644 --- a/react/features/base/lib-jitsi-meet/actions.js +++ b/react/features/base/lib-jitsi-meet/actions.js @@ -40,6 +40,12 @@ export function initLib() { throw new Error('Cannot initialize lib-jitsi-meet without config'); } + // XXX Temporarily until conference.js is moved to the React app we + // shouldn't use JitsiMeetJS from the React app. + if (typeof APP !== 'undefined') { + return Promise.resolve(); + } + return JitsiMeetJS.init(config) .then(() => dispatch({ type: LIB_INITIALIZED })) .catch(error => { diff --git a/react/features/base/lib-jitsi-meet/functions.js b/react/features/base/lib-jitsi-meet/functions.js index 27438b603..03ab8910e 100644 --- a/react/features/base/lib-jitsi-meet/functions.js +++ b/react/features/base/lib-jitsi-meet/functions.js @@ -8,6 +8,13 @@ import { loadScript } from '../../base/util'; * @returns {Promise} */ export function loadConfig(host, path = '/config.js') { + // Returns config.js file from global scope. We can't use the version that's + // being used for the React Native app because the old/current Web app uses + // config from the global scope. + if (typeof APP !== 'undefined') { + return Promise.resolve(window.config); + } + return loadScript(new URL(path, host).toString()) .then(() => { const config = window.config; diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 9acfa8c93..93bd85606 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -1,5 +1,9 @@ -/* global interfaceConfig, APP */ +/* global $, APP, interfaceConfig */ + import React, { Component } from 'react'; +import { connect as reactReduxConnect } from 'react-redux'; + +import { connect, disconnect } from '../../base/connection'; /** * For legacy reasons, inline style for display none. @@ -12,7 +16,42 @@ const DISPLAY_NONE_STYLE = { /** * Implements a React Component which renders initial conference layout */ -export default class Conference extends Component { +class Conference extends Component { + + /** + * Conference component's property types. + * + * @static + */ + static propTypes = { + dispatch: React.PropTypes.func + } + + /** + * Until we don't rewrite UI using react components + * we use UI.start from old app. Also method translates + * component right after it has been mounted. + * + * @inheritdoc + */ + componentDidMount() { + APP.UI.start(); + + // XXX Temporary solution until we add React translation. + APP.translation.translateElement($('#videoconference_page')); + + this.props.dispatch(connect()); + } + + /** + * Disconnect from the conference when component will be + * unmounted. + * + * @inheritdoc + */ + componentWillUnmount() { + this.props.dispatch(disconnect()); + } /** * Initializes Conference component instance. @@ -220,3 +259,5 @@ export default class Conference extends Component { return null; } } + +export default reactReduxConnect()(Conference); diff --git a/react/features/conference/route.js b/react/features/conference/route.js index 9efac31b9..5cfed1311 100644 --- a/react/features/conference/route.js +++ b/react/features/conference/route.js @@ -1,11 +1,126 @@ +/* global APP, config */ + +import BoshAddressChoice from '../../../modules/config/BoshAddressChoice'; +import HttpConfigFetch from '../../../modules/config/HttpConfigFetch'; +import ConferenceUrl from '../../../modules/URL/ConferenceUrl'; + import { RouteRegistry } from '../base/navigator'; import { Conference } from './components'; +const logger = require('jitsi-meet-logger').getLogger(__filename); + /** * Register route for Conference (page). */ RouteRegistry.register({ component: Conference, + onEnter: () => { + // XXX If config or jwt are set by hash or query parameters + // Getting raw URL before stripping it. + _obtainConfigAndInit(); + }, path: '/:room' }); + +/** + * Initialization of the app. + * + * @private + * @returns {void} + */ +function _initConference() { + _setTokenData(); + + // Initialize the conference URL handler + APP.ConferenceUrl = new ConferenceUrl(window.location); +} + +/** + * Promise wrapper on obtain config method. When HttpConfigFetch will be moved + * to React app it's better to use load config instead. + * + * @param {string} location - URL of the domain. + * @param {string} room - Room name. + * @private + * @returns {Promise} + */ +function _obtainConfig(location, room) { + return new Promise((resolve, reject) => { + HttpConfigFetch.obtainConfig(location, room, (success, error) => { + if (success) { + resolve(); + } else { + reject(error); + } + }); + }); +} + +/** + * If we have an HTTP endpoint for getting config.json configured we're going to + * read it and override properties from config.js and interfaceConfig.js. If + * there is no endpoint we'll just continue with initialization. Keep in mind + * that if the endpoint has been configured and we fail to obtain the config for + * any reason then the conference won't start and error message will be + * displayed to the user. + * + * @private + * @returns {void} + */ +function _obtainConfigAndInit() { + // Skip initialization if conference is initialized already. + if (typeof APP !== 'undefined' && !APP.ConferenceUrl) { + const location = config.configLocation; + const room = APP.conference.roomName; + + if (location) { + _obtainConfig(location, room) + .then(() => { + _obtainConfigHandler(); + _initConference(); + }) + .catch(err => { + // Show obtain config error. + APP.UI.messageHandler.openReportDialog( + null, 'dialog.connectError', err); + }); + } else { + BoshAddressChoice.chooseAddress(config, room); + _initConference(); + } + } +} + +/** + * Obtain config handler. + * + * @private + * @returns {Promise} + */ +function _obtainConfigHandler() { + const now = window.performance.now(); + + APP.connectionTimes['configuration.fetched'] = now; + logger.log('(TIME) configuration fetched:\t', now); +} + +/** + * If JWT token data it will be used for local user settings. + * + * @private + * @returns {void} + */ +function _setTokenData() { + const localUser = APP.tokenData.caller; + + if (localUser) { + const email = localUser.getEmail(); + const avatarUrl = localUser.getAvatarUrl(); + const name = localUser.getName(); + + APP.settings.setEmail((email || '').trim(), true); + APP.settings.setAvatarUrl((avatarUrl || '').trim()); + APP.settings.setDisplayName((name || '').trim(), true); + } +} diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 4eaa2daac..9b287c8db 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -1,10 +1,8 @@ -/* global APP, interfaceConfig */ +/* global $, APP, interfaceConfig */ import React from 'react'; import { connect } from 'react-redux'; -import { Conference } from '../../conference'; - import { AbstractWelcomePage, mapStateToProps } from './AbstractWelcomePage'; /** @@ -52,6 +50,9 @@ class WelcomePage extends AbstractWelcomePage { if (this.state.generateRoomnames) { this._updateRoomname(); } + + // XXX Temporary solution until we add React translation. + APP.translation.translateElement($('#welcome_page')); } /** @@ -61,12 +62,6 @@ class WelcomePage extends AbstractWelcomePage { * @returns {ReactElement|null} */ render() { - // FIXME The rendering of Conference bellow is a very quick and dirty - // temporary fix for the following issue: when the WelcomePage is - // disabled, app.js expects Conference to be rendered already and only - // then it builds a room name but the App component expects the room - // name to be built already (by looking at the window's location) in - // order to choose between WelcomePage and Conference. return (
@@ -77,7 +72,6 @@ class WelcomePage extends AbstractWelcomePage { this._renderMain() }
-
); } @@ -132,19 +126,6 @@ class WelcomePage extends AbstractWelcomePage { }); } - /** - * Overrides the super in order to prevent the dispatching of the Redux - * action SET_ROOM. - * - * @override - * @protected - * @returns {null} - */ - _onJoin() { - // Don't call the super implementation and thus prevent the dispatching - // of the Redux action SET_ROOM. - } - /** * Handles 'keydown' event to initiate joining the room when the * 'Enter/Return' button is pressed. diff --git a/react/features/welcome/route.js b/react/features/welcome/route.js index c5b2d7f29..3e0922448 100644 --- a/react/features/welcome/route.js +++ b/react/features/welcome/route.js @@ -1,4 +1,7 @@ +/* global APP */ + import { RouteRegistry } from '../base/navigator'; +import { generateRoomWithoutSeparator } from '../base/util'; import { WelcomePage } from './components'; @@ -7,5 +10,23 @@ import { WelcomePage } from './components'; */ RouteRegistry.register({ component: WelcomePage, + onEnter, path: '/' }); + +/** + * If the Welcome page/screen is disabled, generates a (random) room (name) so + * that the Welcome page/screen is skipped and the Conference page/screen is + * presented instead. + * + * @param {Object} nextState - The next Router state. + * @param {Function} replace - The function to redirect to another path. + * @returns {void} + */ +function onEnter(nextState, replace) { + if (typeof APP !== 'undefined' && !APP.settings.isWelcomePageEnabled()) { + const room = generateRoomWithoutSeparator(); + + replace(`/${room}`); + } +} diff --git a/react/index.native.js b/react/index.native.js index 7df47f219..1f983045d 100644 --- a/react/index.native.js +++ b/react/index.native.js @@ -61,7 +61,9 @@ class Root extends Component { // XXX Start with an empty URL if getting the initial URL fails; // otherwise, nothing will be rendered. - this.setState({ url: null }); + if (this.state.url !== null) { + this.setState({ url: null }); + } }); } diff --git a/react/index.web.js b/react/index.web.js index 963b02b5a..4a18ed22e 100644 --- a/react/index.web.js +++ b/react/index.web.js @@ -1,19 +1,17 @@ +/* global APP */ + import React from 'react'; import ReactDOM from 'react-dom'; import { browserHistory } from 'react-router'; -import { - routerMiddleware, - routerReducer -} from 'react-router-redux'; +import { routerMiddleware, routerReducer } from 'react-router-redux'; import { compose, createStore } from 'redux'; import Thunk from 'redux-thunk'; import config from './config'; import { App } from './features/app'; -import { - MiddlewareRegistry, - ReducerRegistry -} from './features/base/redux'; +import { MiddlewareRegistry, ReducerRegistry } from './features/base/redux'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); // Create combined reducer from all reducers in registry + routerReducer from // 'react-router-redux' module (stores location updates from history). @@ -45,10 +43,33 @@ if (typeof window === 'object' // Create Redux store with our reducer and middleware. const store = createStore(reducer, middleware); -// Render the main Component. -ReactDOM.render( - , - document.getElementById('react')); +/** + * Renders the app when the DOM tree has been loaded. + */ +document.addEventListener('DOMContentLoaded', () => { + const now = window.performance.now(); + + APP.connectionTimes['document.ready'] = now; + logger.log('(TIME) document ready:\t', now); + + // Render the main Component. + ReactDOM.render( + , + document.getElementById('react')); +}); + +/** + * Stops collecting the logs and disposing the API when the user closes the + * page. + */ +window.addEventListener('beforeunload', () => { + // Stop the LogCollector + if (APP.logCollectorStarted) { + APP.logCollector.stop(); + APP.logCollectorStarted = false; + } + APP.API.dispose(); +});