// @flow import { jitsiLocalStorage } from '@jitsi/js-utils'; 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, PersistenceRegistry, ReducerRegistry, StateListenerRegistry } from '../../redux'; import { SoundCollection } from '../../sounds'; import { createDeferred } from '../../util'; import { appWillMount, appWillUnmount } from '../actions'; import logger from '../logger'; declare var APP: Object; /** * The type of the React {@code Component} state of {@link BaseApp}. */ type State = { /** * The {@code Route} rendered by the {@code BaseApp}. */ route: Object, /** * The redux store used by the {@code BaseApp}. */ store: Object }; /** * Base (abstract) class for main App component. * * @abstract */ export default class BaseApp extends Component<*, State> { /** * The deferred for the initialisation {{promise, resolve, reject}}. */ _init: Object; /** * 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 = { route: {}, store: undefined }; } /** * Initializes the app. * * @inheritdoc */ async componentDidMount() { /** * Make the mobile {@code BaseApp} wait until the {@code AsyncStorage} * implementation of {@code Storage} initializes fully. * * @private * @see {@link #_initStorage} * @type {Promise} */ this._init = createDeferred(); try { await this._initStorage(); const setStatePromise = new Promise(resolve => { this.setState({ store: this._createStore() }, resolve); }); await setStatePromise; await this._extraInit(); } catch (err) { /* BaseApp should always initialize! */ logger.error(err); } this.state.store.dispatch(appWillMount(this)); this._init.resolve(); } /** * De-initializes the app. * * @inheritdoc */ componentWillUnmount() { this.state.store.dispatch(appWillUnmount(this)); } /** * Logs for errors that were not caught. * * @param {Error} error - The error that was thrown. * @param {Object} info - Info about the error(stack trace);. * * @returns {void} */ componentDidCatch(error: Error, info: Object) { logger.error(error, info); } /** * 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 = jitsiLocalStorage.getItem('_initializing'); return _initializing || Promise.resolve(); } /** * Extra initialisation that subclasses might require. * * @returns {void} */ _extraInit() { // To be implemented by subclass. } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const { route: { component, props }, store } = this.state; if (store) { return ( { this._createMainElement(component, props) } { this._createExtraElement() } { this._renderDialogContainer() } ); } return null; } /** * Creates an extra {@link ReactElement}s to be added (unconditionally) * 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 component ? React.createElement(component, props || {}) : null; } /** * 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. const middleware = MiddlewareRegistry.applyMiddleware(Thunk); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(reducer, PersistenceRegistry.getPersistedState(), composeEnhancers(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); }); } /** * Renders the platform specific dialog container. * * @returns {React$Element} */ _renderDialogContainer: () => React$Element<*>; }