Bläddra i källkod

Improve persistency layer

master
zbettenbuk 7 år sedan
förälder
incheckning
158cadf4f9

+ 4
- 2
react/features/app/components/AbstractApp.js Visa fil

@@ -14,8 +14,8 @@ import {
14 14
 } from '../../base/participants';
15 15
 import { Fragment, RouteRegistry } from '../../base/react';
16 16
 import {
17
-    getPersistedState,
18 17
     MiddlewareRegistry,
18
+    PersistencyRegistry,
19 19
     ReducerRegistry
20 20
 } from '../../base/redux';
21 21
 import { getProfile } from '../../base/profile';
@@ -346,7 +346,9 @@ export class AbstractApp extends Component {
346 346
             middleware = compose(middleware, devToolsExtension());
347 347
         }
348 348
 
349
-        return createStore(reducer, getPersistedState(), middleware);
349
+        return createStore(
350
+            reducer, PersistencyRegistry.getPersistedState(), middleware
351
+        );
350 352
     }
351 353
 
352 354
     /**

+ 5
- 1
react/features/base/profile/reducer.js Visa fil

@@ -4,7 +4,7 @@ import {
4 4
     PROFILE_UPDATED
5 5
 } from './actionTypes';
6 6
 
7
-import { ReducerRegistry } from '../redux';
7
+import { PersistencyRegistry, ReducerRegistry } from '../redux';
8 8
 
9 9
 const DEFAULT_STATE = {
10 10
     profile: {}
@@ -12,6 +12,10 @@ const DEFAULT_STATE = {
12 12
 
13 13
 const STORE_NAME = 'features/base/profile';
14 14
 
15
+PersistencyRegistry.register(STORE_NAME, {
16
+    profile: true
17
+});
18
+
15 19
 ReducerRegistry.register(
16 20
     STORE_NAME, (state = DEFAULT_STATE, action) => {
17 21
         switch (action.type) {

+ 167
- 0
react/features/base/redux/PersistencyRegistry.js Visa fil

@@ -0,0 +1,167 @@
1
+// @flow
2
+import Logger from 'jitsi-meet-logger';
3
+import md5 from 'js-md5';
4
+
5
+const logger = Logger.getLogger(__filename);
6
+
7
+/**
8
+ * The name of the localStorage store where the app persists its values to.
9
+ */
10
+const PERSISTED_STATE_NAME = 'jitsi-state';
11
+
12
+/**
13
+ * The type of the name-config pairs stored in this reducer.
14
+ */
15
+declare type PersistencyConfigMap = { [name: string]: Object };
16
+
17
+/**
18
+ * A registry to allow features to register their redux store
19
+ * subtree to be persisted and also handles the persistency calls too.
20
+ */
21
+class PersistencyRegistry {
22
+    _checksum: string;
23
+    _elements: PersistencyConfigMap;
24
+
25
+    /**
26
+     * Initiates the PersistencyRegistry.
27
+     */
28
+    constructor() {
29
+        this._elements = {};
30
+    }
31
+
32
+    /**
33
+     * Returns the persisted redux state. This function takes
34
+     * the PersistencyRegistry._elements into account as we may have
35
+     * persisted something in the past that we don't want to retreive anymore.
36
+     * The next {@link #persistState} will remove those values.
37
+     *
38
+     * @returns {Object}
39
+     */
40
+    getPersistedState() {
41
+        let filteredPersistedState = {};
42
+        let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME);
43
+
44
+        if (persistedState) {
45
+            try {
46
+                persistedState = JSON.parse(persistedState);
47
+            } catch (error) {
48
+                logger.error(
49
+                    'Error parsing persisted state', persistedState, error
50
+                );
51
+                persistedState = {};
52
+            }
53
+
54
+            filteredPersistedState
55
+                = this._getFilteredState(persistedState);
56
+        }
57
+
58
+        this._checksum = this._calculateChecksum(filteredPersistedState);
59
+        logger.info('Redux state rehydrated as', filteredPersistedState);
60
+
61
+        return filteredPersistedState;
62
+    }
63
+
64
+    /**
65
+     * Initiates a persist operation, but its execution will depend on
66
+     * the current checksums (checks changes).
67
+     *
68
+     * @param {Object} state - The redux state.
69
+     * @returns {void}
70
+     */
71
+    persistState(state: Object) {
72
+        const filteredState = this._getFilteredState(state);
73
+        const newCheckSum = this._calculateChecksum(filteredState);
74
+
75
+        if (newCheckSum !== this._checksum) {
76
+            try {
77
+                window.localStorage.setItem(
78
+                    PERSISTED_STATE_NAME,
79
+                    JSON.stringify(filteredState)
80
+                );
81
+                logger.info(
82
+                    `Redux state persisted. ${this._checksum} -> ${newCheckSum}`
83
+                );
84
+                this._checksum = newCheckSum;
85
+            } catch (error) {
86
+                logger.error('Error persisting Redux state', error);
87
+            }
88
+        }
89
+    }
90
+
91
+    /**
92
+     * Registers a new subtree config to be used for the persistency.
93
+     *
94
+     * @param {string} name - The name of the subtree the config belongs to.
95
+     * @param {Object} config - The config object.
96
+     * @returns {void}
97
+     */
98
+    register(name: string, config: Object) {
99
+        this._elements[name] = config;
100
+    }
101
+
102
+    /**
103
+     * Calculates the checksum of the current or the new values of the state.
104
+     *
105
+     * @private
106
+     * @param {Object} filteredState - The filtered/persisted Redux state.
107
+     * @returns {string}
108
+     */
109
+    _calculateChecksum(filteredState: Object) {
110
+        try {
111
+            return md5.hex(JSON.stringify(filteredState) || '');
112
+        } catch (error) {
113
+            logger.error(
114
+                'Error calculating checksum for state', filteredState, error
115
+            );
116
+
117
+            return '';
118
+        }
119
+    }
120
+
121
+    /**
122
+     * Prepares a filtered state from the actual or the
123
+     * persisted Redux state, based on this registry.
124
+     *
125
+     * @private
126
+     * @param {Object} state - The actual or persisted redux state.
127
+     * @returns {Object}
128
+     */
129
+    _getFilteredState(state: Object) {
130
+        const filteredState = {};
131
+
132
+        for (const name of Object.keys(this._elements)) {
133
+            if (state[name]) {
134
+                filteredState[name] = this._getFilteredSubtree(
135
+                    state[name],
136
+                    this._elements[name]
137
+                );
138
+            }
139
+        }
140
+
141
+        return filteredState;
142
+    }
143
+
144
+    /**
145
+     * Prepares a filtered subtree based on the config for
146
+     * persisting or for retreival.
147
+     *
148
+     * @private
149
+     * @param {Object} subtree - The redux state subtree.
150
+     * @param {Object} subtreeConfig - The related config.
151
+     * @returns {Object}
152
+     */
153
+    _getFilteredSubtree(subtree, subtreeConfig) {
154
+        const filteredSubtree = {};
155
+
156
+        for (const persistedKey of Object.keys(subtree)) {
157
+            if (subtreeConfig[persistedKey]) {
158
+                filteredSubtree[persistedKey]
159
+                    = subtree[persistedKey];
160
+            }
161
+        }
162
+
163
+        return filteredSubtree;
164
+    }
165
+}
166
+
167
+export default new PersistencyRegistry();

