jiti-meet/react/features/base/app/components/BaseApp.js

247 lines
7.1 KiB
JavaScript

// @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;
/**
* 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> {
_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 = {
route: {},
// $FlowFixMe
store: undefined
};
}
/**
* Initializes the app.
*
* @inheritdoc
*/
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 = this._initStorage()
.catch(() => { /* BaseApp should always initialize! */ })
.then(() => new Promise(resolve => {
this.setState({
store: this._createStore()
}, resolve);
}))
.then(() => this.state.store.dispatch(appWillMount(this)))
.catch(() => { /* BaseApp should always initialize! */ });
}
/**
* De-initializes the app.
*
* @inheritdoc
*/
componentWillUnmount() {
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 { route: { component }, store } = this.state;
if (store && component) {
return (
<I18nextProvider i18n = { i18next }>
<Provider store = { store }>
<Fragment>
{ this._createMainElement(component) }
<SoundCollection />
{ this._createExtraElement() }
{ this._renderDialogContainer() }
</Fragment>
</Provider>
</I18nextProvider>
);
}
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);
});
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer: () => React$Element<*>
}