Bläddra i källkod

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.
master
Saúl Ibarra Corretgé 6 år sedan
förälder
incheckning
dc246960df
39 ändrade filer med 455 tillägg och 406 borttagningar
  1. 0
    44
      react/features/app/actions.js
  2. 40
    256
      react/features/app/components/AbstractApp.js
  3. 38
    31
      react/features/app/components/App.native.js
  4. 4
    9
      react/features/app/components/App.web.js
  5. 2
    26
      react/features/app/functions.any.js
  6. 0
    2
      react/features/app/index.js
  7. 1
    1
      react/features/app/middleware.js
  8. 0
    0
      react/features/base/app/actionTypes.js
  9. 48
    0
      react/features/base/app/actions.js
  10. 255
    0
      react/features/base/app/components/BaseApp.js
  11. 1
    0
      react/features/base/app/components/index.js
  12. 28
    0
      react/features/base/app/functions.js
  13. 6
    0
      react/features/base/app/index.js
  14. 5
    5
      react/features/base/app/reducer.js
  15. 1
    1
      react/features/base/config/middleware.js
  16. 2
    1
      react/features/base/known-domains/middleware.js
  17. 1
    1
      react/features/base/known-domains/reducer.js
  18. 1
    1
      react/features/base/logging/middleware.js
  19. 1
    1
      react/features/base/participants/middleware.js
  20. 1
    2
      react/features/base/settings/reducer.js
  21. 1
    1
      react/features/calendar-sync/middleware.js
  22. 1
    1
      react/features/calendar-sync/reducer.js
  23. 1
    1
      react/features/chat/middleware.js
  24. 1
    1
      react/features/invite/functions.js
  25. 1
    1
      react/features/invite/middleware.any.js
  26. 1
    1
      react/features/invite/middleware.native.js
  27. 1
    1
      react/features/mobile/audio-mode/middleware.js
  28. 1
    1
      react/features/mobile/background/middleware.js
  29. 2
    6
      react/features/mobile/callkit/middleware.js
  30. 1
    1
      react/features/mobile/external-api/middleware.js
  31. 1
    1
      react/features/mobile/full-screen/middleware.js
  32. 1
    1
      react/features/mobile/image-cache/middleware.js
  33. 1
    1
      react/features/mobile/network-activity/middleware.js
  34. 1
    1
      react/features/mobile/picture-in-picture/actions.js
  35. 1
    1
      react/features/mobile/picture-in-picture/components/PictureInPictureButton.js
  36. 1
    1
      react/features/recent-list/middleware.js
  37. 1
    1
      react/features/recent-list/reducer.js
  38. 1
    2
      react/features/recording/middleware.js
  39. 1
    1
      react/features/welcome/functions.js

+ 0
- 44
react/features/app/actions.js Visa fil

@@ -15,7 +15,6 @@ import { loadConfig } from '../base/lib-jitsi-meet';
15 15
 import { parseURIString, toURLString } from '../base/util';
16 16
 import { setFatalError } from '../overlay';
17 17
 
18
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
19 18
 import { getDefaultURL } from './functions';
20 19
 
21 20
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -138,49 +137,6 @@ function _appNavigateToOptionalLocation(
138 137
     return _appNavigateToMandatoryLocation(dispatch, getState, location);
139 138
 }
140 139
 
141
-/**
142
- * Signals that a specific App will mount (in the terms of React).
143
- *
144
- * @param {App} app - The App which will mount.
145
- * @returns {{
146
- *     type: APP_WILL_MOUNT,
147
- *     app: App
148
- * }}
149
- */
150
-export function appWillMount(app: Object) {
151
-    return (dispatch: Dispatch<*>) => {
152
-        dispatch({
153
-            type: APP_WILL_MOUNT,
154
-            app
155
-        });
156
-
157
-        // TODO There was a redux action creator appInit which I did not like
158
-        // because we already had the redux action creator appWillMount and,
159
-        // respectively, the redux action APP_WILL_MOUNT. So I set out to remove
160
-        // appInit and managed to move everything it was doing but the
161
-        // following. Which is not extremely bad because we haven't moved the
162
-        // API module into its own feature yet so we're bound to work on that in
163
-        // the future.
164
-        typeof APP === 'object' && APP.API.init();
165
-    };
166
-}
167
-
168
-/**
169
- * Signals that a specific App will unmount (in the terms of React).
170
- *
171
- * @param {App} app - The App which will unmount.
172
- * @returns {{
173
- *     type: APP_WILL_UNMOUNT,
174
- *     app: App
175
- * }}
176
- */
177
-export function appWillUnmount(app: Object) {
178
-    return {
179
-        type: APP_WILL_UNMOUNT,
180
-        app
181
-    };
182
-}
183
-
184 140
 /**
185 141
  * Loads config.js from a specific host.
186 142
  *

+ 40
- 256
react/features/app/components/AbstractApp.js Visa fil

@@ -1,107 +1,44 @@
1
-/* global APP */
1
+// @flow
2 2
 