+ 0
- 93
react/features/base/redux/functions.js Visa fil

@@ -1,12 +1,6 @@
1 1
 /* @flow */
2 2
 
3 3
 import _ from 'lodash';
4
-import Logger from 'jitsi-meet-logger';
5
-
6
-import persisterConfig from './persisterconfig.json';
7
-
8
-const logger = Logger.getLogger(__filename);
9
-const PERSISTED_STATE_NAME = 'jitsi-state';
10 4
 
11 5
 /**
12 6
  * Sets specific properties of a specific state to specific values and prevents
@@ -44,93 +38,6 @@ export function equals(a: any, b: any) {
44 38
     return _.isEqual(a, b);
45 39
 }
46 40
 
47
-/**
48
- * Prepares a filtered state-slice (Redux term) based on the config for
49
- * persisting or for retreival.
50
- *
51
- * @private
52
- * @param {Object} persistedSlice - The redux state-slice.
53
- * @param {Object} persistedSliceConfig - The related config sub-tree.
54
- * @returns {Object}
55
- */
56
-function _getFilteredSlice(persistedSlice, persistedSliceConfig) {
57
-    const filteredpersistedSlice = {};
58
-
59
-    for (const persistedKey of Object.keys(persistedSlice)) {
60
-        if (persistedSliceConfig[persistedKey]) {
61
-            filteredpersistedSlice[persistedKey] = persistedSlice[persistedKey];
62
-        }
63
-    }
64
-
65
-    return filteredpersistedSlice;
66
-}
67
-
68
-/**
69
- * Prepares a filtered state from the actual or the
70
- * persisted Redux state, based on the config.
71
- *
72
- * @private
73
- * @param {Object} state - The actual or persisted redux state.
74
- * @returns {Object}
75
- */
76
-function _getFilteredState(state: Object) {
77
-    const filteredState = {};
78
-
79
-    for (const slice of Object.keys(persisterConfig)) {
80
-        filteredState[slice] = _getFilteredSlice(
81
-            state[slice],
82
-            persisterConfig[slice]
83
-        );
84
-    }
85
-
86
-    return filteredState;
87
-}
88
-
89
-/**
90
- *  Returns the persisted redux state. This function takes
91
- * the persisterConfig into account as we may have persisted something
92
- * in the past that we don't want to retreive anymore. The next
93
- * {@link #persistState} will remove those values.
94
- *
95
- * @returns {Object}
96
- */
97
-export function getPersistedState() {
98
-    let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME);
99
-
100
-    if (persistedState) {
101
-        try {
102
-            persistedState = JSON.parse(persistedState);
103
-        } catch (error) {
104
-            return {};
105
-        }
106
-
107
-        const filteredPersistedState = _getFilteredState(persistedState);
108
-
109
-        logger.info('Redux state rehydrated', filteredPersistedState);
110
-
111
-        return filteredPersistedState;
112
-    }
113
-
114
-    return {};
115
-}
116
-
117
-/**
118
- * Persists a filtered subtree of the redux state into {@code localStorage}.
119
- *
120
- * @param {Object} state - The redux state.
121
- * @returns {void}
122
- */
123
-export function persistState(state: Object) {
124
-    const filteredState = _getFilteredState(state);
125
-
126
-    window.localStorage.setItem(
127
-        PERSISTED_STATE_NAME,
128
-        JSON.stringify(filteredState)
129
-    );
130
-
131
-    logger.info('Redux state persisted');
132
-}
133
-
134 41
 /**
135 42
  * Sets a specific property of a specific state to a specific value. Prevents
136 43
  * unnecessary state changes (when the specified {@code value} is equal to the

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

@@ -1,5 +1,6 @@
1 1
 export * from './functions';
2 2
 export { default as MiddlewareRegistry } from './MiddlewareRegistry';
3
+export { default as PersistencyRegistry } from './PersistencyRegistry';
3 4
 export { default as ReducerRegistry } from './ReducerRegistry';
4 5
 
5 6
 import './middleware';

+ 2
- 2
react/features/base/redux/middleware.js Visa fil

@@ -1,8 +1,8 @@
1 1
 /* @flow */
