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.

AbstractApp.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. /* global APP */
  2. import PropTypes from 'prop-types';
  3. import React, { Component } from 'react';
  4. import { I18nextProvider } from 'react-i18next';
  5. import { Provider } from 'react-redux';
  6. import { compose, createStore } from 'redux';
  7. import Thunk from 'redux-thunk';
  8. import { i18next } from '../../base/i18n';
  9. import {
  10. localParticipantJoined,
  11. localParticipantLeft
  12. } from '../../base/participants';
  13. import { getProfile } from '../../base/profile';
  14. import { Fragment, RouteRegistry } from '../../base/react';
  15. import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
  16. import { PersistenceRegistry } from '../../base/storage';
  17. import { toURLString } from '../../base/util';
  18. import { OverlayContainer } from '../../overlay';
  19. import { BlankPage } from '../../welcome';
  20. import { appNavigate, appWillMount, appWillUnmount } from '../actions';
  21. /**
  22. * The default URL to open if no other was specified to {@code AbstractApp}
  23. * via props.
  24. */
  25. const DEFAULT_URL = 'https://meet.jit.si';
  26. /**
  27. * Base (abstract) class for main App component.
  28. *
  29. * @abstract
  30. */
  31. export class AbstractApp extends Component {
  32. /**
  33. * {@code AbstractApp} component's property types.
  34. *
  35. * @static
  36. */
  37. static propTypes = {
  38. /**
  39. * The default URL {@code AbstractApp} is to open when not in any
  40. * conference/room.
  41. */
  42. defaultURL: PropTypes.string,
  43. /**
  44. * (Optional) redux store for this app.
  45. */
  46. store: PropTypes.object,
  47. // XXX Refer to the implementation of loadURLObject: in
  48. // ios/sdk/src/JitsiMeetView.m for further information.
  49. timestamp: PropTypes.any,
  50. /**
  51. * The URL, if any, with which the app was launched.
  52. */
  53. url: PropTypes.oneOfType([
  54. PropTypes.object,
  55. PropTypes.string
  56. ])
  57. };
  58. /**
  59. * Initializes a new {@code AbstractApp} instance.
  60. *
  61. * @param {Object} props - The read-only React {@code Component} props with
  62. * which the new instance is to be initialized.
  63. */
  64. constructor(props) {
  65. super(props);
  66. this.state = {
  67. /**
  68. * The Route rendered by this {@code AbstractApp}.
  69. *
  70. * @type {Route}
  71. */
  72. route: undefined,
  73. /**
  74. * The state of the »possible« async initialization of
  75. * the {@code AbstractApp}.
  76. */
  77. appAsyncInitialized: false,
  78. /**
  79. * The redux store used by this {@code AbstractApp}.
  80. *
  81. * @type {Store}
  82. */
  83. store: undefined
  84. };
  85. /**
  86. * This way we make the mobile version wait until the
  87. * {@code AsyncStorage} implementation of {@code Storage}
  88. * properly initializes. On web it does actually nothing, see
  89. * {@link #_initStorage}.
  90. */
  91. this.init = this._initStorage().then(() => {
  92. this.setState({
  93. route: undefined,
  94. store: this._maybeCreateStore(props)
  95. });
  96. });
  97. }
  98. /**
  99. * Init lib-jitsi-meet and create local participant when component is going
  100. * to be mounted.
  101. *
  102. * @inheritdoc
  103. */
  104. componentWillMount() {
  105. this.init.then(() => {
  106. const { dispatch } = this._getStore();
  107. dispatch(appWillMount(this));
  108. // FIXME I believe it makes more sense for a middleware to dispatch
  109. // localParticipantJoined on APP_WILL_MOUNT because the order of
  110. // actions is important, not the call site. Moreover, we've got
  111. // localParticipant business logic in the React Component
  112. // (i.e. UI) AbstractApp now.
  113. let localParticipant = {};
  114. if (typeof APP === 'object') {
  115. localParticipant = {
  116. avatarID: APP.settings.getAvatarId(),
  117. avatarURL: APP.settings.getAvatarUrl(),
  118. email: APP.settings.getEmail(),
  119. name: APP.settings.getDisplayName()
  120. };
  121. }
  122. // Profile is the new React compatible settings.
  123. const profile = getProfile(this._getStore().getState());
  124. if (profile) {
  125. localParticipant.email
  126. = profile.email || localParticipant.email;
  127. localParticipant.name
  128. = profile.displayName || localParticipant.name;
  129. }
  130. // We set the initialized state here and not in the contructor to
  131. // make sure that {@code componentWillMount} gets invoked before
  132. // the app tries to render the actual app content.
  133. this.setState({
  134. appAsyncInitialized: true
  135. });
  136. dispatch(localParticipantJoined(localParticipant));
  137. // If a URL was explicitly specified to this React Component,
  138. // then open it; otherwise, use a default.
  139. this._openURL(toURLString(this.props.url) || this._getDefaultURL());
  140. });
  141. }
  142. /**
  143. * Notifies this mounted React {@code Component} that it will receive new
  144. * props. Makes sure that this {@code AbstractApp} has a redux store to use.
  145. *
  146. * @inheritdoc
  147. * @param {Object} nextProps - The read-only React {@code Component} props
  148. * that this instance will receive.
  149. * @returns {void}
  150. */
  151. componentWillReceiveProps(nextProps) {
  152. const { props } = this;
  153. this.init.then(() => {
  154. // The consumer of this AbstractApp did not provide a redux store.
  155. if (typeof nextProps.store === 'undefined'
  156. // The consumer of this AbstractApp did provide a redux
  157. // store before. Which means that the consumer changed
  158. // their mind. In such a case this instance should create
  159. // its own internal redux store. If the consumer did not
  160. // provide a redux store before, then this instance is
  161. // using its own internal redux store already.
  162. && typeof props.store !== 'undefined') {
  163. this.setState({
  164. store: this._maybeCreateStore(nextProps)
  165. });
  166. }
  167. // Deal with URL changes.
  168. let { url } = nextProps;
  169. url = toURLString(url);
  170. if (toURLString(props.url) !== url
  171. // XXX Refer to the implementation of loadURLObject: in
  172. // ios/sdk/src/JitsiMeetView.m for further information.
  173. || props.timestamp !== nextProps.timestamp) {
  174. this._openURL(url || this._getDefaultURL());
  175. }
  176. });
  177. }
  178. /**
  179. * Dispose lib-jitsi-meet and remove local participant when component is
  180. * going to be unmounted.
  181. *
  182. * @inheritdoc
  183. */
  184. componentWillUnmount() {
  185. const { dispatch } = this._getStore();
  186. dispatch(localParticipantLeft());
  187. dispatch(appWillUnmount(this));
  188. }
  189. /**
  190. * Gets a {@code Location} object from the window with information about the
  191. * current location of the document. Explicitly defined to allow extenders
  192. * to override because React Native does not usually have a location
  193. * property on its window unless debugging remotely in which case the
  194. * browser that is the remote debugger will provide a location property on
  195. * the window.
  196. *
  197. * @public
  198. * @returns {Location} A {@code Location} object with information about the
  199. * current location of the document.
  200. */
  201. getWindowLocation() {
  202. return undefined;
  203. }
  204. /**
  205. * Delays app start until the {@code Storage} implementation initialises.
  206. * This is instantaneous on web, but is async on mobile.
  207. *
  208. * @private
  209. * @returns {ReactElement}
  210. */
  211. _initStorage() {
  212. if (typeof window.localStorage._initialized !== 'undefined') {
  213. return window.localStorage._initialized;
  214. }
  215. return Promise.resolve();
  216. }
  217. /**
  218. * Implements React's {@link Component#render()}.
  219. *
  220. * @inheritdoc
  221. * @returns {ReactElement}
  222. */
  223. render() {
  224. const { appAsyncInitialized, route } = this.state;
  225. const component = (route && route.component) || BlankPage;
  226. if (appAsyncInitialized && component) {
  227. return (
  228. <I18nextProvider i18n = { i18next }>
  229. <Provider store = { this._getStore() }>
  230. <Fragment>
  231. { this._createElement(component) }
  232. <OverlayContainer />
  233. </Fragment>
  234. </Provider>
  235. </I18nextProvider>
  236. );
  237. }
  238. return null;
  239. }
  240. /**
  241. * Creates a {@link ReactElement} from the specified component, the
  242. * specified props and the props of this {@code AbstractApp} which are
  243. * suitable for propagation to the children of this {@code Component}.
  244. *
  245. * @param {Component} component - The component from which the
  246. * {@code ReactElement} is to be created.
  247. * @param {Object} props - The read-only React {@code Component} props with
  248. * which the {@code ReactElement} is to be initialized.
  249. * @returns {ReactElement}
  250. * @protected
  251. */
  252. _createElement(component, props) {
  253. /* eslint-disable no-unused-vars */
  254. const {
  255. // Don't propagate the dispatch and store props because they usually
  256. // come from react-redux and programmers don't really expect them to
  257. // be inherited but rather explicitly connected.
  258. dispatch, // eslint-disable-line react/prop-types
  259. store,
  260. // The following props were introduced to be consumed entirely by
  261. // AbstractApp:
  262. defaultURL,
  263. url,
  264. // The remaining props, if any, are considered suitable for
  265. // propagation to the children of this Component.
  266. ...thisProps
  267. } = this.props;
  268. /* eslint-enable no-unused-vars */
  269. return React.createElement(component, {
  270. ...thisProps,
  271. ...props
  272. });
  273. }
  274. /**
  275. * Initializes a new redux store instance suitable for use by this
  276. * {@code AbstractApp}.
  277. *
  278. * @private
  279. * @returns {Store} - A new redux store instance suitable for use by
  280. * this {@code AbstractApp}.
  281. */
  282. _createStore() {
  283. // Create combined reducer from all reducers in ReducerRegistry.
  284. const reducer = ReducerRegistry.combineReducers();
  285. // Apply all registered middleware from the MiddlewareRegistry and
  286. // additional 3rd party middleware:
  287. // - Thunk - allows us to dispatch async actions easily. For more info
  288. // @see https://github.com/gaearon/redux-thunk.
  289. let middleware = MiddlewareRegistry.applyMiddleware(Thunk);
  290. // Try to enable Redux DevTools Chrome extension in order to make it
  291. // available for the purposes of facilitating development.
  292. let devToolsExtension;
  293. if (typeof window === 'object'
  294. && (devToolsExtension = window.devToolsExtension)) {
  295. middleware = compose(middleware, devToolsExtension());
  296. }
  297. return (
  298. createStore(
  299. reducer,
  300. PersistenceRegistry.getPersistedState(),
  301. middleware));
  302. }
  303. /**
  304. * Gets the default URL to be opened when this {@code App} mounts.
  305. *
  306. * @protected
  307. * @returns {string} The default URL to be opened when this {@code App}
  308. * mounts.
  309. */
  310. _getDefaultURL() {
  311. // If the execution environment provides a Location abstraction, then
  312. // this App at already at that location but it must be made aware of the
  313. // fact.
  314. const windowLocation = this.getWindowLocation();
  315. if (windowLocation) {
  316. const href = windowLocation.toString();
  317. if (href) {
  318. return href;
  319. }
  320. }
  321. return (
  322. this.props.defaultURL
  323. || getProfile(this._getStore().getState()).serverURL
  324. || DEFAULT_URL);
  325. }
  326. /**
  327. * Gets the redux store used by this {@code AbstractApp}.
  328. *
  329. * @protected
  330. * @returns {Store} - The redux store used by this {@code AbstractApp}.
  331. */
  332. _getStore() {
  333. let store = this.state.store;
  334. if (typeof store === 'undefined') {
  335. store = this.props.store;
  336. }
  337. return store;
  338. }
  339. /**
  340. * Creates a redux store to be used by this {@code AbstractApp} if such as a
  341. * store is not defined by the consumer of this {@code AbstractApp} through
  342. * its read-only React {@code Component} props.
  343. *
  344. * @param {Object} props - The read-only React {@code Component} props that
  345. * will eventually be received by this {@code AbstractApp}.
  346. * @private
  347. * @returns {Store} - The redux store to be used by this
  348. * {@code AbstractApp}.
  349. */
  350. _maybeCreateStore(props) {
  351. // The application Jitsi Meet is architected with redux. However, I do
  352. // not want consumers of the App React Component to be forced into
  353. // dealing with redux. If the consumer did not provide an external redux
  354. // store, utilize an internal redux store.
  355. let store = props.store;
  356. if (typeof store === 'undefined') {
  357. store = this._createStore();
  358. // This is temporary workaround to be able to dispatch actions from
  359. // non-reactified parts of the code (conference.js for example).
  360. // Don't use in the react code!!!
  361. // FIXME: remove when the reactification is finished!
  362. if (typeof APP !== 'undefined') {
  363. APP.store = store;
  364. }
  365. }
  366. return store;
  367. }
  368. /**
  369. * Navigates to a specific Route.
  370. *
  371. * @param {Route} route - The Route to which to navigate.
  372. * @returns {Promise}
  373. */
  374. _navigate(route) {
  375. if (RouteRegistry.areRoutesEqual(this.state.route, route)) {
  376. return Promise.resolve();
  377. }
  378. let nextState = {
  379. route
  380. };
  381. // The Web App was using react-router so it utilized react-router's
  382. // onEnter. During the removal of react-router, modifications were
  383. // minimized by preserving the onEnter interface:
  384. // (1) Router would provide its nextState to the Route's onEnter. As the
  385. // role of Router is now this AbstractApp and we use redux, provide the
  386. // redux store instead.
  387. // (2) A replace function would be provided to the Route in case it
  388. // chose to redirect to another path.
  389. route && this._onRouteEnter(route, this._getStore(), pathname => {
  390. if (pathname) {
  391. this._openURL(pathname);
  392. // Do not proceed with the route because it chose to redirect to
  393. // another path.
  394. nextState = undefined;
  395. } else {
  396. nextState.route = undefined;
  397. }
  398. });
  399. // XXX React's setState is asynchronous which means that the value of
  400. // this.state.route above may not even be correct. If the check is
  401. // performed before setState completes, the app may not navigate to the
  402. // expected route. In order to mitigate the problem, _navigate was
  403. // changed to return a Promise.
  404. return new Promise(resolve => {
  405. if (nextState) {
  406. this.setState(nextState, resolve);
  407. } else {
  408. resolve();
  409. }
  410. });
  411. }
  412. /**
  413. * Notifies this {@code App} that a specific Route is about to be rendered.
  414. *
  415. * @param {Route} route - The Route that is about to be rendered.
  416. * @private
  417. * @returns {void}
  418. */
  419. _onRouteEnter(route, ...args) {
  420. // Notify the route that it is about to be entered.
  421. const { onEnter } = route;
  422. typeof onEnter === 'function' && onEnter(...args);
  423. }
  424. /**
  425. * Navigates this {@code AbstractApp} to (i.e. opens) a specific URL.
  426. *
  427. * @param {string|Object} url - The URL to navigate this {@code AbstractApp}
  428. * to (i.e. the URL to open).
  429. * @protected
  430. * @returns {void}
  431. */
  432. _openURL(url) {
  433. this._getStore().dispatch(appNavigate(toURLString(url)));
  434. }
  435. }