From dc246960dfead5afd2d29d5d1732f04e0301fe4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 11 Jul 2018 11:42:43 +0200 Subject: [PATCH] feat(App): refactor App and split it into BaseApp and App BaseApp does all the heavy-lifting related to creating the redux store, navigation, and so on. App currently handles URL props and actually triggering navigation based on them. --- react/features/app/actions.js | 44 --- react/features/app/components/AbstractApp.js | 304 +++--------------- react/features/app/components/App.native.js | 73 +++-- react/features/app/components/App.web.js | 13 +- react/features/app/functions.any.js | 28 +- react/features/app/index.js | 2 - react/features/app/middleware.js | 2 +- react/features/{ => base}/app/actionTypes.js | 0 react/features/base/app/actions.js | 48 +++ react/features/base/app/components/BaseApp.js | 255 +++++++++++++++ react/features/base/app/components/index.js | 1 + react/features/base/app/functions.js | 28 ++ react/features/base/app/index.js | 6 + react/features/{ => base}/app/reducer.js | 10 +- react/features/base/config/middleware.js | 2 +- .../features/base/known-domains/middleware.js | 3 +- react/features/base/known-domains/reducer.js | 2 +- react/features/base/logging/middleware.js | 2 +- .../features/base/participants/middleware.js | 2 +- react/features/base/settings/reducer.js | 3 +- react/features/calendar-sync/middleware.js | 2 +- react/features/calendar-sync/reducer.js | 2 +- react/features/chat/middleware.js | 2 +- react/features/invite/functions.js | 2 +- react/features/invite/middleware.any.js | 2 +- react/features/invite/middleware.native.js | 2 +- .../features/mobile/audio-mode/middleware.js | 2 +- .../features/mobile/background/middleware.js | 2 +- react/features/mobile/callkit/middleware.js | 8 +- .../mobile/external-api/middleware.js | 2 +- .../features/mobile/full-screen/middleware.js | 2 +- .../features/mobile/image-cache/middleware.js | 2 +- .../mobile/network-activity/middleware.js | 2 +- .../mobile/picture-in-picture/actions.js | 2 +- .../components/PictureInPictureButton.js | 2 +- react/features/recent-list/middleware.js | 2 +- react/features/recent-list/reducer.js | 2 +- react/features/recording/middleware.js | 3 +- react/features/welcome/functions.js | 2 +- 39 files changed, 461 insertions(+), 412 deletions(-) rename react/features/{ => base}/app/actionTypes.js (100%) create mode 100644 react/features/base/app/actions.js create mode 100644 react/features/base/app/components/BaseApp.js create mode 100644 react/features/base/app/components/index.js create mode 100644 react/features/base/app/functions.js create mode 100644 react/features/base/app/index.js rename react/features/{ => base}/app/reducer.js (67%) diff --git a/react/features/app/actions.js b/react/features/app/actions.js index f7ca8b0605..2078bc1152 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -15,7 +15,6 @@ import { loadConfig } from '../base/lib-jitsi-meet'; import { parseURIString, toURLString } from '../base/util'; import { setFatalError } from '../overlay'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; import { getDefaultURL } from './functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -138,49 +137,6 @@ function _appNavigateToOptionalLocation( return _appNavigateToMandatoryLocation(dispatch, getState, location); } -/** - * 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: Object) { - return (dispatch: Dispatch<*>) => { - dispatch({ - type: APP_WILL_MOUNT, - app - }); - - // TODO There was a redux action creator appInit which I did not like - // because we already had the redux action creator appWillMount and, - // respectively, the redux action APP_WILL_MOUNT. So I set out to remove - // appInit and managed to move everything it was doing but the - // following. Which is not extremely bad because we haven't moved the - // API module into its own feature yet so we're bound to work on that in - // the future. - typeof APP === 'object' && APP.API.init(); - }; -} - -/** - * 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: Object) { - return { - type: APP_WILL_UNMOUNT, - app - }; -} - /** * Loads config.js from a specific host. * diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index 55e695ad57..e9813ae49a 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -1,107 +1,44 @@ -/* global APP */ +// @flow -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; -import { compose, createStore } from 'redux'; -import Thunk from 'redux-thunk'; +import React, { Fragment } from 'react'; -import { i18next } from '../../base/i18n'; -import { - MiddlewareRegistry, - ReducerRegistry, - StateListenerRegistry -} from '../../base/redux'; -import { SoundCollection } from '../../base/sounds'; -import { PersistenceRegistry } from '../../base/storage'; +import { BaseApp } from '../../base/app'; import { toURLString } from '../../base/util'; import { OverlayContainer } from '../../overlay'; -import { appNavigate, appWillMount, appWillUnmount } from '../actions'; +import { appNavigate } from '../actions'; import { getDefaultURL } from '../functions'; +/** + * {@code AbstractApp} component's property types. + */ +export type Props = { + + /** + * The default URL {@code AbstractApp} is to open when not in any + * conference/room. + */ + defaultURL: string, + + /** + * XXX Refer to the implementation of loadURLObject: in + * ios/sdk/src/JitsiMeetView.m for further information. + */ + timestamp: any, + + /** + * The URL, if any, with which the app was launched. + */ + url: Object | string +}; + /** * Base (abstract) class for main App component. * * @abstract */ -export class AbstractApp extends Component { - /** - * {@code AbstractApp} component's property types. - * - * @static - */ - static propTypes = { - /** - * The default URL {@code AbstractApp} is to open when not in any - * conference/room. - */ - defaultURL: PropTypes.string, - - // XXX Refer to the implementation of loadURLObject: in - // ios/sdk/src/JitsiMeetView.m for further information. - timestamp: PropTypes.any, - - /** - * The URL, if any, with which the app was launched. - */ - url: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.string - ]) - }; - - /** - * Initializes a new {@code AbstractApp} instance. - * - * @param {Object} props - The read-only React {@code Component} props with - * which the new instance is to be initialized. - */ - constructor(props) { - super(props); - - this.state = { - - /** - * The state of the »possible« async initialization of the - * {@code AbstractApp}. - */ - appAsyncInitialized: false, - - /** - * The Route rendered by this {@code AbstractApp}. - * - * @type {Route} - */ - route: {}, - - /** - * The redux store used by this {@code AbstractApp}. - * - * @type {Store} - */ - store: undefined - }; - - /** - * Make the mobile {@code AbstractApp} wait until the - * {@code AsyncStorage} implementation of {@code Storage} initializes - * fully. - * - * @private - * @see {@link #_initStorage} - * @type {Promise} - */ - this._init - = this._initStorage() - .catch(() => { /* AbstractApp should always initialize! */ }) - .then(() => - this.setState({ - store: this._createStore() - })); - } +export class AbstractApp extends BaseApp { + _init: Promise<*>; /** * Initializes the app. @@ -109,20 +46,11 @@ export class AbstractApp extends Component { * @inheritdoc */ componentWillMount() { + super.componentWillMount(); + this._init.then(() => { - const { dispatch } = this.state.store; - - dispatch(appWillMount(this)); - - // We set the initialized state here and not in the constructor to - // make sure that {@code componentWillMount} gets invoked before the - // app tries to render the actual app content. - this.setState({ - appAsyncInitialized: true - }); - - // If a URL was explicitly specified to this React Component, then - // open it; otherwise, use a default. + // If a URL was explicitly specified to this React Component, + // then open it; otherwise, use a default. this._openURL(toURLString(this.props.url) || this._getDefaultURL()); }); } @@ -136,7 +64,7 @@ export class AbstractApp extends Component { * that this instance will receive. * @returns {void} */ - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { props } = this; this._init.then(() => { @@ -154,15 +82,6 @@ export class AbstractApp extends Component { }); } - /** - * De-initializes the app. - * - * @inheritdoc - */ - componentWillUnmount() { - this.state.store.dispatch(appWillUnmount(this)); - } - /** * Gets a {@code Location} object from the window with information about the * current location of the document. Explicitly defined to allow extenders @@ -180,131 +99,22 @@ export class AbstractApp extends Component { } /** - * Delays this {@code AbstractApp}'s startup until the {@code Storage} - * implementation of {@code localStorage} initializes. While the - * initialization is instantaneous on Web (with Web Storage API), it is - * asynchronous on mobile/react-native. + * Creates an extra {@link ReactElement}s to be added (unconditionaly) + * alongside the main element. * - * @private - * @returns {Promise} - */ - _initStorage() { - const localStorageInitializing = window.localStorage._initializing; - - return ( - typeof localStorageInitializing === 'undefined' - ? Promise.resolve() - : localStorageInitializing); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { appAsyncInitialized, route, store } = this.state; - const { component } = route; - - if (appAsyncInitialized && component) { - return ( - - - - { this._createElement(component) } - - - - - - ); - } - - return null; - } - - /** - * Creates a {@link ReactElement} from the specified component, the - * specified props and the props of this {@code AbstractApp} which are - * suitable for propagation to the children of this {@code Component}. - * - * @param {Component} component - The component from which the - * {@code ReactElement} is to be created. - * @param {Object} props - The read-only React {@code Component} props with - * which the {@code ReactElement} is to be initialized. * @returns {ReactElement} + * @abstract * @protected */ - _createElement(component, props) { - /* eslint-disable no-unused-vars */ - - const { - // The following props were introduced to be consumed entirely by - // AbstractApp: - defaultURL, - timestamp, - url, - - // The remaining props, if any, are considered suitable for - // propagation to the children of this Component. - ...thisProps - } = this.props; - - /* eslint-enable no-unused-vars */ - - return React.createElement(component, { - ...thisProps, - ...props - }); + _createExtraElement() { + return ( + + + + ); } - /** - * Initializes a new redux store instance suitable for use by this - * {@code AbstractApp}. - * - * @private - * @returns {Store} - A new redux store instance suitable for use by this - * {@code AbstractApp}. - */ - _createStore() { - // Create combined reducer from all reducers in ReducerRegistry. - const reducer = ReducerRegistry.combineReducers(); - - // Apply all registered middleware from the MiddlewareRegistry and - // additional 3rd party middleware: - // - Thunk - allows us to dispatch async actions easily. For more info - // @see https://github.com/gaearon/redux-thunk. - let middleware = MiddlewareRegistry.applyMiddleware(Thunk); - - // Try to enable Redux DevTools Chrome extension in order to make it - // available for the purposes of facilitating development. - let devToolsExtension; - - if (typeof window === 'object' - && (devToolsExtension = window.devToolsExtension)) { - middleware = compose(middleware, devToolsExtension()); - } - - const store - = createStore( - reducer, - PersistenceRegistry.getPersistedState(), - middleware); - - // StateListenerRegistry - StateListenerRegistry.subscribe(store); - - // This is temporary workaround to be able to dispatch actions from - // non-reactified parts of the code (conference.js for example). - // Don't use in the react code!!! - // FIXME: remove when the reactification is finished! - if (typeof APP !== 'undefined') { - APP.store = store; - } - - return store; - } + _createMainElement: (React$Element<*>, Object) => ?React$Element<*>; /** * Gets the default URL to be opened when this {@code App} mounts. @@ -317,32 +127,6 @@ export class AbstractApp extends Component { return getDefaultURL(this.state.store); } - /** - * Navigates to a specific Route. - * - * @param {Route} route - The Route to which to navigate. - * @returns {Promise} - */ - _navigate(route) { - if (_.isEqual(route, this.state.route)) { - return Promise.resolve(); - } - - if (route.href) { - // This navigation requires loading a new URL in the browser. - window.location.href = route.href; - - return Promise.resolve(); - } - - // XXX React's setState is asynchronous which means that the value of - // this.state.route above may not even be correct. If the check is - // performed before setState completes, the app may not navigate to the - // expected route. In order to mitigate the problem, _navigate was - // changed to return a Promise. - return new Promise(resolve => this.setState({ route }, resolve)); - } - /** * Navigates this {@code AbstractApp} to (i.e. opens) a specific URL. * diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index 872cccdccf..93a666c28d 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -1,6 +1,5 @@ -/* global __DEV__ */ +// @flow -import PropTypes from 'prop-types'; import React from 'react'; import { Linking } from 'react-native'; @@ -23,6 +22,39 @@ import '../../mobile/proximity'; import '../../mobile/wake-lock'; import { AbstractApp } from './AbstractApp'; +import type { Props as AbstractAppProps } from './AbstractApp'; + +declare var __DEV__; + +/** + * App component's property types. + */ +type Props = AbstractAppProps & { + + /** + * Whether the add people feature is enabled or not. + */ + addPeopleEnabled: boolean, + + /** + * Whether the dial-out feature is enabled or not. + */ + dialOutEnabled: boolean, + + /** + * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar + * button is rendered in the {@link Conference} view to afford entering + * Picture-in-Picture. + */ + pictureInPictureEnabled: boolean, + + /** + * Whether the Welcome page is enabled. If {@code true}, the Welcome + * page is rendered when the {@link App} is not at a location (URL) + * identifying a Jitsi Meet conference/room. + */ + welcomePageEnabled: boolean +}; /** * Root application component. @@ -30,40 +62,13 @@ import { AbstractApp } from './AbstractApp'; * @extends AbstractApp */ export class App extends AbstractApp { - /** - * App component's property types. - * - * @static - */ - static propTypes = { - ...AbstractApp.propTypes, - - addPeopleEnabled: PropTypes.bool, - - dialOutEnabled: PropTypes.bool, - - /** - * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar - * button is rendered in the {@link Conference} view to afford entering - * Picture-in-Picture. - */ - pictureInPictureEnabled: PropTypes.bool, - - /** - * Whether the Welcome page is enabled. If {@code true}, the Welcome - * page is rendered when the {@link App} is not at a location (URL) - * identifying a Jitsi Meet conference/room. - */ - welcomePageEnabled: PropTypes.bool - }; - /** * Initializes a new App instance. * * @param {Object} props - The read-only React Component props with which * the new instance is to be initialized. */ - constructor(props) { + constructor(props: Props) { super(props); // Bind event handlers so they are only bound once for every instance. @@ -110,11 +115,11 @@ export class App extends AbstractApp { * * @override */ - _createElement(component, props) { + _createMainElement(component, props) { return ( - { super._createElement(component, props) } + { super._createMainElement(component, props) } ); @@ -158,6 +163,8 @@ export class App extends AbstractApp { } } + _onLinkingURL: (*) => void; + /** * Notified by React's Linking API that a specific URL registered to be * handled by this App was activated. @@ -169,7 +176,7 @@ export class App extends AbstractApp { * @returns {void} */ _onLinkingURL({ url }) { - this._openURL(url); + super._openURL(url); } } diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index e1e281cb1c..413608aa17 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -1,3 +1,5 @@ +// @flow + import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; @@ -14,23 +16,16 @@ import { AbstractApp } from './AbstractApp'; * @extends AbstractApp */ export class App extends AbstractApp { - /** - * App component's property types. - * - * @static - */ - static propTypes = AbstractApp.propTypes; - /** * Overrides the parent method to inject {@link AtlasKitThemeProvider} as * the top most component. * * @override */ - _createElement(component, props) { + _createMainElement(component, props) { return ( - { super._createElement(component, props) } + { super._createMainElement(component, props) } ); } diff --git a/react/features/app/functions.any.js b/react/features/app/functions.any.js index d1f03d3e6f..fbabebcde3 100644 --- a/react/features/app/functions.any.js +++ b/react/features/app/functions.any.js @@ -1,33 +1,9 @@ // @flow +import { getAppProp } from '../base/app'; import { toState } from '../base/redux'; import { getServerURL } from '../base/settings'; -/** - * Gets the value of a specific React {@code Component} prop of the currently - * mounted {@link App}. - * - * @param {Function|Object} stateful - The redux store or {@code getState} - * function. - * @param {string} propName - The name of the React {@code Component} prop of - * the currently mounted {@code App} to get. - * @returns {*} The value of the specified React {@code Compoennt} prop of the - * currently mounted {@code App}. - */ -export function getAppProp(stateful: Function | Object, propName: string) { - const state = toState(stateful)['features/app']; - - if (state) { - const { app } = state; - - if (app) { - return app.props[propName]; - } - } - - return undefined; -} - /** * Retrieves the default URL for the app. This can either come from a prop to * the root App component or be configured in the settings. @@ -38,7 +14,7 @@ export function getAppProp(stateful: Function | Object, propName: string) { */ export function getDefaultURL(stateful: Function | Object) { const state = toState(stateful); - const { app } = state['features/app']; + const { app } = state['features/base/app']; // If the execution environment provides a Location abstraction (e.g. a Web // browser), then we'll presume it's the one and only base URL it can be on. diff --git a/react/features/app/index.js b/react/features/app/index.js index 44000a1a7b..0f9be4294f 100644 --- a/react/features/app/index.js +++ b/react/features/app/index.js @@ -1,7 +1,5 @@ export * from './actions'; -export * from './actionTypes'; export * from './components'; export * from './functions'; import './middleware'; -import './reducer'; diff --git a/react/features/app/middleware.js b/react/features/app/middleware.js index 452df63525..b4fa5d0b52 100644 --- a/react/features/app/middleware.js +++ b/react/features/app/middleware.js @@ -72,7 +72,7 @@ function _connectionEstablished(store, next, action) { */ function _navigate({ getState }) { const state = getState(); - const { app } = state['features/app']; + const { app } = state['features/base/app']; _getRouteToRender(state).then(route => app._navigate(route)); } diff --git a/react/features/app/actionTypes.js b/react/features/base/app/actionTypes.js similarity index 100% rename from react/features/app/actionTypes.js rename to react/features/base/app/actionTypes.js diff --git a/react/features/base/app/actions.js b/react/features/base/app/actions.js new file mode 100644 index 0000000000..6cb05768cf --- /dev/null +++ b/react/features/base/app/actions.js @@ -0,0 +1,48 @@ +// @flow + +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; + +declare var APP; + +/** + * 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: Object) { + return (dispatch: Dispatch<*>) => { + dispatch({ + type: APP_WILL_MOUNT, + app + }); + + // TODO There was a redux action creator appInit which I did not like + // because we already had the redux action creator appWillMount and, + // respectively, the redux action APP_WILL_MOUNT. So I set out to remove + // appInit and managed to move everything it was doing but the + // following. Which is not extremely bad because we haven't moved the + // API module into its own feature yet so we're bound to work on that in + // the future. + typeof APP === 'object' && APP.API.init(); + }; +} + +/** + * 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: Object) { + return { + type: APP_WILL_UNMOUNT, + app + }; +} diff --git a/react/features/base/app/components/BaseApp.js b/react/features/base/app/components/BaseApp.js new file mode 100644 index 0000000000..9b89a375a5 --- /dev/null +++ b/react/features/base/app/components/BaseApp.js @@ -0,0 +1,255 @@ +// @flow + +import _ from 'lodash'; +import React, { Component, Fragment } from 'react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { compose, createStore } from 'redux'; +import Thunk from 'redux-thunk'; + +import { i18next } from '../../i18n'; +import { + MiddlewareRegistry, + ReducerRegistry, + StateListenerRegistry +} from '../../redux'; +import { SoundCollection } from '../../sounds'; +import { PersistenceRegistry } from '../../storage'; + +import { appWillMount, appWillUnmount } from '../actions'; + +declare var APP: Object; + +type State = { + + /** + * The state of the »possible« async initialization of + * the {@code BaseApp}. + */ + initialized: boolean, + + /** + * The Route rendered by this {@code BaseApp}. + */ + route: Object, + + /** + * The redux store used by this {@code BaseApp}. + */ + store: Object +}; + +/** + * Base (abstract) class for main App component. + * + * @abstract + */ +export default class BaseApp extends Component<*, State> { + _init: Promise<*>; + + /** + * Initializes a new {@code BaseApp} instance. + * + * @param {Object} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + this.state = { + initialized: false, + route: {}, + + // $FlowFixMe + store: undefined + }; + + /** + * Make the mobile {@code BaseApp} wait until the + * {@code AsyncStorage} implementation of {@code Storage} initializes + * fully. + * + * @private + * @see {@link #_initStorage} + * @type {Promise} + */ + this._init + = this._initStorage() + .catch(() => { /* AbstractApp should always initialize! */ }) + .then(() => + this.setState({ + store: this._createStore() + })); + } + + /** + * Initialize the application. + * + * @inheritdoc + */ + componentWillMount() { + this._init.then(() => { + const { dispatch } = this.state.store; + + dispatch(appWillMount(this)); + + // We set the initialized state here and not in the constructor to + // make sure that {@code componentWillMount} gets invoked before + // the app tries to render the actual app content. + this.setState({ initialized: true }); + }); + } + + /** + * De-initialize the application. + * + * @inheritdoc + */ + componentWillUnmount() { + const { dispatch } = this.state.store; + + dispatch(appWillUnmount(this)); + } + + /** + * Delays this {@code BaseApp}'s startup until the {@code Storage} + * implementation of {@code localStorage} initializes. While the + * initialization is instantaneous on Web (with Web Storage API), it is + * asynchronous on mobile/react-native. + * + * @private + * @returns {Promise} + */ + _initStorage(): Promise<*> { + const { _initializing } = window.localStorage; + + return _initializing || Promise.resolve(); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { initialized, route, store } = this.state; + const { component } = route; + + if (initialized && component) { + return ( + + + + { this._createMainElement(component) } + + { this._createExtraElement() } + + + + ); + } + + return null; + } + + /** + * Creates an extra {@link ReactElement}s to be added (unconditionaly) + * alongside the main element. + * + * @returns {ReactElement} + * @abstract + * @protected + */ + _createExtraElement() { + return null; + } + + /** + * Creates a {@link ReactElement} from the specified component, the + * specified props and the props of this {@code AbstractApp} which are + * suitable for propagation to the children of this {@code Component}. + * + * @param {Component} component - The component from which the + * {@code ReactElement} is to be created. + * @param {Object} props - The read-only React {@code Component} props with + * which the {@code ReactElement} is to be initialized. + * @returns {ReactElement} + * @protected + */ + _createMainElement(component, props) { + return React.createElement(component, props || {}); + } + + /** + * Initializes a new redux store instance suitable for use by this + * {@code AbstractApp}. + * + * @private + * @returns {Store} - A new redux store instance suitable for use by + * this {@code AbstractApp}. + */ + _createStore() { + // Create combined reducer from all reducers in ReducerRegistry. + const reducer = ReducerRegistry.combineReducers(); + + // Apply all registered middleware from the MiddlewareRegistry and + // additional 3rd party middleware: + // - Thunk - allows us to dispatch async actions easily. For more info + // @see https://github.com/gaearon/redux-thunk. + let middleware = MiddlewareRegistry.applyMiddleware(Thunk); + + // Try to enable Redux DevTools Chrome extension in order to make it + // available for the purposes of facilitating development. + let devToolsExtension; + + if (typeof window === 'object' + && (devToolsExtension = window.devToolsExtension)) { + middleware = compose(middleware, devToolsExtension()); + } + + const store = createStore( + reducer, PersistenceRegistry.getPersistedState(), middleware); + + // StateListenerRegistry + StateListenerRegistry.subscribe(store); + + // This is temporary workaround to be able to dispatch actions from + // non-reactified parts of the code (conference.js for example). + // Don't use in the react code!!! + // FIXME: remove when the reactification is finished! + if (typeof APP !== 'undefined') { + APP.store = store; + } + + return store; + } + + /** + * Navigates to a specific Route. + * + * @param {Route} route - The Route to which to navigate. + * @returns {Promise} + */ + _navigate(route): Promise<*> { + if (_.isEqual(route, this.state.route)) { + return Promise.resolve(); + } + + if (route.href) { + // This navigation requires loading a new URL in the browser. + window.location.href = route.href; + + return Promise.resolve(); + } + + // XXX React's setState is asynchronous which means that the value of + // this.state.route above may not even be correct. If the check is + // performed before setState completes, the app may not navigate to the + // expected route. In order to mitigate the problem, _navigate was + // changed to return a Promise. + return new Promise(resolve => { + this.setState({ route }, resolve); + }); + } +} diff --git a/react/features/base/app/components/index.js b/react/features/base/app/components/index.js new file mode 100644 index 0000000000..76921fb758 --- /dev/null +++ b/react/features/base/app/components/index.js @@ -0,0 +1 @@ +export { default as BaseApp } from './BaseApp'; diff --git a/react/features/base/app/functions.js b/react/features/base/app/functions.js new file mode 100644 index 0000000000..459c19c4a9 --- /dev/null +++ b/react/features/base/app/functions.js @@ -0,0 +1,28 @@ +// @flow + +import { toState } from '../redux'; + +/** + * Gets the value of a specific React {@code Component} prop of the currently + * mounted {@link App}. + * + * @param {Function|Object} stateful - The redux store or {@code getState} + * function. + * @param {string} propName - The name of the React {@code Component} prop of + * the currently mounted {@code App} to get. + * @returns {*} The value of the specified React {@code Compoennt} prop of the + * currently mounted {@code App}. + */ +export function getAppProp(stateful: Function | Object, propName: string) { + const state = toState(stateful)['features/base/app']; + + if (state) { + const { app } = state; + + if (app) { + return app.props[propName]; + } + } + + return undefined; +} diff --git a/react/features/base/app/index.js b/react/features/base/app/index.js new file mode 100644 index 0000000000..5d564613e6 --- /dev/null +++ b/react/features/base/app/index.js @@ -0,0 +1,6 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; +export * from './functions'; + +import './reducer'; diff --git a/react/features/app/reducer.js b/react/features/base/app/reducer.js similarity index 67% rename from react/features/app/reducer.js rename to react/features/base/app/reducer.js index 07cfce539c..2badd0c96c 100644 --- a/react/features/app/reducer.js +++ b/react/features/base/app/reducer.js @@ -1,10 +1,10 @@ // @flow -import { ReducerRegistry } from '../base/redux'; +import { ReducerRegistry } from '../redux'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; -ReducerRegistry.register('features/app', (state = {}, action) => { +ReducerRegistry.register('features/base/app', (state = {}, action) => { switch (action.type) { case APP_WILL_MOUNT: { const { app } = action; @@ -14,10 +14,10 @@ ReducerRegistry.register('features/app', (state = {}, action) => { ...state, /** - * The one and only (i.e. singleton) {@link App} instance which - * is currently mounted. + * The one and only (i.e. singleton) {@link BaseApp} instance + * which is currently mounted. * - * @type {App} + * @type {BaseApp} */ app }; diff --git a/react/features/base/config/middleware.js b/react/features/base/config/middleware.js index 7a8674a63a..610016e77c 100644 --- a/react/features/base/config/middleware.js +++ b/react/features/base/config/middleware.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT } from '../../app'; +import { APP_WILL_MOUNT } from '../app'; import { addKnownDomains } from '../known-domains'; import { MiddlewareRegistry } from '../redux'; import { parseURIString } from '../util'; diff --git a/react/features/base/known-domains/middleware.js b/react/features/base/known-domains/middleware.js index 3d06943924..dc9f98b4bc 100644 --- a/react/features/base/known-domains/middleware.js +++ b/react/features/base/known-domains/middleware.js @@ -1,7 +1,8 @@ // @flow -import { APP_WILL_MOUNT, getDefaultURL } from '../../app'; +import { getDefaultURL } from '../../app'; +import { APP_WILL_MOUNT } from '../app'; import { SET_ROOM } from '../conference'; import { MiddlewareRegistry } from '../redux'; import { parseURIString } from '../util'; diff --git a/react/features/base/known-domains/reducer.js b/react/features/base/known-domains/reducer.js index 544d157477..bdd0f687db 100644 --- a/react/features/base/known-domains/reducer.js +++ b/react/features/base/known-domains/reducer.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT } from '../../app'; +import { APP_WILL_MOUNT } from '../app'; import { ReducerRegistry } from '../redux'; import { PersistenceRegistry } from '../storage'; diff --git a/react/features/base/logging/middleware.js b/react/features/base/logging/middleware.js index 032817383b..0ffdccd10e 100644 --- a/react/features/base/logging/middleware.js +++ b/react/features/base/logging/middleware.js @@ -2,7 +2,7 @@ import Logger from 'jitsi-meet-logger'; -import { APP_WILL_MOUNT } from '../../app'; +import { APP_WILL_MOUNT } from '../app'; import JitsiMeetJS, { LIB_WILL_INIT } from '../lib-jitsi-meet'; import { MiddlewareRegistry } from '../redux'; diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index bfacded26d..51089aa6a8 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; import { CONFERENCE_WILL_JOIN, forEachConference, diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index 529a4b8aab..57d8af8f86 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -1,8 +1,7 @@ // @flow import _ from 'lodash'; -import { APP_WILL_MOUNT } from '../../app'; - +import { APP_WILL_MOUNT } from '../app'; import JitsiMeetJS, { browser } from '../lib-jitsi-meet'; import { ReducerRegistry } from '../redux'; import { PersistenceRegistry } from '../storage'; diff --git a/react/features/calendar-sync/middleware.js b/react/features/calendar-sync/middleware.js index 4091a30ebf..6fed0b8b30 100644 --- a/react/features/calendar-sync/middleware.js +++ b/react/features/calendar-sync/middleware.js @@ -3,7 +3,7 @@ import md5 from 'js-md5'; import RNCalendarEvents from 'react-native-calendar-events'; -import { APP_WILL_MOUNT } from '../app'; +import { APP_WILL_MOUNT } from '../base/app'; import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains'; import { MiddlewareRegistry } from '../base/redux'; import { APP_LINK_SCHEME, parseURIString } from '../base/util'; diff --git a/react/features/calendar-sync/reducer.js b/react/features/calendar-sync/reducer.js index b305bdb4fe..65f33a632d 100644 --- a/react/features/calendar-sync/reducer.js +++ b/react/features/calendar-sync/reducer.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT } from '../app'; +import { APP_WILL_MOUNT } from '../base/app'; import { ReducerRegistry, set } from '../base/redux'; import { PersistenceRegistry } from '../base/storage'; diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js index 45964ade91..2f1dbbc16d 100644 --- a/react/features/chat/middleware.js +++ b/react/features/chat/middleware.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { CONFERENCE_JOINED } from '../base/conference'; import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; import { MiddlewareRegistry } from '../base/redux'; diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index 9de54cdd18..516dcf820f 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -1,6 +1,6 @@ // @flow -import { getAppProp } from '../app'; +import { getAppProp } from '../base/app'; import { isLocalParticipantModerator } from '../base/participants'; import { doGetJSON } from '../base/util'; diff --git a/react/features/invite/middleware.any.js b/react/features/invite/middleware.any.js index a6c78b3899..bc39a0bca5 100644 --- a/react/features/invite/middleware.any.js +++ b/react/features/invite/middleware.any.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { CONFERENCE_JOINED } from '../base/conference'; diff --git a/react/features/invite/middleware.native.js b/react/features/invite/middleware.native.js index 9226cd6a6a..890e636ee7 100644 --- a/react/features/invite/middleware.native.js +++ b/react/features/invite/middleware.native.js @@ -3,8 +3,8 @@ import i18next from 'i18next'; import { NativeEventEmitter, NativeModules } from 'react-native'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, getAppProp } from '../base/app'; import { MiddlewareRegistry } from '../base/redux'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, getAppProp } from '../app'; import { invite } from './actions'; import { diff --git a/react/features/mobile/audio-mode/middleware.js b/react/features/mobile/audio-mode/middleware.js index 7a16644185..dc7c70ec93 100644 --- a/react/features/mobile/audio-mode/middleware.js +++ b/react/features/mobile/audio-mode/middleware.js @@ -2,7 +2,7 @@ import { NativeModules } from 'react-native'; -import { APP_WILL_MOUNT } from '../../app'; +import { APP_WILL_MOUNT } from '../../base/app'; import { CONFERENCE_FAILED, CONFERENCE_LEFT, diff --git a/react/features/mobile/background/middleware.js b/react/features/mobile/background/middleware.js index 17ec4aaae0..8d2328668e 100644 --- a/react/features/mobile/background/middleware.js +++ b/react/features/mobile/background/middleware.js @@ -3,7 +3,7 @@ import { AppState } from 'react-native'; import type { Dispatch } from 'redux'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; import { MiddlewareRegistry } from '../../base/redux'; import { diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/callkit/middleware.js index 5e1f04d85d..6c1ed9d085 100644 --- a/react/features/mobile/callkit/middleware.js +++ b/react/features/mobile/callkit/middleware.js @@ -3,12 +3,8 @@ import uuid from 'uuid'; import { createTrackMutedEvent, sendAnalytics } from '../../analytics'; -import { - APP_WILL_MOUNT, - APP_WILL_UNMOUNT, - appNavigate, - getName -} from '../../app'; +import { appNavigate, getName } from '../../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; import { CONFERENCE_FAILED, CONFERENCE_LEFT, diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index 6c0eda69e2..9c768a1f4e 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -2,7 +2,7 @@ import { NativeModules } from 'react-native'; -import { getAppProp } from '../../app'; +import { getAppProp } from '../../base/app'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, diff --git a/react/features/mobile/full-screen/middleware.js b/react/features/mobile/full-screen/middleware.js index e702f0c7ba..f77b23bedc 100644 --- a/react/features/mobile/full-screen/middleware.js +++ b/react/features/mobile/full-screen/middleware.js @@ -3,7 +3,7 @@ import { StatusBar } from 'react-native'; import { Immersive } from 'react-native-immersive'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; import { getCurrentConference } from '../../base/conference'; import { Platform } from '../../base/react'; import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux'; diff --git a/react/features/mobile/image-cache/middleware.js b/react/features/mobile/image-cache/middleware.js index dd9d18b4f1..789d32f24d 100644 --- a/react/features/mobile/image-cache/middleware.js +++ b/react/features/mobile/image-cache/middleware.js @@ -1,6 +1,6 @@ /* @flow */ -import { APP_WILL_MOUNT } from '../../app'; +import { APP_WILL_MOUNT } from '../../base/app'; import { getAvatarURL, getLocalParticipant, diff --git a/react/features/mobile/network-activity/middleware.js b/react/features/mobile/network-activity/middleware.js index 09d686d24e..3d953d1b7d 100644 --- a/react/features/mobile/network-activity/middleware.js +++ b/react/features/mobile/network-activity/middleware.js @@ -2,7 +2,7 @@ import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; import { MiddlewareRegistry } from '../../base/redux'; import { diff --git a/react/features/mobile/picture-in-picture/actions.js b/react/features/mobile/picture-in-picture/actions.js index 4bec87d67b..015776296a 100644 --- a/react/features/mobile/picture-in-picture/actions.js +++ b/react/features/mobile/picture-in-picture/actions.js @@ -2,7 +2,7 @@ import { NativeModules } from 'react-native'; -import { getAppProp } from '../../app'; +import { getAppProp } from '../../base/app'; import { Platform } from '../../base/react'; import { ENTER_PICTURE_IN_PICTURE } from './actionTypes'; diff --git a/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js b/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js index 065d6d356d..de076701c4 100644 --- a/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js +++ b/react/features/mobile/picture-in-picture/components/PictureInPictureButton.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; -import { getAppProp } from '../../../app'; +import { getAppProp } from '../../../base/app'; import { translate } from '../../../base/i18n'; import { AbstractButton } from '../../../base/toolbox'; import type { AbstractButtonProps } from '../../../base/toolbox'; diff --git a/react/features/recent-list/middleware.js b/react/features/recent-list/middleware.js index fcc750f5a9..81dfb1e09f 100644 --- a/react/features/recent-list/middleware.js +++ b/react/features/recent-list/middleware.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT } from '../app'; +import { APP_WILL_MOUNT } from '../base/app'; import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference'; import { addKnownDomains } from '../base/known-domains'; import { MiddlewareRegistry } from '../base/redux'; diff --git a/react/features/recent-list/reducer.js b/react/features/recent-list/reducer.js index aa2abd67bf..de262a039b 100644 --- a/react/features/recent-list/reducer.js +++ b/react/features/recent-list/reducer.js @@ -1,6 +1,6 @@ // @flow -import { APP_WILL_MOUNT } from '../app'; +import { APP_WILL_MOUNT } from '../base/app'; import { getURLWithoutParamsNormalized } from '../base/connection'; import { ReducerRegistry } from '../base/redux'; import { PersistenceRegistry } from '../base/storage'; diff --git a/react/features/recording/middleware.js b/react/features/recording/middleware.js index 69db6d1d28..b2c40527ef 100644 --- a/react/features/recording/middleware.js +++ b/react/features/recording/middleware.js @@ -1,5 +1,6 @@ /* @flow */ +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { CONFERENCE_WILL_JOIN, getCurrentConference } from '../base/conference'; import JitsiMeetJS, { JitsiConferenceEvents, @@ -13,8 +14,6 @@ import { unregisterSound } from '../base/sounds'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; - import { clearRecordingSessions, hidePendingRecordingNotification, diff --git a/react/features/welcome/functions.js b/react/features/welcome/functions.js index 1a5d7990e0..bc81f3d331 100644 --- a/react/features/welcome/functions.js +++ b/react/features/welcome/functions.js @@ -1,6 +1,6 @@ // @flow -import { getAppProp } from '../app'; +import { getAppProp } from '../base/app'; import { toState } from '../base/redux'; declare var APP: Object;