2 2
 import _ from 'lodash';
3 3
 
4
-import { persistState } from './functions';
5 4
 import MiddlewareRegistry from './MiddlewareRegistry';
5
+import PersistencyRegistry from './PersistencyRegistry';
6 6
 
7 7
 import { toState } from '../redux';
8 8
 
@@ -16,7 +16,7 @@ const PERSIST_DELAY = 2000;
16 16
  * A throttled function to avoid repetitive state persisting.
17 17
  */
18 18
 const throttledFunc = _.throttle(state => {
19
-    persistState(state);
19
+    PersistencyRegistry.persistState(state);
20 20
 }, PERSIST_DELAY);
21 21
 
22 22
 /**

+ 0
- 5
react/features/base/redux/persisterconfig.json Visa fil

@@ -1,5 +0,0 @@
1
-{
2
-    "features/base/profile": {
3
-        "profile": true
4
-    }
5
-}

+ 13
- 19
react/features/base/redux/readme.md Visa fil

@@ -1,30 +1,24 @@
1 1
 Jitsi Meet - redux state persistency
2 2
 ====================================
3
-Jitsi Meet has a persistency layer that persist a subtree (or specific subtrees) into window.localStorage (on web) or
3
+Jitsi Meet has a persistency layer that persists a subtree (or specific subtrees) into window.localStorage (on web) or
4 4
 AsyncStorage (on mobile).
5 5
 
6 6
 Usage
7 7
 =====
8
-If a subtree of the redux store should be persisted (e.g. ``'features/base/participants'``), then persistency for that
9
-subtree should be enabled in the config file by creating a key in
8
+If a subtree of the redux store should be persisted (e.g. ``'features/base/profile'``), then persistency for that
9
+subtree should be requested by registering the subtree (and related config) into PersistencyRegistry.
10 10
 
11
+E.g. to register the field ``profile`` of the Redux subtree ``'features/base/profile'`` to be persisted, use:
12
+
13
+```JavaScript
14
+PersistencyRegistry.register('features/base/profile', {
15
+    profile: true
16
+});
11 17
 ```
12
-react/features/base/redux/persisterconfig.json
13
-```
14
-and defining all the fields of the subtree that has to be persisted, e.g.:
15
-```json
16
-{
17
-    "features/base/participants": {
18
-        "avatarID": true,
19
-        "avatarURL": true,
20
-        "name": true
21
-    },
22
-    "another/subtree": {
23
-        "someField": true
24
-    }
25
-}
26
-```
27
-When it's done, Jitsi Meet will persist these subtrees/fields and rehidrate them on startup.
18
+
19
+in the ``reducer.js`` of the ``profile`` feature.
20
+
21
+When it's done, Jitsi Meet will automatically persist these subtrees/fields and rehidrate them on startup.
28 22
 
29 23
 Throttling
30 24
 ==========

+ 11
- 0
react/features/recent-list/actionTypes.js Visa fil

@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+/**
4
+ * Action type to signal a new addition to the list.
5
+ */
6
+export const STORE_CURRENT_CONFERENCE = Symbol('STORE_CURRENT_CONFERENCE');
7
+
8
+/**
9
+ * Action type to signal that a new conference duration info is available.
10
+ */
11
+export const UPDATE_CONFERENCE_DURATION = Symbol('UPDATE_CONFERENCE_DURATION');

