You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BaseApp.tsx 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. // @ts-expect-error
  2. import { jitsiLocalStorage } from '@jitsi/js-utils';
  3. import { isEqual } from 'lodash-es';
  4. import React, { Component, ComponentType, Fragment } from 'react';
  5. import { I18nextProvider } from 'react-i18next';
  6. import { Provider } from 'react-redux';
  7. import { compose, createStore } from 'redux';
  8. import Thunk from 'redux-thunk';
  9. import { IStore } from '../../../app/types';
  10. import i18next from '../../i18n/i18next';
  11. import MiddlewareRegistry from '../../redux/MiddlewareRegistry';
  12. import PersistenceRegistry from '../../redux/PersistenceRegistry';
  13. import ReducerRegistry from '../../redux/ReducerRegistry';
  14. import StateListenerRegistry from '../../redux/StateListenerRegistry';
  15. import SoundCollection from '../../sounds/components/SoundCollection';
  16. import { appWillMount, appWillUnmount } from '../actions';
  17. import logger from '../logger';
  18. /**
  19. * The type of the React {@code Component} state of {@link BaseApp}.
  20. */
  21. interface IState {
  22. /**
  23. * The {@code Route} rendered by the {@code BaseApp}.
  24. */
  25. route: {
  26. component?: ComponentType;
  27. props?: Object;
  28. };
  29. /**
  30. * The redux store used by the {@code BaseApp}.
  31. */
  32. store?: IStore;
  33. }
  34. /**
  35. * Base (abstract) class for main App component.
  36. *
  37. * @abstract
  38. */
  39. export default class BaseApp<P> extends Component<P, IState> {
  40. /**
  41. * The deferred for the initialisation {{promise, resolve, reject}}.
  42. */
  43. _init: PromiseWithResolvers<any>
  44. /**
  45. * Initializes a new {@code BaseApp} instance.
  46. *
  47. * @param {Object} props - The read-only React {@code Component} props with
  48. * which the new instance is to be initialized.
  49. */
  50. constructor(props: P) {
  51. super(props);
  52. this.state = {
  53. route: {},
  54. store: undefined
  55. };
  56. }
  57. /**
  58. * Initializes the app.
  59. *
  60. * @inheritdoc
  61. */
  62. async componentDidMount() {
  63. /**
  64. * Make the mobile {@code BaseApp} wait until the {@code AsyncStorage}
  65. * implementation of {@code Storage} initializes fully.
  66. *
  67. * @private
  68. * @see {@link #_initStorage}
  69. * @type {Promise}
  70. */
  71. this._init = Promise.withResolvers();
  72. try {
  73. await this._initStorage();
  74. const setStatePromise = new Promise(resolve => {
  75. this.setState({
  76. // @ts-ignore
  77. store: this._createStore()
  78. }, resolve);
  79. });
  80. await setStatePromise;
  81. await this._extraInit();
  82. } catch (err) {
  83. /* BaseApp should always initialize! */
  84. logger.error(err);
  85. }
  86. this.state.store?.dispatch(appWillMount(this));
  87. // @ts-ignore
  88. this._init.resolve();
  89. }
  90. /**
  91. * De-initializes the app.
  92. *
  93. * @inheritdoc
  94. */
  95. componentWillUnmount() {
  96. this.state.store?.dispatch(appWillUnmount(this));
  97. }
  98. /**
  99. * Logs for errors that were not caught.
  100. *
  101. * @param {Error} error - The error that was thrown.
  102. * @param {Object} info - Info about the error(stack trace);.
  103. *
  104. * @returns {void}
  105. */
  106. componentDidCatch(error: Error, info: Object) {
  107. logger.error(error, info);
  108. }
  109. /**
  110. * Delays this {@code BaseApp}'s startup until the {@code Storage}
  111. * implementation of {@code localStorage} initializes. While the
  112. * initialization is instantaneous on Web (with Web Storage API), it is
  113. * asynchronous on mobile/react-native.
  114. *
  115. * @private
  116. * @returns {Promise}
  117. */
  118. _initStorage(): Promise<any> {
  119. const _initializing = jitsiLocalStorage.getItem('_initializing');
  120. return _initializing || Promise.resolve();
  121. }
  122. /**
  123. * Extra initialisation that subclasses might require.
  124. *
  125. * @returns {void}
  126. */
  127. _extraInit() {
  128. // To be implemented by subclass.
  129. }
  130. /**
  131. * Implements React's {@link Component#render()}.
  132. *
  133. * @inheritdoc
  134. * @returns {ReactElement}
  135. */
  136. render() {
  137. const { route: { component, props }, store } = this.state;
  138. if (store) {
  139. return (
  140. <I18nextProvider i18n = { i18next }>
  141. {/* @ts-ignore */}
  142. <Provider store = { store }>
  143. <Fragment>
  144. { this._createMainElement(component, props) }
  145. <SoundCollection />
  146. { this._createExtraElement() }
  147. { this._renderDialogContainer() }
  148. </Fragment>
  149. </Provider>
  150. </I18nextProvider>
  151. );
  152. }
  153. return null;
  154. }
  155. /**
  156. * Creates an extra {@link ReactElement}s to be added (unconditionally)
  157. * alongside the main element.
  158. *
  159. * @returns {ReactElement}
  160. * @abstract
  161. * @protected
  162. */
  163. _createExtraElement(): React.ReactElement | null {
  164. return null;
  165. }
  166. /**
  167. * Creates a {@link ReactElement} from the specified component, the
  168. * specified props and the props of this {@code AbstractApp} which are
  169. * suitable for propagation to the children of this {@code Component}.
  170. *
  171. * @param {Component} component - The component from which the
  172. * {@code ReactElement} is to be created.
  173. * @param {Object} props - The read-only React {@code Component} props with
  174. * which the {@code ReactElement} is to be initialized.
  175. * @returns {ReactElement}
  176. * @protected
  177. */
  178. _createMainElement(component?: ComponentType, props?: Object) {
  179. return component ? React.createElement(component, props || {}) : null;
  180. }
  181. /**
  182. * Initializes a new redux store instance suitable for use by this
  183. * {@code AbstractApp}.
  184. *
  185. * @private
  186. * @returns {Store} - A new redux store instance suitable for use by
  187. * this {@code AbstractApp}.
  188. */
  189. _createStore() {
  190. // Create combined reducer from all reducers in ReducerRegistry.
  191. const reducer = ReducerRegistry.combineReducers();
  192. // Apply all registered middleware from the MiddlewareRegistry and
  193. // additional 3rd party middleware:
  194. // - Thunk - allows us to dispatch async actions easily. For more info
  195. // @see https://github.com/gaearon/redux-thunk.
  196. const middleware = MiddlewareRegistry.applyMiddleware(Thunk);
  197. // @ts-ignore
  198. const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  199. const store = createStore(reducer, PersistenceRegistry.getPersistedState(), composeEnhancers(middleware));
  200. // StateListenerRegistry
  201. StateListenerRegistry.subscribe(store);
  202. // This is temporary workaround to be able to dispatch actions from
  203. // non-reactified parts of the code (conference.js for example).
  204. // Don't use in the react code!!!
  205. // FIXME: remove when the reactification is finished!
  206. if (typeof APP !== 'undefined') {
  207. // @ts-ignore
  208. APP.store = store;
  209. }
  210. return store;
  211. }
  212. /**
  213. * Navigates to a specific Route.
  214. *
  215. * @param {Route} route - The Route to which to navigate.
  216. * @returns {Promise}
  217. */
  218. _navigate(route: {
  219. component?: ComponentType<any>;
  220. href?: string;
  221. props?: Object;
  222. }): Promise<any> {
  223. if (isEqual(route, this.state.route)) {
  224. return Promise.resolve();
  225. }
  226. if (route.href) {
  227. // This navigation requires loading a new URL in the browser.
  228. window.location.href = route.href;
  229. return Promise.resolve();
  230. }
  231. // XXX React's setState is asynchronous which means that the value of
  232. // this.state.route above may not even be correct. If the check is
  233. // performed before setState completes, the app may not navigate to the
  234. // expected route. In order to mitigate the problem, _navigate was
  235. // changed to return a Promise.
  236. return new Promise(resolve => { // @ts-ignore
  237. this.setState({ route }, resolve);
  238. });
  239. }
  240. /**
  241. * Renders the platform specific dialog container.
  242. *
  243. * @returns {React$Element}
  244. */
  245. _renderDialogContainer(): React.ReactElement | null {
  246. return null;
  247. }
  248. }