浏览代码

[RN] Fix base/profile and recent-list bugs

master
Lyubo Marinov 7 年前
父节点
当前提交
d727ee80b2

+ 4
- 6
react/features/app/actions.js 查看文件

@@ -4,7 +4,6 @@ import { setRoom } from '../base/conference';
4 4
 import { configWillLoad, loadConfigError, setConfig } from '../base/config';
5 5
 import { setLocationURL } from '../base/connection';
6 6
 import { loadConfig } from '../base/lib-jitsi-meet';
7
-import { getProfile } from '../base/profile';
8 7
 import { parseURIString } from '../base/util';
9 8
 
10 9
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
@@ -15,7 +14,7 @@ declare var APP: Object;
15 14
  * Triggers an in-app navigation to a specific route. Allows navigation to be
16 15
  * abstracted between the mobile/React Native and Web/React applications.
17 16
  *
18
- * @param {(string|undefined)} uri - The URI to which to navigate. It may be a
17
+ * @param {string|undefined} uri - The URI to which to navigate. It may be a
19 18
  * full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
20 19
  * scheme, or a mere room name.
21 20
  * @returns {Function}
@@ -83,11 +82,10 @@ function _appNavigateToMandatoryLocation(
83 82
             });
84 83
         }
85 84
 
86
-        const profile = getProfile(getState());
85
+        const profile = getState()['features/base/profile'];
87 86
 
88
-        return promise.then(() => dispatch(setConfig(
89
-            _mergeConfigWithProfile(config, profile)
90
-        )));
87
+        return promise.then(() =>
88
+            dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
91 89
     }
92 90
 }
93 91
 

+ 5
- 4
react/features/app/components/AbstractApp.js 查看文件

@@ -12,7 +12,7 @@ import {
12 12
     localParticipantJoined,
13 13
     localParticipantLeft
14 14
 } from '../../base/participants';
15
-import { getProfile } from '../../base/profile';
15
+import '../../base/profile';
16 16
 import { Fragment, RouteRegistry } from '../../base/react';
17 17
 import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
18 18
 import { PersistenceRegistry } from '../../base/storage';