+ 38
- 0
react/features/recent-list/actions.js Visa fil

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import {
4
+    STORE_CURRENT_CONFERENCE,
5
+    UPDATE_CONFERENCE_DURATION
6
+} from './actionTypes';
7
+
8
+/**
9
+ * Action to initiate a new addition to the list.
10
+ *
11
+ * @param {Object} locationURL - The current location URL.
12
+ * @returns {{
13
+ *      type: STORE_CURRENT_CONFERENCE,
14
+ *      locationURL: Object
15
+ * }}
16
+ */
17
+export function storeCurrentConference(locationURL: Object) {
18
+    return {
19
+        type: STORE_CURRENT_CONFERENCE,
20
+        locationURL
21
+    };
22
+}
23
+
24
+/**
25
+ * Action to initiate the update of the duration of the last conference.
26
+ *
27
+ * @param {Object} locationURL - The current location URL.
28
+ * @returns {{
29
+ *      type: UPDATE_CONFERENCE_DURATION,
30
+ *      locationURL: Object
31
+ * }}
32
+ */
33
+export function updateConferenceDuration(locationURL: Object) {
34
+    return {
35
+        type: UPDATE_CONFERENCE_DURATION,
36
+        locationURL
37
+    };
38
+}

+ 17
- 53
react/features/recent-list/components/AbstractRecentList.js Visa fil

@@ -1,12 +1,9 @@
1 1
 // @flow
2 2
 
3 3
 import { Component } from 'react';
4
-import { ListView } from 'react-native';
5 4
 
6 5
 import { appNavigate } from '../../app';
7 6
 
8
-import { getRecentRooms } from '../functions';
9
-
10 7
 /**
11 8
  * The type of the React {@code Component} props of {@link AbstractRecentList}
12 9
  */
@@ -18,19 +15,6 @@ type Props = {
18 15
     dispatch: Dispatch<*>
19 16
 };
20 17
 
21
-/**
22
- * The type of the React {@code Component} state of {@link AbstractRecentList}.
23
- */
24
-type State = {
25
-
26
-    /**
27
-     * The {@code ListView.DataSource} to be used for the {@code ListView}. Its
28
-     * content comes from the native implementation of
29
-     * {@code window.localStorage}.
30
-     */
31
-    dataSource: Object
32
-};
33
-
34 18
 /**
35 19
  * Implements a React {@link Component} which represents the list of conferences
36 20
  * recently joined, similar to how a list of last dialed numbers list would do
@@ -38,43 +22,7 @@ type State = {
38 22
  *
39 23
  * @extends Component
40 24
  */