3
-import _ from 'lodash';
4
-import PropTypes from 'prop-types';
5
-import React, { Component, Fragment } from 'react';
6
-import { I18nextProvider } from 'react-i18next';
7
-import { Provider } from 'react-redux';
8
-import { compose, createStore } from 'redux';
9
-import Thunk from 'redux-thunk';
3
+import React, { Fragment } from 'react';
10 4
 
11
-import { i18next } from '../../base/i18n';
12
-import {
13
-    MiddlewareRegistry,
14
-    ReducerRegistry,
15
-    StateListenerRegistry
16
-} from '../../base/redux';
17
-import { SoundCollection } from '../../base/sounds';
18
-import { PersistenceRegistry } from '../../base/storage';
5
+import { BaseApp } from '../../base/app';
19 6
 import { toURLString } from '../../base/util';
20 7
 import { OverlayContainer } from '../../overlay';
21 8
 
22
-import { appNavigate, appWillMount, appWillUnmount } from '../actions';
9
+import { appNavigate } from '../actions';
23 10
 import { getDefaultURL } from '../functions';
24 11
 
25 12
 /**
26
- * Base (abstract) class for main App component.
27
- *
28
- * @abstract
13
+ * {@code AbstractApp} component's property types.
29 14
  */
30
-export class AbstractApp extends Component {
15
+export type Props = {
16
+
31 17
     /**
32
-     * {@code AbstractApp} component's property types.
33
-     *
34
-     * @static
18
+     * The default URL {@code AbstractApp} is to open when not in any
19
+     * conference/room.
35 20
      */
36
-    static propTypes = {
37
-        /**
38
-         * The default URL {@code AbstractApp} is to open when not in any
39
-         * conference/room.
40
-         */
41
-        defaultURL: PropTypes.string,
42
-
43
-        // XXX Refer to the implementation of loadURLObject: in
44
-        // ios/sdk/src/JitsiMeetView.m for further information.
45
-        timestamp: PropTypes.any,
46
-
47
-        /**
48
-         * The URL, if any, with which the app was launched.
49
-         */
50
-        url: PropTypes.oneOfType([
51
-            PropTypes.object,
52
-            PropTypes.string
53
-        ])
54
-    };
21
+    defaultURL: string,
55 22
 
56 23
     /**
57
-     * Initializes a new {@code AbstractApp} instance.
58
-     *
59
-     * @param {Object} props - The read-only React {@code Component} props with
60
-     * which the new instance is to be initialized.
24
+     * XXX Refer to the implementation of loadURLObject: in
25
+     * ios/sdk/src/JitsiMeetView.m for further information.
61 26
      */
62
-    constructor(props) {
63
-        super(props);
64
-
65
-        this.state = {
27
+    timestamp: any,
66 28
 
67
-            /**
68
-             * The state of the »possible« async initialization of the
69
-             * {@code AbstractApp}.
70
-             */
71
-            appAsyncInitialized: false,
72
-
73
-            /**
74
-             * The Route rendered by this {@code AbstractApp}.
75
-             *
76
-             * @type {Route}
77
-             */
78
-            route: {},
79
-
80
-            /**
81
-             * The redux store used by this {@code AbstractApp}.
82
-             *
83
-             * @type {Store}
84
-             */
85
-            store: undefined
86
-        };
29
+    /**
30
+     * The URL, if any, with which the app was launched.
31
+     */
32
+    url: Object | string
33
+};
87 34
 
88
-        /**
89
-         * Make the mobile {@code AbstractApp} wait until the
90
-         * {@code AsyncStorage} implementation of {@code Storage} initializes
91
-         * fully.
92
-         *
93
-         * @private
94
-         * @see {@link #_initStorage}
95
-         * @type {Promise}
96
-         */
97
-        this._init
98
-            = this._initStorage()
99
-                .catch(() => { /* AbstractApp should always initialize! */ })
100
-                .then(() =>
101
-                    this.setState({
102
-                        store: this._createStore()
103
-                    }));
104
-    }
35
+/**
36
+ * Base (abstract) class for main App component.
37
+ *
38
+ * @abstract
39
+ */
40
+export class AbstractApp extends BaseApp<Props, *> {
41
+    _init: Promise<*>;
105 42
 
106 43
     /**
107 44
      * Initializes the app.
@@ -109,20 +46,11 @@ export class AbstractApp extends Component {
109 46
      * @inheritdoc
110 47
      */
111 48
     componentWillMount() {
112
-        this._init.then(() => {
113
-            const { dispatch } = this.state.store;
114
-
115
-            dispatch(appWillMount(this));
49
+        super.componentWillMount();
116 50
 
117
-            // We set the initialized state here and not in the constructor to
118
-            // make sure that {@code componentWillMount} gets invoked before the
119
-            // app tries to render the actual app content.
120
-            this.setState({
121
-                appAsyncInitialized: true
122
-            });
123
-
124
-            // If a URL was explicitly specified to this React Component, then
125
-            // open it; otherwise, use a default.
51
+        this._init.then(() => {
52
+            // If a URL was explicitly specified to this React Component,
53
+            // then open it; otherwise, use a default.
126 54
             this._openURL(toURLString(this.props.url) || this._getDefaultURL());
127 55
         });
128 56
     }
@@ -136,7 +64,7 @@ export class AbstractApp extends Component {
136 64
      * that this instance will receive.
137 65
      * @returns {void}
138 66
      */
139
-    componentWillReceiveProps(nextProps) {
67
+    componentWillReceiveProps(nextProps: Props) {
140 68
         const { props } = this;
141 69
 
142 70
         this._init.then(() => {
@@ -154,15 +82,6 @@ export class AbstractApp extends Component {
154 82
         });
155 83
     }
156 84
 
157
-    /**
158
-     * De-initializes the app.
159
-     *
160
-     * @inheritdoc
161
-     */
162
-    componentWillUnmount() {
163
-        this.state.store.dispatch(appWillUnmount(this));
164
-    }
165
-
166 85
     /**
167 86
      * Gets a {@code Location} object from the window with information about the
168 87
      * current location of the document. Explicitly defined to allow extenders
@@ -180,131 +99,22 @@ export class AbstractApp extends Component {
180 99
     }
181 100
 
182 101
     /**
183
-     * Delays this {@code AbstractApp}'s startup until the {@code Storage}
184
-     * implementation of {@code localStorage} initializes. While the
185
-     * initialization is instantaneous on Web (with Web Storage API), it is
186
-     * asynchronous on mobile/react-native.
187
-     *
188
-     * @private
189
-     * @returns {Promise}
190
-     */
191
-    _initStorage() {
192
-        const localStorageInitializing = window.localStorage._initializing;
193
-
194
-        return (
195
-            typeof localStorageInitializing === 'undefined'
196
-                ? Promise.resolve()
197
-                : localStorageInitializing);
198
-    }
199
-
200
-    /**
201
-     * Implements React's {@link Component#render()}.
202
-     *
203
-     * @inheritdoc
204
-     * @returns {ReactElement}
205
-     */
206
-    render() {
207
-        const { appAsyncInitialized, route, store } = this.state;
208
-        const { component } = route;
209
-
210
-        if (appAsyncInitialized && component) {
211
-            return (
212
-                <I18nextProvider i18n = { i18next }>
213
-                    <Provider store = { store }>
214
-                        <Fragment>
215
-                            { this._createElement(component) }
216
-                            <SoundCollection />
217
-                            <OverlayContainer />
218
-                        </Fragment>
219
-                    </Provider>
220
-                </I18nextProvider>
221
-            );
222
-        }
223
-
224
-        return null;
225
-    }
226
-
227
-    /**
228
-     * Creates a {@link ReactElement} from the specified component, the
229
-     * specified props and the props of this {@code AbstractApp} which are
230
-     * suitable for propagation to the children of this {@code Component}.
102
+     * Creates an extra {@link ReactElement}s to be added (unconditionaly)
103
+     * alongside the main element.
231 104
      *
232
-     * @param {Component} component - The component from which the
233
-     * {@code ReactElement} is to be created.
234
-     * @param {Object} props - The read-only React {@code Component} props with
235
-     * which the {@code ReactElement} is to be initialized.
236 105
      * @returns {ReactElement}
106
+     * @abstract
237 107
      * @protected
238 108
      */
239
-    _createElement(component, props) {
240
-        /* eslint-disable no-unused-vars */
241
-
242
-        const {
243
-            // The following props were introduced to be consumed entirely by
244
-            // AbstractApp:
245
-            defaultURL,
246
-            timestamp,
247
-            url,
248
-
249
-            // The remaining props, if any, are considered suitable for
250
-            // propagation to the children of this Component.
251
-            ...thisProps
252
-        } = this.props;
253
-
254
-        /* eslint-enable no-unused-vars */
255
-
256
-        return React.createElement(component, {
257
-            ...thisProps,
258
-            ...props
259
-        });
109
+    _createExtraElement() {
110
+        return (
111
+            <Fragment>
112
+                <OverlayContainer />
113
+            </Fragment>
114
+        );
260 115
     }
261 116
 
262
-    /**
263
-     * Initializes a new redux store instance suitable for use by this
264
-     * {@code AbstractApp}.
265
-     *
266
-     * @private
267
-     * @returns {Store} - A new redux store instance suitable for use by this
268
-     * {@code AbstractApp}.
269
-     */
270
-    _createStore() {
271
-        // Create combined reducer from all reducers in ReducerRegistry.
272
-        const reducer = ReducerRegistry.combineReducers();
273
-
274
-        // Apply all registered middleware from the MiddlewareRegistry and
275
-        // additional 3rd party middleware:
276
-        // - Thunk - allows us to dispatch async actions easily. For more info
277
-        // @see https://github.com/gaearon/redux-thunk.
278
-        let middleware = MiddlewareRegistry.applyMiddleware(Thunk);
279
-
280
-        // Try to enable Redux DevTools Chrome extension in order to make it
281
-        // available for the purposes of facilitating development.
282
-        let devToolsExtension;
283
-
284
-        if (typeof window === 'object'
285
-                && (devToolsExtension = window.devToolsExtension)) {
286
-            middleware = compose(middleware, devToolsExtension());
287
-        }
288
-
289
-        const store
290
-            = createStore(
291
-                reducer,
292
-                PersistenceRegistry.getPersistedState(),
293
-                middleware);
294
-
295
-        // StateListenerRegistry
296
-        StateListenerRegistry.subscribe(store);
297
-
298
-        // This is temporary workaround to be able to dispatch actions from
299
-        // non-reactified parts of the code (conference.js for example).
300
-        // Don't use in the react code!!!
301
-        // FIXME: remove when the reactification is finished!
302
-        if (typeof APP !== 'undefined') {
303
-            APP.store = store;
304
-        }
305
-
306
-        return store;
307
-    }
117
+    _createMainElement: (React$Element<*>, Object) => ?React$Element<*>;
308 118
 
309 119
     /**
310 120
      * Gets the default URL to be opened when this {@code App} mounts.
@@ -317,32 +127,6 @@ export class AbstractApp extends Component {
317 127
         return getDefaultURL(this.state.store);
318 128
     }
319 129
 
320
-    /**
321
-     * Navigates to a specific Route.
322
-     *
323
-     * @param {Route} route - The Route to which to navigate.
324
-     * @returns {Promise}
325
-     */
326
-    _navigate(route) {
327
-        if (_.isEqual(route, this.state.route)) {
328
-            return Promise.resolve();
329
-        }
330
-
331
-        if (route.href) {
332
-            // This navigation requires loading a new URL in the browser.
333
-            window.location.href = route.href;
334
-
335
-            return Promise.resolve();
336
-        }
337
-
338
-        // XXX React's setState is asynchronous which means that the value of
339
-        // this.state.route above may not even be correct. If the check is
340
-        // performed before setState completes, the app may not navigate to the
341
-        // expected route. In order to mitigate the problem, _navigate was
342
-        // changed to return a Promise.
343
-        return new Promise(resolve => this.setState({ route }, resolve));
344
-    }
345
-
346 130
     /**
347 131
      * Navigates this {@code AbstractApp} to (i.e. opens) a specific URL.
348 132
      *

+ 38
- 31
react/features/app/components/App.native.js Visa fil

@@ -1,6 +1,5 @@
1
-/* global __DEV__ */
1
+// @flow
2 2
 
3
-import PropTypes from 'prop-types';
4 3
 import React from 'react';
5 4
 import { Linking } from 'react-native';
6 5
 
@@ -23,47 +22,53 @@ import '../../mobile/proximity';
23 22
 import '../../mobile/wake-lock';
24 23
 
25 24
 import { AbstractApp } from './AbstractApp';
25
+import type { Props as AbstractAppProps } from './AbstractApp';
26
+
27
+declare var __DEV__;
26 28
 
27 29
 /**
28
- * Root application component.
29
- *
30
- * @extends AbstractApp
30
+ * App component's property types.
31 31
  */
32
-export class App extends AbstractApp {
32
+type Props = AbstractAppProps & {
33
+
33 34
     /**
34
-     * App component's property types.
35
-     *
36
-     * @static
35
+     * Whether the add people feature is enabled or not.
37 36
      */
38
-    static propTypes = {
39
-        ...AbstractApp.propTypes,
40
-
41
-        addPeopleEnabled: PropTypes.bool,
37
+    addPeopleEnabled: boolean,
42 38
 
43
-        dialOutEnabled: PropTypes.bool,
39
+    /**
40
+     * Whether the dial-out feature is enabled or not.
41
+     */
42
+    dialOutEnabled: boolean,
44 43
 
45
-        /**
46
-         * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
47
-         * button is rendered in the {@link Conference} view to afford entering
48
-         * Picture-in-Picture.
49
-         */
50
-        pictureInPictureEnabled: PropTypes.bool,
44
+    /**
45
+     * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
46
+     * button is rendered in the {@link Conference} view to afford entering
47
+     * Picture-in-Picture.
48
+     */
49
+    pictureInPictureEnabled: boolean,
51 50
 
52
-        /**
53
-         * Whether the Welcome page is enabled. If {@code true}, the Welcome
54
-         * page is rendered when the {@link App} is not at a location (URL)
55
-         * identifying a Jitsi Meet conference/room.
56
-         */
57
-        welcomePageEnabled: PropTypes.bool
58
-    };
51
+    /**
52
+     * Whether the Welcome page is enabled. If {@code true}, the Welcome
53
+     * page is rendered when the {@link App} is not at a location (URL)
54
+     * identifying a Jitsi Meet conference/room.
55
+     */
56
+    welcomePageEnabled: boolean
57
+};
59 58
 
59
+/**
60
+ * Root application component.
61
+ *
62
+ * @extends AbstractApp
63
+ */
64
+export class App extends AbstractApp {
60 65
     /**
61 66
      * Initializes a new App instance.
62 67
      *
63 68
      * @param {Object} props - The read-only React Component props with which
64 69
      * the new instance is to be initialized.
65 70
      */
66
-    constructor(props) {
71
+    constructor(props: Props) {
67 72
         super(props);
68 73
 
69 74
         // Bind event handlers so they are only bound once for every instance.
@@ -110,11 +115,11 @@ export class App extends AbstractApp {
110 115
      *
111 116
      * @override
112 117
      */
113
-    _createElement(component, props) {
118
+    _createMainElement(component, props) {
114 119
         return (
115 120
             <AspectRatioDetector>
116 121
                 <ReducedUIDetector>
117
-                    { super._createElement(component, props) }
122
+                    { super._createMainElement(component, props) }
118 123
                 </ReducedUIDetector>
119 124
             </AspectRatioDetector>
120 125
         );
@@ -158,6 +163,8 @@ export class App extends AbstractApp {
158 163
         }
159 164
     }
160 165
 
166
+    _onLinkingURL: (*) => void;
167
+
161 168
     /**
162 169
      * Notified by React's Linking API that a specific URL registered to be
163 170
      * handled by this App was activated.
@@ -169,7 +176,7 @@ export class App extends AbstractApp {
169 176
      * @returns {void}
170 177
      */
171 178
     _onLinkingURL({ url }) {
172
-        this._openURL(url);
179
+        super._openURL(url);
173 180
     }
174 181
 }
175 182
 

+ 4
- 9
react/features/app/components/App.web.js Visa fil

@@ -1,3 +1,5 @@
1
+// @flow
2
+
1 3
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
2 4
 import React from 'react';
3 5
 
@@ -14,23 +16,16 @@ import { AbstractApp } from './AbstractApp';
14 16
  * @extends AbstractApp
15 17
  */
16 18
 export class App extends AbstractApp {
17
-    /**
18
-     * App component's property types.
19
-     *
20
-     * @static
21
-     */
22
-    static propTypes = AbstractApp.propTypes;
23
-
24 19
     /**
25 20
      * Overrides the parent method to inject {@link AtlasKitThemeProvider} as
26 21
      * the top most component.
27 22
      *
28 23
      * @override
29 24
      */
30
-    _createElement(component, props) {
25
+    _createMainElement(component, props) {
31 26
         return (
32 27
             <AtlasKitThemeProvider mode = 'dark'>
33
-                { super._createElement(component, props) }
28
+                { super._createMainElement(component, props) }
34 29
             </AtlasKitThemeProvider>
35 30
         );
36 31
     }

+ 2
- 26
react/features/app/functions.any.js Visa fil

@@ -1,33 +1,9 @@
1 1
 // @flow
2 2
 
3
+import { getAppProp } from '../base/app';
3 4
 import { toState } from '../base/redux';
4 5
 import { getServerURL } from '../base/settings';
5 6
 
6
-/**
7
- * Gets the value of a specific React {@code Component} prop of the currently
8
- * mounted {@link App}.
9
- *
10
- * @param {Function|Object} stateful - The redux store or {@code getState}
11
- * function.
12
- * @param {string} propName - The name of the React {@code Component} prop of
13
- * the currently mounted {@code App} to get.
14
- * @returns {*} The value of the specified React {@code Compoennt} prop of the
15
- * currently mounted {@code App}.
16
- */
17
-export function getAppProp(stateful: Function | Object, propName: string) {
18
-    const state = toState(stateful)['features/app'];
19
-
20
-    if (state) {
21
-        const { app } = state;
22
-
23
-        if (app) {
24
-            return app.props[propName];
25
-        }
26
-    }
27
-
28
-    return undefined;
29
-}
30
-
31 7
 /**
32 8
  * Retrieves the default URL for the app. This can either come from a prop to
33 9
  * the root App component or be configured in the settings.
@@ -38,7 +14,7 @@ export function getAppProp(stateful: Function | Object, propName: string) {
38 14
  */
39 15
 export function getDefaultURL(stateful: Function | Object) {
40 16
     const state = toState(stateful);
41
-    const { app } = state['features/app'];
17
+    const { app } = state['features/base/app'];
42 18
 
43 19
     // If the execution environment provides a Location abstraction (e.g. a Web
44 20
     // browser), then we'll presume it's the one and only base URL it can be on.

+ 0
- 2
react/features/app/index.js Visa fil

@@ -1,7 +1,5 @@
1 1
 export * from './actions';
2
-export * from './actionTypes';
3 2
 export * from './components';
4 3
 export * from './functions';
5 4
 
6 5
 import './middleware';
7
-import './reducer';

+ 1
- 1
react/features/app/middleware.js Visa fil

@@ -72,7 +72,7 @@ function _connectionEstablished(store, next, action) {
72 72
  */
73 73
 function _navigate({ getState }) {
74 74
     const state = getState();
75
-    const { app } = state['features/app'];
75
+    const { app } = state['features/base/app'];
76 76
 
77 77
     _getRouteToRender(state).then(route => app._navigate(route));
78 78
 }

react/features/app/actionTypes.js → react/features/base/app/actionTypes.js Visa fil


+ 48
- 0
react/features/base/app/actions.js Visa fil

@@ -0,0 +1,48 @@
1
+// @flow
2
+
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
4
+
5
+declare var APP;
6
+
7
+/**
8
+ * Signals that a specific App will mount (in the terms of React).
9
+ *
10
+ * @param {App} app - The App which will mount.
11
+ * @returns {{
12
+ *     type: APP_WILL_MOUNT,
13
+ *     app: App
14
+ * }}
15
+ */
16
+export function appWillMount(app: Object) {
17
+    return (dispatch: Dispatch<*>) => {
18
+        dispatch({
19
+            type: APP_WILL_MOUNT,
20
+            app
21
+        });
22
+
23
+        // TODO There was a redux action creator appInit which I did not like
24
+        // because we already had the redux action creator appWillMount and,
25
+        // respectively, the redux action APP_WILL_MOUNT. So I set out to remove
26
+        // appInit and managed to move everything it was doing but the
27
+        // following. Which is not extremely bad because we haven't moved the
28
+        // API module into its own feature yet so we're bound to work on that in
29
+        // the future.
30
+        typeof APP === 'object' && APP.API.init();
31
+    };
32
+}
33
+
34
+/**
35
+ * Signals that a specific App will unmount (in the terms of React).
36
+ *
37
+ * @param {App} app - The App which will unmount.
38
+ * @returns {{
39
+ *     type: APP_WILL_UNMOUNT,
40
+ *     app: App
41
+ * }}
42
+ */
43
+export function appWillUnmount(app: Object) {
44
+    return {
45
+        type: APP_WILL_UNMOUNT,
46
+        app
47
+    };
48
+}

+ 255
- 0
react/features/base/app/components/BaseApp.js Visa fil

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

+ 1
- 0
react/features/base/app/components/index.js Visa fil

@@ -0,0 +1 @@
1
+export { default as BaseApp } from './BaseApp';

+ 28
- 0
react/features/base/app/functions.js Visa fil

@@ -0,0 +1,28 @@
1
+// @flow
2
+
3
+import { toState } from '../redux';
4
+
5
+/**
6
+ * Gets the value of a specific React {@code Component} prop of the currently
7
+ * mounted {@link App}.
8
+ *
9
+ * @param {Function|Object} stateful - The redux store or {@code getState}
10
+ * function.
11
+ * @param {string} propName - The name of the React {@code Component} prop of
12
+ * the currently mounted {@code App} to get.
13
+ * @returns {*} The value of the specified React {@code Compoennt} prop of the
14
+ * currently mounted {@code App}.
15
+ */
16
+export function getAppProp(stateful: Function | Object, propName: string) {
17
+    const state = toState(stateful)['features/base/app'];
18
+
19
+    if (state) {
20
+        const { app } = state;
21
+
22
+        if (app) {
23
+            return app.props[propName];
24
+        }
25
+    }
26
+
27
+    return undefined;
28
+}

+ 6
- 0
react/features/base/app/index.js Visa fil

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './components';
4
+export * from './functions';
5
+
6
+import './reducer';

react/features/app/reducer.js → react/features/base/app/reducer.js Visa fil

@@ -1,10 +1,10 @@
1 1
 // @flow
2 2
 
3
-import { ReducerRegistry } from '../base/redux';
3
+import { ReducerRegistry } from '../redux';
4 4
 
5 5
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
6 6
 
7
-ReducerRegistry.register('features/app', (state = {}, action) => {
7
+ReducerRegistry.register('features/base/app', (state = {}, action) => {
8 8
     switch (action.type) {
9 9
     case APP_WILL_MOUNT: {
10 10
         const { app } = action;
@@ -14,10 +14,10 @@ ReducerRegistry.register('features/app', (state = {}, action) => {
14 14
                 ...state,
15 15
 
16 16
                 /**
17
-                 * The one and only (i.e. singleton) {@link App} instance which
18
-                 * is currently mounted.
17
+                 * The one and only (i.e. singleton) {@link BaseApp} instance
18
+                 * which is currently mounted.
19 19
                  *
20
-                 * @type {App}
20
+                 * @type {BaseApp}
21 21
                  */
22 22
                 app
23 23
             };

+ 1
- 1
react/features/base/config/middleware.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT } from '../../app';
3
+import { APP_WILL_MOUNT } from '../app';
4 4
 import { addKnownDomains } from '../known-domains';
5 5
 import { MiddlewareRegistry } from '../redux';
6 6
 import { parseURIString } from '../util';

+ 2
- 1
react/features/base/known-domains/middleware.js Visa fil

@@ -1,7 +1,8 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT, getDefaultURL } from '../../app';
3
+import { getDefaultURL } from '../../app';
4 4
 
5
+import { APP_WILL_MOUNT } from '../app';
5 6
 import { SET_ROOM } from '../conference';
6 7
 import { MiddlewareRegistry } from '../redux';
7 8
 import { parseURIString } from '../util';

+ 1
- 1
react/features/base/known-domains/reducer.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT } from '../../app';
3
+import { APP_WILL_MOUNT } from '../app';
4 4
 import { ReducerRegistry } from '../redux';
5 5
 import { PersistenceRegistry } from '../storage';
6 6
 

+ 1
- 1
react/features/base/logging/middleware.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import Logger from 'jitsi-meet-logger';
4 4
 
5
-import { APP_WILL_MOUNT } from '../../app';
5
+import { APP_WILL_MOUNT } from '../app';
6 6
 import JitsiMeetJS, { LIB_WILL_INIT } from '../lib-jitsi-meet';
7 7
 import { MiddlewareRegistry } from '../redux';
8 8
 

+ 1
- 1
react/features/base/participants/middleware.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
4 4
 import {
5 5
     CONFERENCE_WILL_JOIN,
6 6
     forEachConference,

+ 1
- 2
react/features/base/settings/reducer.js Visa fil

@@ -1,8 +1,7 @@
1 1
 // @flow
2 2
 import _ from 'lodash';
3 3
 
4
-import { APP_WILL_MOUNT } from '../../app';
5
-
4
+import { APP_WILL_MOUNT } from '../app';
6 5
 import JitsiMeetJS, { browser } from '../lib-jitsi-meet';
7 6
 import { ReducerRegistry } from '../redux';
8 7
 import { PersistenceRegistry } from '../storage';

+ 1
- 1
react/features/calendar-sync/middleware.js Visa fil

@@ -3,7 +3,7 @@
3 3
 import md5 from 'js-md5';
4 4
 import RNCalendarEvents from 'react-native-calendar-events';
5 5
 
6
-import { APP_WILL_MOUNT } from '../app';
6
+import { APP_WILL_MOUNT } from '../base/app';
7 7
 import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains';
8 8
 import { MiddlewareRegistry } from '../base/redux';
9 9
 import { APP_LINK_SCHEME, parseURIString } from '../base/util';

+ 1
- 1
react/features/calendar-sync/reducer.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT } from '../app';
3
+import { APP_WILL_MOUNT } from '../base/app';
4 4
 import { ReducerRegistry, set } from '../base/redux';
5 5
 import { PersistenceRegistry } from '../base/storage';
6 6
 

+ 1
- 1
react/features/chat/middleware.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4 4
 import { CONFERENCE_JOINED } from '../base/conference';
5 5
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6 6
 import { MiddlewareRegistry } from '../base/redux';

+ 1
- 1
react/features/invite/functions.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { getAppProp } from '../app';
3
+import { getAppProp } from '../base/app';
4 4
 import { isLocalParticipantModerator } from '../base/participants';
5 5
 import { doGetJSON } from '../base/util';
6 6
 

+ 1
- 1
react/features/invite/middleware.any.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4 4
 import {
5 5
     CONFERENCE_JOINED
6 6
 } from '../base/conference';

+ 1
- 1
react/features/invite/middleware.native.js Visa fil

@@ -3,8 +3,8 @@
3 3
 import i18next from 'i18next';
4 4
 import { NativeEventEmitter, NativeModules } from 'react-native';
5 5
 
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, getAppProp } from '../base/app';
6 7
 import { MiddlewareRegistry } from '../base/redux';
7
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, getAppProp } from '../app';
8 8
 
9 9
 import { invite } from './actions';
10 10
 import {

+ 1
- 1
react/features/mobile/audio-mode/middleware.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { NativeModules } from 'react-native';
4 4
 
5
-import { APP_WILL_MOUNT } from '../../app';
5
+import { APP_WILL_MOUNT } from '../../base/app';
6 6
 import {
7 7
     CONFERENCE_FAILED,
8 8
     CONFERENCE_LEFT,

+ 1
- 1
react/features/mobile/background/middleware.js Visa fil

@@ -3,7 +3,7 @@
3 3
 import { AppState } from 'react-native';
4 4
 import type { Dispatch } from 'redux';
5 5
 
6
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
7 7
 import { MiddlewareRegistry } from '../../base/redux';
8 8
 
9 9
 import {

+ 2
- 6
react/features/mobile/callkit/middleware.js Visa fil

@@ -3,12 +3,8 @@
3 3
 import uuid from 'uuid';
4 4
 
5 5
 import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
6
-import {
7
-    APP_WILL_MOUNT,
8
-    APP_WILL_UNMOUNT,
9
-    appNavigate,
10
-    getName
11
-} from '../../app';
6
+import { appNavigate, getName } from '../../app';
7
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
12 8
 import {
13 9
     CONFERENCE_FAILED,
14 10
     CONFERENCE_LEFT,

+ 1
- 1
react/features/mobile/external-api/middleware.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { NativeModules } from 'react-native';
4 4
 
5
-import { getAppProp } from '../../app';
5
+import { getAppProp } from '../../base/app';
6 6
 import {
7 7
     CONFERENCE_FAILED,
8 8
     CONFERENCE_JOINED,

+ 1
- 1
react/features/mobile/full-screen/middleware.js Visa fil

@@ -3,7 +3,7 @@
3 3
 import { StatusBar } from 'react-native';
4 4
 import { Immersive } from 'react-native-immersive';
5 5
 
6
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
7 7
 import { getCurrentConference } from '../../base/conference';
8 8
 import { Platform } from '../../base/react';
9 9
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';

+ 1
- 1
react/features/mobile/image-cache/middleware.js Visa fil

@@ -1,6 +1,6 @@
1 1
 /* @flow */
2 2
 
3
-import { APP_WILL_MOUNT } from '../../app';
3
+import { APP_WILL_MOUNT } from '../../base/app';
4 4
 import {
5 5
     getAvatarURL,
6 6
     getLocalParticipant,

+ 1
- 1
react/features/mobile/network-activity/middleware.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
4 4
 
5
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
5
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
6 6
 import { MiddlewareRegistry } from '../../base/redux';
7 7
 
8 8
 import {

+ 1
- 1
react/features/mobile/picture-in-picture/actions.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { NativeModules } from 'react-native';
4 4
 
5
-import { getAppProp } from '../../app';
5
+import { getAppProp } from '../../base/app';
6 6
 import { Platform } from '../../base/react';
7 7
 
8 8
 import { ENTER_PICTURE_IN_PICTURE } from './actionTypes';

+ 1
- 1
react/features/mobile/picture-in-picture/components/PictureInPictureButton.js Visa fil

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { connect } from 'react-redux';
4 4
 
5
-import { getAppProp } from '../../../app';
5
+import { getAppProp } from '../../../base/app';
6 6
 import { translate } from '../../../base/i18n';
7 7
 import { AbstractButton } from '../../../base/toolbox';
8 8
 import type { AbstractButtonProps } from '../../../base/toolbox';

+ 1
- 1
react/features/recent-list/middleware.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT } from '../app';
3
+import { APP_WILL_MOUNT } from '../base/app';
4 4
 import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
5 5
 import { addKnownDomains } from '../base/known-domains';
6 6
 import { MiddlewareRegistry } from '../base/redux';

+ 1
- 1
react/features/recent-list/reducer.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { APP_WILL_MOUNT } from '../app';
3
+import { APP_WILL_MOUNT } from '../base/app';
4 4
 import { getURLWithoutParamsNormalized } from '../base/connection';
5 5
 import { ReducerRegistry } from '../base/redux';
6 6
 import { PersistenceRegistry } from '../base/storage';

+ 1
- 2
react/features/recording/middleware.js Visa fil

@@ -1,5 +1,6 @@
1 1
 /* @flow */
2 2
 
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
3 4
 import { CONFERENCE_WILL_JOIN, getCurrentConference } from '../base/conference';
4 5
 import JitsiMeetJS, {
5 6
     JitsiConferenceEvents,
@@ -13,8 +14,6 @@ import {
13 14
     unregisterSound
14 15
 } from '../base/sounds';
15 16
 
16
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
17
-
18 17
 import {
19 18
     clearRecordingSessions,
20 19
     hidePendingRecordingNotification,

+ 1
- 1
react/features/welcome/functions.js Visa fil

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { getAppProp } from '../app';
3
+import { getAppProp } from '../base/app';
4 4
 import { toState } from '../base/redux';
5 5
 
6 6
 declare var APP: Object;

Laddar…
Avbryt
Spara