@@ -123,7 +123,7 @@ export class AbstractApp extends Component {
123 123
      */
124 124
     componentWillMount() {
125 125
         this._init.then(() => {
126
-            const { dispatch } = this._getStore();
126
+            const { dispatch, getState } = this._getStore();
127 127
 
128 128
             dispatch(appWillMount(this));
129 129
 
@@ -144,7 +144,7 @@ export class AbstractApp extends Component {
144 144
             }
145 145
 
146 146
             // Profile is the new React compatible settings.
147
-            const profile = getProfile(this._getStore().getState());
147
+            const profile = getState()['features/base/profile'];
148 148
 
149 149
             if (profile) {
150 150
                 localParticipant.email
@@ -381,7 +381,8 @@ export class AbstractApp extends Component {
381 381
 
382 382
         return (
383 383
             this.props.defaultURL
384
-                || getProfile(this._getStore().getState()).serverURL
384
+                || this._getStore().getState()['features/base/profile']
385
+                    .serverURL
385 386
                 || DEFAULT_URL);
386 387
     }
387 388
 

+ 1
- 1
react/features/base/conference/middleware.js 查看文件

@@ -128,7 +128,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
128 128
 
129 129
     const state = getState();
130 130
     const { audioOnly } = state['features/base/conference'];
131
-    const { startAudioOnly } = state['features/base/profile'].profile;
131
+    const { startAudioOnly } = state['features/base/profile'];
132 132
 
133 133
     // FIXME: Consider implementing a standalone audio-only feature that handles
134 134
     // all these state changes.

+ 0
- 15
react/features/base/profile/functions.js 查看文件

@@ -1,15 +0,0 @@
1
-/* @flow */
2
-
3
-/**
4
- * Retreives the current profile settings from redux store. The profile
5
- * is persisted to localStorage so it's a good candidate to store settings
6
- * in it.
7
- *
8
- * @param {Object} state - The Redux state.
9
- * @returns {Object}
10
- */
11
-export function getProfile(state: Object) {
12
-    const profileStateSlice = state['features/base/profile'];
13
-
14
-    return profileStateSlice || {};
15
-}

+ 0
- 1
react/features/base/profile/index.js 查看文件

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

+ 6
- 7
react/features/base/profile/middleware.js 查看文件

@@ -2,7 +2,6 @@
2 2
 
3 3
 import { setAudioOnly } from '../conference';
4 4
 import { getLocalParticipant, participantUpdated } from '../participants';
5
-import { getProfile } from '../profile';
6 5
 import { MiddlewareRegistry, toState } from '../redux';
7 6
 
8 7
 import { PROFILE_UPDATED } from './actionTypes';
@@ -33,11 +32,11 @@ MiddlewareRegistry.register(store => next => action => {
33 32
  * @param {Object} action - The redux action.
34 33
  * @returns {void}
35 34
  */
36
-function _maybeUpdateStartAudioOnly(store, action) {
37
-    const { profile } = action;
38
-
39
-    if (typeof profile.startAudioOnly === 'boolean') {
40
-        store.dispatch(setAudioOnly(profile.startAudioOnly));
35
+function _maybeUpdateStartAudioOnly(
36
+        { dispatch },
37
+        { profile: { startAudioOnly } }) {
38
+    if (typeof startAudioOnly === 'boolean') {
39
+        dispatch(setAudioOnly(startAudioOnly));
41 40
     }
42 41
 }
43 42
 
@@ -50,7 +49,7 @@ function _maybeUpdateStartAudioOnly(store, action) {
50 49
 function _updateLocalParticipant(store) {
51 50
     const state = toState(store);
52 51
     const localParticipant = getLocalParticipant(state);
53
-    const profile = getProfile(state);
52
+    const profile = state['features/base/profile'];
54 53
 
55 54
     store.dispatch(participantUpdated({
56 55
         // Identify that the participant to update i.e. the local participant:

+ 38
- 11
react/features/base/profile/reducer.js 查看文件

@@ -1,26 +1,53 @@
1 1
 // @flow
2 2
 
3
+import { APP_WILL_MOUNT } from '../../app';
3 4
 import { ReducerRegistry } from '../redux';
4 5
 import { PersistenceRegistry } from '../storage';
5 6
 
6 7
 import { PROFILE_UPDATED } from './actionTypes';
7 8
 
9
+/**
10
+ * The default/initial redux state of the feature {@code base/profile}.
11
+ *
12
+ * @type Object
13
+ */
14
+const DEFAULT_STATE = {};
15
+
8 16
 const STORE_NAME = 'features/base/profile';
9 17
 
10 18
 /**
11
- * Sets up the persistence of the feature base/profile.
19
+ * Sets up the persistence of the feature {@code base/profile}.
12 20
  */
13 21
 PersistenceRegistry.register(STORE_NAME);
14 22
 
15
-ReducerRegistry.register(
16
-    STORE_NAME, (state = {}, action) => {
17
-        switch (action.type) {
18
-        case PROFILE_UPDATED:
19
-            return {
20
-                ...state,
21
-                ...action.profile
22
-            };
23
+ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
24
+    switch (action.type) {
25
+    case APP_WILL_MOUNT:
26
+        // XXX APP_WILL_MOUNT is the earliest redux action of ours dispatched in
27
+        // the store. For the purposes of legacy support, make sure that the
28
+        // deserialized base/profile's state is in the format deemed current by
29
+        // the current app revision.
30
+        if (state && typeof state === 'object') {
31
+            // In an enterprise/internal build of Jitsi Meet for Android and iOS
32
+            // we had base/profile's state as an object with property profile.
33
+            const { profile } = state;
34
+
35
+            if (profile && typeof profile === 'object') {
36
+                return { ...profile };
37
+            }
38
+        } else {
39
+            // In the weird case that we have previously persisted/serialized
40
+            // null.
41
+            return DEFAULT_STATE;
23 42
         }
43
+        break;
44
+
45
+    case PROFILE_UPDATED:
46
+        return {
47
+            ...state,
48
+            ...action.profile
49
+        };
50
+    }
24 51
 
25
-        return state;
26
-    });
52
+    return state;
53
+});

+ 109
- 123
react/features/base/storage/PersistenceRegistry.js 查看文件

@@ -1,25 +1,23 @@
1 1
 // @flow
2 2
 
3
-import Logger from 'jitsi-meet-logger';
4 3
 import md5 from 'js-md5';
5 4
 
6
-const logger = Logger.getLogger(__filename);
5
+const logger = require('jitsi-meet-logger').getLogger(__filename);
7 6
 
8 7
 /**
9
- * The name of the localStorage store where the app persists its values to.
8
+ * The name of the {@code localStorage} store where the app persists its values.
10 9
  */
11 10
 const PERSISTED_STATE_NAME = 'jitsi-state';
12 11
 
13 12
 /**
14
- * Mixed type of the element (subtree) config. If it's a boolean,
15
- * (and is true) we persist the entire subtree. If it's an object,
16
- * we perist a filtered subtree based on the properties in the
17
- * config object.
13
+ * Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
14
+ * {@code true}), we persist the entire subtree. If it's an {@code Object}, we
15
+ * perist a filtered subtree based on the properties of the config object.
18 16
  */
19
-declare type ElementConfig = Object | boolean;
17
+declare type ElementConfig = boolean | Object;
20 18
 
21 19
 /**
22
- * The type of the name-config pairs stored in this reducer.
20
+ * The type of the name-config pairs stored in {@code PersistenceRegistry}.
23 21
  */
24 22
 declare type PersistencyConfigMap = { [name: string]: ElementConfig };
25 23
 
@@ -30,74 +28,64 @@ declare type PersistencyConfigMap = { [name: string]: ElementConfig };
30 28
 class PersistenceRegistry {
31 29
     _checksum: string;
32 30
 
33
-    _elements: PersistencyConfigMap;
31
+    _elements: PersistencyConfigMap = {};
34 32
 
35 33
     /**
36
-     * Initializes a new {@ code PersistenceRegistry} instance.
37
-     */
38
-    constructor() {
39
-        this._elements = {};
40
-    }
41
-
42
-    /**
43
-     * Returns the persisted redux state. This function takes the
44
-     * {@link #_elements} into account as we may have persisted something in the
45
-     * past that we don't want to retreive anymore. The next
46
-     * {@link #persistState} will remove those values.
34
+     * Returns the persisted redux state. Takes the {@link #_elements} into
35
+     * account as we may have persisted something in the past that we don't want
36
+     * to retreive anymore. The next {@link #persistState} will remove such
37
+     * values.
47 38
      *
48 39
      * @returns {Object}
49 40
      */
50 41
     getPersistedState() {
51 42
         let filteredPersistedState = {};
52
-        let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME);
53 43
 
54
-        if (persistedState) {
55
-            // This is the legacy implementation,
56
-            // must be removed in a later version.
57
-            try {
58
-                persistedState = JSON.parse(persistedState);
59
-            } catch (error) {
60
-                logger.error(
61
-                    'Error parsing persisted state',
62
-                    persistedState,
63
-                    error);
64
-                persistedState = {};
44
+        // localStorage key per feature
45
+        for (const subtreeName of Object.keys(this._elements)) {
46
+            // Assumes that the persisted value is stored under the same key as
47
+            // the feature's redux state name.
48
+            // TODO We'll need to introduce functions later that can control the
49
+            // persist key's name. Similar to control serialization and
50
+            // deserialization. But that should be a straightforward change.
51
+            const persistedSubtree
52
+                = this._getPersistedSubtree(
53
+                    subtreeName,
54
+                    this._elements[subtreeName]);
55
+
56
+            if (persistedSubtree !== undefined) {
57
+                filteredPersistedState[subtreeName] = persistedSubtree;
65 58
             }
59
+        }
66 60
 
67
-            filteredPersistedState
68
-                = this._getFilteredState(persistedState);
69
-
70
-            // legacy values must be written to the new store format and
71
-            // old values to be deleted, so then it'll never be used again.
72
-            this.persistState(filteredPersistedState);
73
-            window.localStorage.removeItem(PERSISTED_STATE_NAME);
74
-        } else {
75
-            // new, split-keys implementation
76
-            for (const subtreeName of Object.keys(this._elements)) {
77
-                /*
78
-                 * this assumes that the persisted value is stored under the
79
-                 * same key as the feature's redux state name.
80
-                 * We'll need to introduce functions later that can control
81
-                 * the persist key's name. Similar to control serialization
82
-                 * and deserialization.
83
-                 * But that should be a straightforward change.
84
-                 */
85
-                const persistedSubtree
86
-                    = this._getPersistedSubtree(
87
-                        subtreeName,
88
-                        this._elements[subtreeName]
89
-                    );
61
+        // legacy
62
+        if (Object.keys(filteredPersistedState).length === 0) {
63
+            const { localStorage } = window;
64
+            let persistedState = localStorage.getItem(PERSISTED_STATE_NAME);
90 65
 
91
-                if (persistedSubtree !== undefined) {
92
-                    filteredPersistedState[subtreeName] = persistedSubtree;
66
+            if (persistedState) {
67
+                try {
68
+                    persistedState = JSON.parse(persistedState);
69
+                } catch (error) {
70
+                    logger.error(
71
+                        'Error parsing persisted state',
72
+                        persistedState,
73
+                        error);
74
+                    persistedState = {};
93 75
                 }
76
+
77
+                filteredPersistedState = this._getFilteredState(persistedState);
78
+
79
+                // Store into the new format and delete the old format so that
80
+                // it's not used again.
81
+                this.persistState(filteredPersistedState);
82
+                localStorage.removeItem(PERSISTED_STATE_NAME);
94 83
             }
95 84
         }
96 85
 
97
-        // initialize checksum
86
+        // Initialize the checksum.
98 87
         this._checksum = this._calculateChecksum(filteredPersistedState);
99 88
 
100
-        this._checksum = this._calculateChecksum(filteredPersistedState);
101 89
         logger.info('redux state rehydrated as', filteredPersistedState);
102 90
 
103 91
         return filteredPersistedState;
@@ -112,26 +100,25 @@ class PersistenceRegistry {
112 100
      */
113 101
     persistState(state: Object) {
114 102
         const filteredState = this._getFilteredState(state);
115
-        const newCheckSum = this._calculateChecksum(filteredState);
103
+        const checksum = this._calculateChecksum(filteredState);
116 104
 
117
-        if (newCheckSum !== this._checksum) {
105
+        if (checksum !== this._checksum) {
118 106
             for (const subtreeName of Object.keys(filteredState)) {
119 107
                 try {
120 108
                     window.localStorage.setItem(
121 109
                         subtreeName,
122 110
                         JSON.stringify(filteredState[subtreeName]));
123 111
                 } catch (error) {
124
-                    logger.error('Error persisting redux subtree',
112
+                    logger.error(
113
+                        'Error persisting redux subtree',
125 114
                         subtreeName,
126 115
                         filteredState[subtreeName],
127
-                        error
128
-                    );
116
+                        error);
129 117
                 }
130 118
             }
131 119
             logger.info(
132
-                `redux state persisted. ${this._checksum} -> ${
133
-                    newCheckSum}`);
134
-            this._checksum = newCheckSum;
120
+                `redux state persisted. ${this._checksum} -> ${checksum}`);
121
+            this._checksum = checksum;
135 122
         }
136 123
     }
137 124
 
@@ -139,8 +126,8 @@ class PersistenceRegistry {
139 126
      * Registers a new subtree config to be used for the persistency.
140 127
      *
141 128
      * @param {string} name - The name of the subtree the config belongs to.
142
-     * @param {ElementConfig} config - The config object, or boolean
143
-     * if the entire subtree needs to be persisted.
129
+     * @param {ElementConfig} config - The config {@code Object}, or
130
+     * {@code boolean} if the entire subtree needs to be persisted.
144 131
      * @returns {void}
145 132
      */
146 133
     register(name: string, config?: ElementConfig = true) {
@@ -148,64 +135,28 @@ class PersistenceRegistry {
148 135
     }
149 136
 
150 137
     /**
151
-     * Calculates the checksum of the current or the new values of the state.
138
+     * Calculates the checksum of a specific state.
152 139
      *
140
+     * @param {Object} state - The redux state to calculate the checksum of.
153 141
      * @private
154
-     * @param {Object} filteredState - The filtered/persisted redux state.
155
-     * @returns {string}
142
+     * @returns {string} The checksum of the specified {@code state}.
156 143
      */
157
-    _calculateChecksum(filteredState: Object) {
144
+    _calculateChecksum(state: Object) {
158 145
         try {
159
-            return md5.hex(JSON.stringify(filteredState) || '');
146
+            return md5.hex(JSON.stringify(state) || '');
160 147
         } catch (error) {
161
-            logger.error(
162
-                'Error calculating checksum for state',
163
-                filteredState,
164
-                error);
148
+            logger.error('Error calculating checksum for state', state, error);
165 149
 
166 150
             return '';
167 151
         }
168 152
     }
169 153
 
170
-    /**
171
-     * Retreives a persisted subtree from the storage.
172
-     *
173
-     * @private
174
-     * @param {string} subtreeName - The name of the subtree.
175
-     * @param {Object} subtreeConfig - The config of the subtree
176
-     * from this._elements.
177
-     * @returns {Object}
178
-     */
179
-    _getPersistedSubtree(subtreeName, subtreeConfig) {
180
-        let persistedSubtree = window.localStorage.getItem(subtreeName);
181
-
182
-        if (persistedSubtree) {
183
-            try {
184
-                persistedSubtree = JSON.parse(persistedSubtree);
185
-                const filteredSubtree
186
-                    = this._getFilteredSubtree(persistedSubtree, subtreeConfig);
187
-
188
-                if (filteredSubtree !== undefined) {
189
-                    return filteredSubtree;
190
-                }
191
-            } catch (error) {
192
-                logger.error(
193
-                    'Error parsing persisted subtree',
194
-                    subtreeName,
195
-                    persistedSubtree,
196
-                    error);
197
-            }
198
-        }
199
-
200
-        return null;
201
-    }
202
-
203 154
     /**
204 155
      * Prepares a filtered state from the actual or the persisted redux state,
205 156
      * based on this registry.
206 157
      *
207
-     * @private
208 158
      * @param {Object} state - The actual or persisted redux state.
159
+     * @private
209 160
      * @returns {Object}
210 161
      */
211 162
     _getFilteredState(state: Object) {
@@ -213,9 +164,10 @@ class PersistenceRegistry {
213 164
 
214 165
         for (const name of Object.keys(this._elements)) {
215 166
             if (state[name]) {
216
-                filteredState[name] = this._getFilteredSubtree(
217
-                    state[name],
218
-                    this._elements[name]);
167
+                filteredState[name]
168
+                    = this._getFilteredSubtree(
169
+                        state[name],
170
+                        this._elements[name]);
219 171
             }
220 172
         }
221 173
 
@@ -226,30 +178,64 @@ class PersistenceRegistry {
226 178
      * Prepares a filtered subtree based on the config for persisting or for
227 179
      * retrieval.
228 180
      *
229
-     * @private
230 181
      * @param {Object} subtree - The redux state subtree.
231 182
      * @param {ElementConfig} subtreeConfig - The related config.
183
+     * @private
232 184
      * @returns {Object}
233 185
      */
234 186
     _getFilteredSubtree(subtree, subtreeConfig) {
235 187
         let filteredSubtree;
236 188
 
237
-        if (subtreeConfig === true) {
238
-            // we persist the entire subtree
239
-            filteredSubtree = subtree;
240
-        } else if (typeof subtreeConfig === 'object') {
241
-            // only a filtered subtree gets persisted, based on the
242
-            // subtreeConfig object.
189
+        if (typeof subtreeConfig === 'object') {
190
+            // Only a filtered subtree gets persisted as specified by
191
+            // subtreeConfig.
243 192
             filteredSubtree = {};
244 193
             for (const persistedKey of Object.keys(subtree)) {
245 194
                 if (subtreeConfig[persistedKey]) {
246 195
                     filteredSubtree[persistedKey] = subtree[persistedKey];
247 196
                 }
248 197
             }
198
+        } else if (subtreeConfig) {
199
+            // Persist the entire subtree.
200
+            filteredSubtree = subtree;
249 201
         }
250 202
 
251 203
         return filteredSubtree;
252 204
     }
205
+
206
+    /**
207
+     * Retreives a persisted subtree from the storage.
208
+     *
209
+     * @param {string} subtreeName - The name of the subtree.
210
+     * @param {Object} subtreeConfig - The config of the subtree from
211
+     * {@link #_elements}.
212
+     * @private
213
+     * @returns {Object}
214
+     */
215
+    _getPersistedSubtree(subtreeName, subtreeConfig) {
216
+        let persistedSubtree = window.localStorage.getItem(subtreeName);
217
+
218
+        if (persistedSubtree) {
219
+            try {
220
+                persistedSubtree = JSON.parse(persistedSubtree);
221
+
222
+                const filteredSubtree
223
+                    = this._getFilteredSubtree(persistedSubtree, subtreeConfig);
224
+
225
+                if (filteredSubtree !== undefined) {
226
+                    return filteredSubtree;
227
+                }
228
+            } catch (error) {
229
+                logger.error(
230
+                    'Error parsing persisted subtree',
231
+                    subtreeName,
232
+                    persistedSubtree,
233
+                    error);
234
+            }
235
+        }
236
+
237
+        return undefined;
238
+    }
253 239
 }
254 240
 
255 241
 export default new PersistenceRegistry();

+ 16
- 10
react/features/recent-list/components/AbstractRecentList.js 查看文件

@@ -8,16 +8,19 @@ import { appNavigate } from '../../app';
8 8
  * The type of the React {@code Component} props of {@link AbstractRecentList}
9 9
  */
10 10
 type Props = {
11
+    _defaultURL: string,
12
+
13
+    _recentList: Array<Object>,
11 14
 
12 15
     /**
13
-     * Indicates if the list is disabled or not.
16
+     * The redux store's {@code dispatch} function.
14 17
      */
15
-    disabled: boolean,
18
+    dispatch: Dispatch<*>,
16 19
 
17 20
     /**
18
-     * The redux store's {@code dispatch} function.
21
+     * Whether {@code AbstractRecentList} is enabled.
19 22
      */
20
-    dispatch: Dispatch<*>
23
+    enabled: boolean
21 24
 };
22 25
 
23 26
 /**
@@ -33,18 +36,20 @@ export default class AbstractRecentList extends Component<Props> {
33 36
      * Joins the selected room.
34 37
      *
35 38
      * @param {string} room - The selected room.
39
+     * @protected
36 40
      * @returns {void}
37 41
      */
38 42
     _onJoin(room) {
39
-        const { disabled, dispatch } = this.props;
43
+        const { dispatch, enabled } = this.props;
40 44
 
41
-        !disabled && room && dispatch(appNavigate(room));
45
+        enabled && room && dispatch(appNavigate(room));
42 46
     }
43 47
 
44 48
     /**
45 49
      * Creates a bound onPress action for the list item.
46 50
      *
47 51
      * @param {string} room - The selected room.
52
+     * @protected
48 53
      * @returns {Function}
49 54
      */
50 55
     _onSelect(room) {
@@ -53,17 +58,18 @@ export default class AbstractRecentList extends Component<Props> {
53 58
 }
54 59
 
55 60
 /**
56
- * Maps redux state to component props.
61
+ * Maps (parts of) the redux state into {@code AbstractRecentList}'s React
62
+ * {@code Component} props.
57 63
  *
58 64
  * @param {Object} state - The redux state.
59 65
  * @returns {{
60
- *      _homeServer: string,
61
- *      _recentList: Array
66
+ *     _defaultURL: string,
67
+ *     _recentList: Array
62 68
  * }}
63 69
  */
64 70
 export function _mapStateToProps(state: Object) {
65 71
     return {
66
-        _homeServer: state['features/app'].app._getDefaultURL(),
72
+        _defaultURL: state['features/app'].app._getDefaultURL(),
67 73
         _recentList: state['features/recent-list']
68 74
     };
69 75
 }

+ 39
- 44
react/features/recent-list/components/RecentList.native.js 查看文件

@@ -47,21 +47,20 @@ class RecentList extends AbstractRecentList {
47 47
      * @returns {ReactElement}
48 48
      */
49 49
     render() {
50
-        const { _recentList, disabled } = this.props;
50
+        const { enabled, _recentList } = this.props;
51 51
 
52 52
         if (!_recentList) {
53 53
             return null;
54 54
         }
55 55
 
56 56
         const listViewDataSource
57
-            = this.dataSource.cloneWithRows(
58
-                getRecentRooms(_recentList));
57
+            = this.dataSource.cloneWithRows(getRecentRooms(_recentList));
59 58
 
60 59
         return (
61 60
             <View
62 61
                 style = { [
63 62
                     styles.container,
64
-                    disabled ? styles.containerDisabled : null
63
+                    enabled ? null : styles.containerDisabled
65 64
                 ] }>
66 65
                 <ListView
67 66
                     dataSource = { listViewDataSource }
@@ -72,19 +71,19 @@ class RecentList extends AbstractRecentList {
72 71
     }
73 72
 
74 73
     /**
75
-     * Assembles the style array of the avatar based on if the conference was a
76
-     * home or remote server conference (based on current app setting).
74
+     * Assembles the style array of the avatar based on if the conference was
75
+     * hosted on the default Jitsi Meet deployment or on a non-default one
76
+     * (based on current app setting).
77 77
      *
78 78
      * @param {Object} recentListEntry - The recent list entry being rendered.
79 79
      * @private
80 80
      * @returns {Array<Object>}
81 81
      */
82
-    _getAvatarStyle(recentListEntry) {
82
+    _getAvatarStyle({ baseURL, serverName }) {
83 83
         const avatarStyles = [ styles.avatar ];
84 84
 
85
-        if (recentListEntry.baseURL !== this.props._homeServer) {
86
-            avatarStyles.push(
87
-                this._getColorForServerName(recentListEntry.serverName));
85
+        if (baseURL !== this.props._defaultURL) {
86
+            avatarStyles.push(this._getColorForServerName(serverName));
88 87
         }
89 88
 
90 89
         return avatarStyles;
@@ -115,40 +114,15 @@ class RecentList extends AbstractRecentList {
115 114
      * @private
116 115
      * @returns {ReactElement}
117 116
      */
118
-    _renderConfDuration({ conferenceDurationString }) {
119
-        if (conferenceDurationString) {
117
+    _renderConfDuration({ durationString }) {
118
+        if (durationString) {
120 119
             return (
121 120
                 <View style = { styles.infoWithIcon } >
122 121
                     <Icon
123 122
                         name = 'timer'
124 123
                         style = { styles.inlineIcon } />
125 124
                     <Text style = { styles.confLength }>
126
-                        { conferenceDurationString }
127
-                    </Text>
128
-                </View>
129
-            );
130
-        }
131
-
132
-        return null;
133
-    }
134
-
135
-    /**
136
-     * Renders the server info component based on if the entry was on a
137
-     * different server or not.
138
-     *
139
-     * @param {Object} recentListEntry - The recent list entry being rendered.
140
-     * @private
141
-     * @returns {ReactElement}
142
-     */
143
-    _renderServerInfo(recentListEntry) {
144
-        if (recentListEntry.baseURL !== this.props._homeServer) {
145
-            return (
146
-                <View style = { styles.infoWithIcon } >
147
-                    <Icon
148
-                        name = 'public'
149
-                        style = { styles.inlineIcon } />
150
-                    <Text style = { styles.serverName }>
151
-                        { recentListEntry.serverName }
125
+                        { durationString }
152 126
                     </Text>
153 127
                 </View>
154 128
             );
@@ -191,17 +165,38 @@ class RecentList extends AbstractRecentList {
191 165
                                 { data.dateString }
192 166
                             </Text>
193 167
                         </View>
194
-                        {
195
-                            this._renderConfDuration(data)
196
-                        }
197
-                        {
198
-                            this._renderServerInfo(data)
199
-                        }
168
+                        { this._renderConfDuration(data) }
169
+                        { this._renderServerInfo(data) }
200 170
                     </View>
201 171
                 </View>
202 172
             </TouchableHighlight>
203 173
         );
204 174
     }
175
+
176
+    /**
177
+     * Renders the server info component based on whether the entry was on a
178
+     * different server.
179
+     *
180
+     * @param {Object} recentListEntry - The recent list entry being rendered.
181
+     * @private
182
+     * @returns {ReactElement}
183
+     */
184
+    _renderServerInfo({ baseURL, serverName }) {
185
+        if (baseURL !== this.props._defaultURL) {
186
+            return (
187
+                <View style = { styles.infoWithIcon } >
188
+                    <Icon
189
+                        name = 'public'
190
+                        style = { styles.inlineIcon } />
191
+                    <Text style = { styles.serverName }>
192
+                        { serverName }
193
+                    </Text>
194
+                </View>
195
+            );
196
+        }
197
+
198
+        return null;
199
+    }
205 200
 }
206 201
 
207 202
 export default connect(_mapStateToProps)(RecentList);

+ 12
- 13
react/features/recent-list/functions.js 查看文件

@@ -17,7 +17,6 @@ require('moment/locale/it');
17 17
 require('moment/locale/nb');
18 18
 
19 19
 // OC is not available. Please submit OC translation to the MomentJS project.
20
-
21 20
 require('moment/locale/pl');
22 21
 require('moment/locale/pt');
23 22
 require('moment/locale/pt-br');
@@ -47,22 +46,22 @@ export function getRecentRooms(list: Array<Object>): Array<Object> {
47 46
         const locale = _getSupportedLocale();
48 47
 
49 48
         for (const e of list) {
50
-            const location = parseURIString(e.conference);
49
+            const uri = parseURIString(e.conference);
50
+
51
+            if (uri && uri.room && uri.hostname) {
52
+                const duration
53
+                    = e.duration || /* legacy */ e.conferenceDuration;
51 54
 
52
-            if (location && location.room && location.hostname) {
53 55
                 recentRoomDS.push({
54
-                    baseURL: `${location.protocol}//${location.host}`,
56
+                    baseURL: `${uri.protocol}//${uri.host}`,
55 57
                     conference: e.conference,
56
-                    conferenceDuration: e.conferenceDuration,
57
-                    conferenceDurationString:
58
-                        _getDurationString(
59
-                            e.conferenceDuration,
60
-                            locale),
61 58
                     dateString: _getDateString(e.date, locale),
62 59
                     dateTimeStamp: e.date,
63
-                    initials: _getInitials(location.room),
64
-                    room: location.room,
65
-                    serverName: location.hostname
60
+                    duration,
61
+                    durationString: _getDurationString(duration, locale),
62
+                    initials: _getInitials(uri.room),
63
+                    room: uri.room,
64
+                    serverName: uri.hostname
66 65
                 });
67 66
             }
68 67
         }
@@ -124,7 +123,7 @@ function _getInitials(room: string) {
124 123
  * or duration ({@code number}).
125 124
  *
126 125
  * @private
127
- * @param {Date | number} dateOrDuration - The date or duration to format.
126
+ * @param {Date|number} dateOrDuration - The date or duration to format.
128 127
  * @param {string} locale - The locale to init the formatter with. Note: The
129 128
  * specified locale must be supported by the formatter so ensure the
130 129
  * prerequisite is met before invoking the function.

+ 9
- 12
react/features/recent-list/middleware.js 查看文件

@@ -15,11 +15,11 @@ import { storeCurrentConference, updateConferenceDuration } from './actions';
15 15
 MiddlewareRegistry.register(store => next => action => {
16 16
     switch (action.type) {
17 17
     case CONFERENCE_WILL_LEAVE:
18
-        _updateConferenceDuration(store, next);
18
+        _updateConferenceDuration(store);
19 19
         break;
20 20
 
21 21
     case SET_ROOM:
22
-        _maybeStoreCurrentConference(store, next, action);
22
+        _maybeStoreCurrentConference(store, action);
23 23
         break;
24 24
     }
25 25
 
@@ -36,12 +36,11 @@ MiddlewareRegistry.register(store => next => action => {
36 36
  * @private
37 37
  * @returns {void}
38 38
  */
39
-function _maybeStoreCurrentConference(store, next, action) {
40
-    const { locationURL } = store.getState()['features/base/connection'];
41
-    const { room } = action;
42
-
39
+function _maybeStoreCurrentConference({ dispatch, getState }, { room }) {
43 40
     if (room) {
44
-        next(storeCurrentConference(locationURL));
41
+        const { locationURL } = getState()['features/base/connection'];
42
+
43
+        dispatch(storeCurrentConference(locationURL));
45 44
     }
46 45
 }
47 46
 
@@ -49,13 +48,11 @@ function _maybeStoreCurrentConference(store, next, action) {
49 48
  * Updates the duration of the last conference stored in the list.
50 49
  *
51 50
  * @param {Store} store - The redux store.
52
- * @param {Dispatch} next - The redux {@code dispatch} function.
53
- * @param {Action} action - The redux action.
54 51
  * @private
55 52
  * @returns {void}
56 53
  */
57
-function _updateConferenceDuration(store, next) {
58
-    const { locationURL } = store.getState()['features/base/connection'];
54
+function _updateConferenceDuration({ dispatch, getState }) {
55
+    const { locationURL } = getState()['features/base/connection'];
59 56
 
60
-    next(updateConferenceDuration(locationURL));
57
+    dispatch(updateConferenceDuration(locationURL));
61 58
 }

+ 97
- 38
react/features/recent-list/reducer.js 查看文件

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+import { APP_WILL_MOUNT } from '../app';
3 4
 import { ReducerRegistry } from '../base/redux';
4 5
 import { PersistenceRegistry } from '../base/storage';
5 6
 
@@ -10,6 +11,13 @@ import {
10 11
 
11 12
 const logger = require('jitsi-meet-logger').getLogger(__filename);
12 13
 
14
+/**
15
+ * The default/initial redux state of the feature {@code recent-list}.
16
+ *
17
+ * @type {Array<Object>}
18
+ */
19
+const DEFAULT_STATE = [];
20
+
13 21
 /**
14 22
  * The name of the {@code window.localStorage} item where recent rooms are
15 23
  * stored.
@@ -31,17 +39,20 @@ export const MAX_LIST_SIZE = 30;
31 39
 const STORE_NAME = 'features/recent-list';
32 40
 
33 41
 /**
34
- * Sets up the persistence of the feature recent-list.
42
+ * Sets up the persistence of the feature {@code recent-list}.
35 43
  */
36 44
 PersistenceRegistry.register(STORE_NAME);
37 45
 
38 46
 /**
39
- * Reduces the redux actions of the feature recent-list.
47
+ * Reduces redux actions for the purposes of the feature {@code recent-list}.
40 48
  */
41 49
 ReducerRegistry.register(
42 50
     STORE_NAME,
43 51
     (state = _getLegacyRecentRoomList(), action) => {
44 52
         switch (action.type) {
53
+        case APP_WILL_MOUNT:
54
+            return _appWillMount(state);
55
+
45 56
         case STORE_CURRENT_CONFERENCE:
46 57
             return _storeCurrentConference(state, action);
47 58
 
@@ -53,18 +64,48 @@ ReducerRegistry.register(
53 64
         }
54 65
     });
55 66
 
67
+/**
68
+ * Reduces the redux action {@link APP_WILL_MOUNT}.
69
+ *
70
+ * @param {Object} state - The redux state of the feature {@code recent-list}.
71
+ * @param {Action} action - The redux action {@code APP_WILL_MOUNT}.
72
+ * @returns {Array<Object>} The next redux state of the feature
73
+ * {@code recent-list}.
74
+ */
75
+function _appWillMount(state) {
76
+    // XXX APP_WILL_MOUNT is the earliest redux action of ours dispatched in the
77
+    // store. For the purposes of legacy support, make sure that the
78
+    // deserialized recent-list's state is in the format deemed current by the
79
+    // current app revision.
80
+    if (state && typeof state === 'object') {
81
+        if (Array.isArray(state)) {
82
+            return state;
83
+        }
84
+
85
+        // In an enterprise/internal build of Jitsi Meet for Android and iOS we
86
+        // had recent-list's state as an object with property list.
87
+        const { list } = state;
88
+
89
+        if (Array.isArray(list) && list.length) {
90
+            return list.slice();
91
+        }
92
+    }
93
+
94
+    // In the weird case that we have previously persisted/serialized null.
95
+    return DEFAULT_STATE;
96
+}
97
+
56 98
 /**
57 99
  * Retrieves the recent room list that was stored using the legacy way.
58 100
  *
59 101
  * @returns {Array<Object>}
60 102
  */
61
-export function _getLegacyRecentRoomList(): Array<Object> {
103
+function _getLegacyRecentRoomList(): Array<Object> {
62 104
     try {
63
-        const list
64
-            = JSON.parse(window.localStorage.getItem(LEGACY_STORAGE_KEY));
105
+        const str = window.localStorage.getItem(LEGACY_STORAGE_KEY);
65 106
 
66
-        if (list && list.length) {
67
-            return list;
107
+        if (str) {
108
+            return JSON.parse(str);
68 109
         }
69 110
     } catch (error) {
70 111
         logger.warn('Failed to parse legacy recent-room list!');
@@ -74,61 +115,79 @@ export function _getLegacyRecentRoomList(): Array<Object> {
74 115
 }
75 116
 
76 117
 /**
77
-* Adds a new list entry to the redux store.
78
-*
79
-* @param {Object} state - The redux state.
80
-* @param {Object} action - The redux action.
81
-* @returns {Object}
82
-*/
83
-function _storeCurrentConference(state, action) {
84
-    const { locationURL } = action;
118
+ * Adds a new list entry to the redux store.
119
+ *
120
+ * @param {Object} state - The redux state of the feature {@code recent-list}.
121
+ * @param {Object} action - The redux action.
122
+ * @returns {Object}
123
+ */
124
+function _storeCurrentConference(state, { locationURL }) {
85 125
     const conference = locationURL.href;
86 126
 
87 127
     // If the current conference is already in the list, we remove it to re-add
88 128
     // it to the top.
89
-    const list = (Array.isArray(state) ? state : [])
90
-        .filter(e => e.conference !== conference);
129
+    const nextState
130
+        = state.filter(e => !_urlStringEquals(e.conference, conference));
91 131
 
92 132
     // The list is a reverse-sorted (i.e. the newer elements are at the end).
93
-    list.push({
133
+    nextState.push({
94 134
         conference,
95
-        conferenceDuration: 0, // We don't have this data yet!
96
-        date: Date.now()
135
+        date: Date.now(),
136
+        duration: 0 // We don't have the duration yet!
97 137
     });
98 138
 
99 139
     // Ensure the list doesn't exceed a/the maximum size.
100
-    list.splice(0, list.length - MAX_LIST_SIZE);
140
+    nextState.splice(0, nextState.length - MAX_LIST_SIZE);
101 141
 
102
-    return list;
142
+    return nextState;
103 143
 }
104 144
 
105 145
 /**
106 146
  * Updates the conference length when left.
107 147
  *
108
- * @param {Object} state - The redux state.
148
+ * @param {Object} state - The redux state of the feature {@code recent-list}.
109 149
  * @param {Object} action - The redux action.
110
- * @returns {Object}
150
+ * @returns {Object} The next redux state of the feature {@code recent-list}.
111 151
  */
112
-function _updateConferenceDuration(state, action) {
113
-    const { locationURL } = action;
152
+function _updateConferenceDuration(state, { locationURL }) {
153
+    if (locationURL && locationURL.href && state.length) {
154
+        const mostRecentIndex = state.length - 1;
155
+        const mostRecent = state[mostRecentIndex];
156
+
157
+        if (_urlStringEquals(mostRecent.conference, locationURL.href)) {
158
+            // The last conference start was stored so we need to update the
159
+            // length.
160
+            const nextMostRecent = {
161
+                ...mostRecent,
162
+                duration: Date.now() - mostRecent.date
163
+            };
114 164
 
115
-    if (locationURL && locationURL.href) {
116
-        // shallow copy to avoid in-place modification.
117
-        const list = (Array.isArray(state) ? state : []).slice();
165
+            delete nextMostRecent.conferenceDuration; // legacy
118 166
 
119
-        if (list.length > 0) {
120
-            const mostRecentURL = list[list.length - 1];
167
+            // Shallow copy to avoid in-place modification.
168
+            const nextState = state.slice();
121 169
 
122
-            if (mostRecentURL.conference === locationURL.href) {
123
-                // The last conference start was stored so we need to update the
124
-                // length.
125
-                mostRecentURL.conferenceDuration
126
-                    = Date.now() - mostRecentURL.date;
170
+            nextState[mostRecentIndex] = nextMostRecent;
127 171
 
128
-                return list;
129
-            }
172
+            return nextState;
130 173
         }
131 174
     }
132 175
 
133 176
     return state;
134 177
 }
178
+
179
+/**
180
+ * Determines whether two specific URL {@code strings} are equal in the sense
181
+ * that they identify one and the same conference resource (irrespective of
182
+ * time) for the purposes of the feature {@code recent-list}.
183
+ *
184
+ * @param {string} a - The URL {@code string} to test for equality to {@code b}.
185
+ * @param {string} b - The URL {@code string} to test for equality to {@code a}.
186
+ * @returns {boolean}
187
+ */
188
+function _urlStringEquals(a: string, b: string) {
189
+    // FIXME Case-sensitive comparison is wrong because the room name at least
190
+    // is case insensitive on the server and elsewhere (where it matters) in the
191
+    // client. I don't think domain names are case-sensitive either.
192
+    return a === b;
193
+}

+ 8
- 2
react/features/settings/components/AbstractSettingsView.js 查看文件

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { Component } from 'react';
4 4
 
5
-import { getProfile, updateProfile } from '../../base/profile';
5
+import { updateProfile } from '../../base/profile';
6 6
 
7 7
 /**
8 8
  * The type of the React {@code Component} props of
@@ -12,16 +12,22 @@ type Props = {
12 12
 
13 13
     /**
14 14
      * The current profile object.
15
+     *
16
+     * @protected
15 17
      */
16 18
     _profile: Object,
17 19
 
18 20
     /**
19 21
      * The default URL for when there is no custom URL set in the profile.
22
+     *
23
+     * @protected
20 24
      */
21 25
     _serverURL: string,
22 26
 
23 27
     /**
24 28
      * Whether {@link AbstractSettingsView} is visible.
29
+     *
30
+     * @protected
25 31
      */
26 32
     _visible: boolean,
27 33
 
@@ -168,7 +174,7 @@ export class AbstractSettingsView extends Component<Props> {
168 174
  */
169 175
 export function _mapStateToProps(state: Object) {
170 176
     return {
171
-        _profile: getProfile(state),
177
+        _profile: state['features/base/profile'],
172 178
         _serverURL: state['features/app'].app._getDefaultURL(),
173 179
         _visible: state['features/settings'].visible
174 180
     };

+ 3
- 18
react/features/welcome/components/AbstractWelcomePage.js 查看文件

@@ -1,6 +1,5 @@
1 1
 // @flow
2 2
 
3
-import PropTypes from 'prop-types';
4 3
 import { Component } from 'react';
5 4
 
6 5
 import { createWelcomePageEvent, sendAnalytics } from '../../analytics';
@@ -14,11 +13,6 @@ import { generateRoomWithoutSeparator } from '../functions';
14 13
  */
15 14
 type Props = {
16 15
 
17
-    /**
18
-     * Boolean to indicate if the room field is focused or not.
19
-     */
20
-    _fieldFocused: boolean,
21
-
22 16
     /**
23 17
      * The user's profile.
24 18
      */
@@ -32,17 +26,7 @@ type Props = {
32 26
  *
33 27
  * @abstract
34 28
  */
35
-export class AbstractWelcomePage extends Component<*, *> {
36
-    /**
37
-     * {@code AbstractWelcomePage}'s React {@code Component} prop types.
38
-     *
39
-     * @static
40
-     */
41
-    static propTypes = {
42
-        _room: PropTypes.string,
43
-        dispatch: PropTypes.func
44
-    };
45
-
29
+export class AbstractWelcomePage extends Component<Props, *> {
46 30
     _mounted: ?boolean;
47 31
 
48 32
     /**
@@ -245,12 +229,13 @@ export class AbstractWelcomePage extends Component<*, *> {
245 229
  * @param {Object} state - The redux state.
246 230
  * @protected
247 231
  * @returns {{
232
+ *     _profile: Object,
248 233
  *     _room: string
249 234
  * }}
250 235
  */
251 236
 export function _mapStateToProps(state: Object) {
252 237
     return {
253
-        _profile: state['features/base/profile'].profile,
238
+        _profile: state['features/base/profile'],
254 239
         _room: state['features/base/conference'].room
255 240
     };
256 241
 }

+ 1
- 8
react/features/welcome/components/WelcomePage.native.js 查看文件

@@ -40,13 +40,6 @@ import WelcomePageSideBar from './WelcomePageSideBar';
40 40
  * @extends AbstractWelcomePage
41 41
  */
42 42
 class WelcomePage extends AbstractWelcomePage {
43
-    /**
44
-     * WelcomePage component's property types.
45
-     *
46
-     * @static
47
-     */
48
-    static propTypes = AbstractWelcomePage.propTypes;
49
-
50 43
     /**
51 44
      * Constructor of the Component.
52 45
      *
@@ -140,7 +133,7 @@ class WelcomePage extends AbstractWelcomePage {
140 133
                         {
141 134
                             this._renderHintBox()
142 135
                         }
143
-                        <RecentList disabled = { this.state._fieldFocused } />
136
+                        <RecentList enabled = { !this.state._fieldFocused } />
144 137
                     </SafeAreaView>
145 138
                     <SettingsView />
146 139
                 </View>

正在加载...
取消
保存