41
-export default class AbstractRecentList extends Component<Props, State> {
42
-
43
-    /**
44
-     * The datasource that backs the {@code ListView}.
45
-     */
46
-    listDataSource = new ListView.DataSource({
47
-        rowHasChanged: (r1, r2) =>
48
-            r1.conference !== r2.conference
49
-                && r1.dateTimeStamp !== r2.dateTimeStamp
50
-    });
51
-
52
-    /**
53
-     * Initializes a new {@code AbstractRecentList} instance.
54
-     */
55
-    constructor() {
56
-        super();
57
-
58
-        this.state = {
59
-            dataSource: this.listDataSource.cloneWithRows([])
60
-        };
61
-    }
62
-
63
-    /**
64
-     * Implements React's {@link Component#componentWillMount()}. Invoked
65
-     * immediately before mounting occurs.
66
-     *
67
-     * @inheritdoc
68
-     */
69
-    componentWillMount() {
70
-        // The following must be done asynchronously because we don't have the
71
-        // storage initiated on app startup immediately.
72
-        getRecentRooms()
73
-            .then(rooms =>
74
-                this.setState({
75
-                    dataSource: this.listDataSource.cloneWithRows(rooms)
76
-                }));
77
-    }
25
+export default class AbstractRecentList extends Component<Props> {
78 26
 
79 27
     /**
80 28
      * Joins the selected room.
@@ -96,3 +44,19 @@ export default class AbstractRecentList extends Component<Props, State> {
96 44
         return this._onJoin.bind(this, room);
97 45
     }
98 46
 }
47
+
48
+/**
49
+ * Maps Redux state to component props.
50
+ *
51
+ * @param {Object} state - The redux state.
52
+ * @returns {{
53
+ *      _homeServer: string,
54
+ *      _recentList: Array
55
+ * }}
56
+ */
57
+export function _mapStateToProps(state: Object) {
58
+    return {
59
+        _homeServer: state['features/app'].app._getDefaultURL(),
60
+        _recentList: state['features/recent-list'].list
61
+    };
62
+}

+ 22
- 27
react/features/recent-list/components/RecentList.native.js Visa fil

@@ -2,19 +2,32 @@ import React from 'react';
2 2
 import { ListView, Text, TouchableHighlight, View } from 'react-native';
3 3
 import { connect } from 'react-redux';
4 4
 
5
-import { Icon } from '../../base/font-icons';
6
-
7
-import AbstractRecentList from './AbstractRecentList';
5
+import AbstractRecentList, { _mapStateToProps } from './AbstractRecentList';
8 6
 import styles, { UNDERLAY_COLOR } from './styles';
9 7
 
8
+import { getRecentRooms } from '../functions';
9
+
10
+import { Icon } from '../../base/font-icons';
11
+
10 12
 /**
11 13
  * The native container rendering the list of the recently joined rooms.
12 14
  *
13 15
  * @extends AbstractRecentList
14 16
  */
15 17
 class RecentList extends AbstractRecentList {
18
+    /**
19
+     * The datasource wrapper to be used for the display.
20
+     */
21
+    dataSource = new ListView.DataSource({
22
+        rowHasChanged: (r1, r2) =>
23
+            r1.conference !== r2.conference
24
+                && r1.dateTimeStamp !== r2.dateTimeStamp
25
+    });
26
+
16 27
     /**
17 28
      * Initializes a new {@code RecentList} instance.
29
+     *
30
+     * @inheritdoc
18 31
      */
19 32
     constructor() {
20 33
         super();
@@ -35,14 +48,18 @@ class RecentList extends AbstractRecentList {
35 48
      * @returns {ReactElement}
36 49
      */
37 50
     render() {
38
-        if (!this.state.dataSource.getRowCount()) {
51
+        if (!this.props || !this.props._recentList) {
39 52
             return null;
40 53
         }
41 54
 
55
+        const listViewDataSource = this.dataSource.cloneWithRows(
56
+            getRecentRooms(this.props._recentList)
57
+        );
58
+
42 59
         return (
43 60
             <View style = { styles.container }>
44 61
                 <ListView
45
-                    dataSource = { this.state.dataSource }
62
+                    dataSource = { listViewDataSource }
46 63
                     enableEmptySections = { true }
47 64
                     renderRow = { this._renderRow } />
48 65
             </View>
@@ -182,26 +199,4 @@ class RecentList extends AbstractRecentList {
182 199
     }
183 200
 }
184 201
 
185
-/**
186
- * Maps (parts of) the Redux state to the associated RecentList's props.
187
- *
188
- * @param {Object} state - The Redux state.
189
- * @private
190
- * @returns {{
191
- *     _homeServer: string
192
- * }}
193
- */
194
-function _mapStateToProps(state) {
195
-    return {
196
-        /**
197
-         * The default server name based on which we determine the render
198
-         * method.
199
-         *
200
-         * @private
201
-         * @type {string}
202
-         */
203
-        _homeServer: state['features/app'].app._getDefaultURL()
204
-    };
205
-}
206
-
207 202
 export default connect(_mapStateToProps)(RecentList);

+ 43
- 78
react/features/recent-list/functions.js Visa fil

@@ -5,8 +5,6 @@ import moment from 'moment';
5 5
 import { i18next } from '../base/i18n';
6 6
 import { parseURIString } from '../base/util';
7 7
 
8
-import { RECENT_URL_STORAGE } from './constants';
9
-
10 8
 /**
11 9
  * MomentJS uses static language bundle loading, so in order to support dynamic
12 10
  * language selection in the app we need to load all bundles that we support in
@@ -36,76 +34,43 @@ require('moment/locale/tr');
36 34
 require('moment/locale/zh-cn');
37 35
 
38 36
 /**
39
- * Retreives the recent room list and generates all the data needed to be
37
+ * Retrieves the recent room list and generates all the data needed to be
40 38
  * displayed.
41 39
  *
42
- * @returns {Promise} The {@code Promise} to be resolved when the list is
43
- * available.
40
+ * @param {Array<Object>} list - The stored recent list retrieved from Redux.
41
+ * @returns {Array}
44 42
  */
45
-export function getRecentRooms(): Promise<Array<Object>> {
46
-    return new Promise((resolve, reject) =>
47
-        window.localStorage._getItemAsync(RECENT_URL_STORAGE).then(
48
-            /* onFulfilled */ recentURLs => {
49
-                const recentRoomDS = [];
50
-
51
-                if (recentURLs) {
52
-                    // We init the locale on every list render, so then it
53
-                    // changes immediately if a language change happens in the
54
-                    // app.
55
-                    const locale = _getSupportedLocale();
56
-
57
-                    for (const e of JSON.parse(recentURLs)) {
58
-                        const location = parseURIString(e.conference);
59
-
60
-                        if (location && location.room && location.hostname) {
61
-                            recentRoomDS.push({
62
-                                baseURL:
63
-                                    `${location.protocol}//${location.host}`,
64
-                                conference: e.conference,
65
-                                conferenceDuration: e.conferenceDuration,
66
-                                conferenceDurationString:
67
-                                    _getDurationString(
68
-                                        e.conferenceDuration,
69
-                                        locale
70
-                                    ),
71
-                                dateString: _getDateString(e.date, locale),
72
-                                dateTimeStamp: e.date,
73
-                                initials: _getInitials(location.room),
74
-                                room: location.room,
75
-                                serverName: location.hostname
76
-                            });
77
-                        }
78
-                    }
79
-                }
80
-
81
-                resolve(recentRoomDS.reverse());
82
-            },
83
-            /* onRejected */ reject)
84
-    );
85
-}
86
-
87
-/**
88
- * Retreives the recent URL list as a list of objects.
89
- *
90
- * @returns {Array} The list of already stored recent URLs.
91
- */
92
-export function getRecentURLs() {
93
-    const recentURLs = window.localStorage.getItem(RECENT_URL_STORAGE);
94
-
95
-    return recentURLs ? JSON.parse(recentURLs) : [];
96
-}
43
+export function getRecentRooms(list: Array<Object>): Array<Object> {
44
+    const recentRoomDS = [];
45
+
46
+    if (list.length) {
47
+        // We init the locale on every list render, so then it changes
48
+        // immediately if a language change happens in the app.
49
+        const locale = _getSupportedLocale();
50
+
51
+        for (const e of list) {
52
+            const location = parseURIString(e.conference);
53
+
54
+            if (location && location.room && location.hostname) {
55
+                recentRoomDS.push({
56
+                    baseURL: `${location.protocol}//${location.host}`,
57
+                    conference: e.conference,
58
+                    conferenceDuration: e.conferenceDuration,
59
+                    conferenceDurationString:
60
+                        _getDurationString(
61
+                            e.conferenceDuration,
62
+                            locale),
63
+                    dateString: _getDateString(e.date, locale),
64
+                    dateTimeStamp: e.date,
65
+                    initials: _getInitials(location.room),
66
+                    room: location.room,
67
+                    serverName: location.hostname
68
+                });
69
+            }
70
+        }
71
+    }
97 72
 
98
-/**
99
- * Updates the recent URL list.
100
- *
101
- * @param {Array} recentURLs - The new URL list.
102
- * @returns {void}
103
- */
104
-export function updateRecentURLs(recentURLs: Array<Object>) {
105
-    window.localStorage.setItem(
106
-        RECENT_URL_STORAGE,
107
-        JSON.stringify(recentURLs)
108
-    );
73
+    return recentRoomDS.reverse();
109 74
 }
110 75
 
111 76
 /**
@@ -142,8 +107,7 @@ function _getDateString(dateTimeStamp: number, locale: string) {
142 107
  * @returns {string}
143 108
  */
144 109
 function _getDurationString(duration: number, locale: string) {
145
-    return _getLocalizedFormatter(duration, locale)
146
-            .humanize();
110
+    return _getLocalizedFormatter(duration, locale).humanize();
147 111
 }
148 112
 
149 113
 /**
@@ -158,22 +122,23 @@ function _getInitials(room: string) {
158 122
 }
159 123
 
160 124
 /**
161
- * Returns a localized date formatter initialized with the
162
- * provided date (@code Date) or duration (@code Number).
125
+ * Returns a localized date formatter initialized with the provided date
126
+ * (@code Date) or duration (@code number).
163 127
  *
164 128
  * @private
165
- * @param {Date | number} dateToFormat - The date or duration to format.
129
+ * @param {Date | number} dateOrDuration - The date or duration to format.
166 130
  * @param {string} locale - The locale to init the formatter with. Note: This
167 131
  * locale must be supported by the formatter so ensure this prerequisite before
168 132
  * invoking the function.
169 133
  * @returns {Object}
170 134
  */
171
-function _getLocalizedFormatter(dateToFormat: Date | number, locale: string) {
172
-    if (typeof dateToFormat === 'number') {
173
-        return moment.duration(dateToFormat).locale(locale);
174
-    }
135
+function _getLocalizedFormatter(dateOrDuration: Date | number, locale: string) {
136
+    const m
137
+        = typeof dateOrDuration === 'number'
138
+            ? moment.duration(dateOrDuration)
139
+            : moment(dateOrDuration);
175 140
 
176
-    return moment(dateToFormat).locale(locale);
141
+    return m.locale(locale);
177 142
 }
178 143
 
179 144
 /**

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

@@ -1,3 +1,4 @@
1 1
 export * from './components';
2 2
 
3 3
 import './middleware';
4
+import './reducer';

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

@@ -3,8 +3,7 @@
3 3
 import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
4 4
 import { MiddlewareRegistry } from '../base/redux';
5 5
 
6
-import { LIST_SIZE } from './constants';
7
-import { getRecentURLs, updateRecentURLs } from './functions';
6
+import { storeCurrentConference, updateConferenceDuration } from './actions';
8 7
 
9 8
 /**
10 9
  * Middleware that captures joined rooms so they can be saved into
@@ -16,93 +15,47 @@ import { getRecentURLs, updateRecentURLs } from './functions';
16 15
 MiddlewareRegistry.register(store => next => action => {
17 16
     switch (action.type) {
18 17
     case CONFERENCE_WILL_LEAVE:
19
-        return _updateConferenceDuration(store, next, action);
18
+        _updateConferenceDuration(store, next);
19
+        break;
20 20
 
21 21
     case SET_ROOM:
22
-        return _storeJoinedRoom(store, next, action);
22
+        _maybeStoreCurrentConference(store, next, action);
23
+        break;
23 24
     }
24 25
 
25 26
     return next(action);
26 27
 });
27 28
 
28 29
 /**
29
- * Stores the recently joined room into {@code window.localStorage}.
30
+ * Checks if there is a current conference (upon SET_ROOM action), and saves it
31
+ * if necessary.
30 32
  *
31
- * @param {Store} store - The redux store in which the specified action is being
32
- * dispatched.
33
- * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
34
- * specified action to the specified store.
35
- * @param {Action} action - The redux action {@code SET_ROOM} which is being
36
- * dispatched in the specified store.
33
+ * @param {Store} store - The redux store.
34
+ * @param {Dispatch} next - The redux {@code dispatch} function.
35
+ * @param {Action} action - The redux action.
37 36
  * @private
38
- * @returns {Object} The new state that is the result of the reduction of the
39
- * specified action.
37
+ * @returns {void}
40 38
  */
41
-function _storeJoinedRoom(store, next, action) {
42
-    const result = next(action);
43
-
39
+function _maybeStoreCurrentConference(store, next, action) {
40
+    const { locationURL } = store.getState()['features/base/connection'];
44 41
     const { room } = action;
45 42
 
46 43
     if (room) {
47
-        const { locationURL } = store.getState()['features/base/connection'];
48
-        const conference = locationURL.href;
49
-
50
-        // If the current conference is already in the list, we remove it to add
51
-        // it to the top at the end.
52
-        const recentURLs
53
-            = getRecentURLs()
54
-                .filter(e => e.conference !== conference);
55
-
56
-        // XXX This is a reverse sorted array (i.e. newer elements at the end).
57
-        recentURLs.push({
58
-            conference,
59
-            conferenceDuration: 0,
60
-            date: Date.now()
61
-        });
62
-
63
-        // maximising the size
64
-        recentURLs.splice(0, recentURLs.length - LIST_SIZE);
65
-
66
-        updateRecentURLs(recentURLs);
44
+        next(storeCurrentConference(locationURL));
67 45
     }
68
-
69
-    return result;
70 46
 }
71 47
 
72 48
 /**
73
- * Updates the conference length when left.
49
+ * Updates the duration of the last conference stored in the list.
74 50
  *
75
- * @param {Store} store - The redux store in which the specified action is being
76
- * dispatched.
77
- * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
78
- * specified action to the specified store.
79
- * @param {Action} action - The redux action {@code CONFERENCE_WILL_LEAVE} which
80
- * is being dispatched in the specified store.
51
+ * @param {Store} store - The redux store.
52
+ * @param {Dispatch} next - The redux {@code dispatch} function.
53
+ * @param {Action} action - The redux action.
81 54
  * @private
82
- * @returns {Object} The new state that is the result of the reduction of the
83
- * specified action.
55
+ * @returns {void}
84 56
  */
85
-function _updateConferenceDuration({ getState }, next, action) {
86
-    const result = next(action);
87
-
88
-    const { locationURL } = getState()['features/base/connection'];
89
-
90
-    if (locationURL && locationURL.href) {
91
-        const recentURLs = getRecentURLs();
92
-
93
-        if (recentURLs.length > 0) {
94
-            const mostRecentURL = recentURLs[recentURLs.length - 1];
95
-
96
-            if (mostRecentURL.conference === locationURL.href) {
97
-                // The last conference start was stored so we need to update the
98
-                // length.
99
-                mostRecentURL.conferenceDuration
100
-                    = Date.now() - mostRecentURL.date;
101
-
102
-                updateRecentURLs(recentURLs);
103
-            }
104
-        }
105
-    }
57
+function _updateConferenceDuration(store, next) {
58
+    const { locationURL } = store.getState()['features/base/connection'];
106 59
 
107
-    return result;
60
+    next(updateConferenceDuration(locationURL));
108 61
 }

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

@@ -0,0 +1,106 @@
1
+// @flow
2
+
3
+import {
4
+    STORE_CURRENT_CONFERENCE,
5
+    UPDATE_CONFERENCE_DURATION
6
+} from './actionTypes';
7
+import { LIST_SIZE } from './constants';
8
+
9
+import { PersistencyRegistry, ReducerRegistry } from '../base/redux';
10
+
11
+/**
12
+ * The initial state of this feature.
13
+ */
14
+const DEFAULT_STATE = {
15
+    list: []
16
+};
17
+
18
+/**
19
+ * The Redux subtree of this feature.
20
+ */
21
+const STORE_NAME = 'features/recent-list';
22
+
23
+/**
24
+ * Registers the redux store subtree of this feature for persistency.
25
+ */
26
+PersistencyRegistry.register(STORE_NAME, {
27
+    list: true
28
+});
29
+
30
+/**
31
+ * Reduces the Redux actions of the feature features/recent-list.
32
+ */
33
+ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
34
+    switch (action.type) {
35
+    case STORE_CURRENT_CONFERENCE:
36
+        return _storeCurrentConference(state, action);
37
+    case UPDATE_CONFERENCE_DURATION:
38
+        return _updateConferenceDuration(state, action);
39
+    default:
40
+        return state;
41
+    }
42
+});
43
+
44
+/**
45
+* Adds a new list entry to the redux store.
46
+*
47
+* @param {Object} state - The redux state.
48
+* @param {Object} action - The redux action.
49
+* @returns {Object}
50
+*/
51
+function _storeCurrentConference(state, action) {
52
+    const { locationURL } = action;
53
+    const conference = locationURL.href;
54
+
55
+    // If the current conference is already in the list, we remove it to re-add
56
+    // it to the top.
57
+    const list
58
+        = state.list
59
+            .filter(e => e.conference !== conference);
60
+
61
+    // This is a reverse sorted array (i.e. newer elements at the end).
62
+    list.push({
63
+        conference,
64
+        conferenceDuration: 0, // we don't have this data yet
65
+        date: Date.now()
66
+    });
67
+
68
+    // maximising the size
69
+    list.splice(0, list.length - LIST_SIZE);
70
+
71
+    return {
72
+        list
73
+    };
74
+}
75
+
76
+/**
77
+ * Updates the conference length when left.
78
+ *
79
+ * @param {Object} state - The redux state.
80
+ * @param {Object} action - The redux action.
81
+ * @returns {Object}
82
+ */
83
+function _updateConferenceDuration(state, action) {
84
+    const { locationURL } = action;
85
+
86
+    if (locationURL && locationURL.href) {
87
+        const list = state.list;
88
+
89
+        if (list.length > 0) {
90
+            const mostRecentURL = list[list.length - 1];
91
+
92
+            if (mostRecentURL.conference === locationURL.href) {
93
+                // The last conference start was stored so we need to update the
94
+                // length.
95
+                mostRecentURL.conferenceDuration
96
+                    = Date.now() - mostRecentURL.date;
97
+
98
+                return {
99
+                    list
100
+                };
101
+            }
102
+        }
103
+    }
104
+
105
+    return state;
106
+}

Laddar…
Avbryt
Spara