瀏覽代碼

[RN] Add app-settings feature

[RN] Fix PR feedbacks, write persistency docs
master
zbettenbuk 7 年之前
父節點
當前提交
bfcd34358b
共有 29 個檔案被更改,包括 1294 行新增82 行删除
  1. 8
    0
      lang/main.json
  2. 19
    0
      react/features/app-settings/actionTypes.js
  3. 32
    0
      react/features/app-settings/actions.js
  4. 311
    0
      react/features/app-settings/components/AbstractAppSettings.js
  5. 99
    0
      react/features/app-settings/components/AppSettings.native.js
  6. 0
    0
      react/features/app-settings/components/AppSettings.web.js
  7. 138
    0
      react/features/app-settings/components/FormRow.native.js
  8. 1
    0
      react/features/app-settings/components/index.js
  9. 98
    0
      react/features/app-settings/components/styles.js
  10. 0
    0
      react/features/app-settings/functions.js
  11. 5
    0
      react/features/app-settings/index.js
  12. 31
    0
      react/features/app-settings/reducer.js
  13. 26
    1
      react/features/app/actions.js
  14. 120
    50
      react/features/app/components/AbstractApp.js
  15. 46
    27
      react/features/base/lib-jitsi-meet/native/Storage.js
  16. 15
    0
      react/features/base/profile/actionTypes.js
  17. 23
    0
      react/features/base/profile/actions.js
  18. 15
    0
      react/features/base/profile/functions.js
  19. 5
    0
      react/features/base/profile/index.js
  20. 43
    0
      react/features/base/profile/middleware.js
  21. 25
    0
      react/features/base/profile/reducer.js
  22. 93
    0
      react/features/base/redux/functions.js
  23. 2
    0
      react/features/base/redux/index.js
  24. 36
    0
      react/features/base/redux/middleware.js
  25. 5
    0
      react/features/base/redux/persisterconfig.json
  26. 36
    0
      react/features/base/redux/readme.md
  27. 14
    0
      react/features/welcome/components/AbstractWelcomePage.js
  28. 18
    4
      react/features/welcome/components/WelcomePage.native.js
  29. 30
    0
      react/features/welcome/components/styles.js

+ 8
- 0
lang/main.json 查看文件

@@ -535,5 +535,13 @@
535 535
         "invite": "Invite in __app__",
536 536
         "title": "Call access info",
537 537
         "tooltip": "Get access info about the meeting"
538
+    },
539
+    "profileModal": {
540
+        "displayName": "Display name",
541
+        "email": "Email",
542
+        "header": "Settings",
543
+        "serverURL": "Server URL",
544
+        "startWithAudioMuted": "Start with audio muted",
545
+        "startWithVideoMuted": "Start with video muted"
538 546
     }
539 547
 }

+ 19
- 0
react/features/app-settings/actionTypes.js 查看文件

@@ -0,0 +1,19 @@
1
+/**
2
+ * The type of (redux) action which signals the request
3
+ * to hide the app settings modal.
4
+ *
5
+ * {
6
+ *     type: HIDE_APP_SETTINGS
7
+ * }
8
+ */
9
+export const HIDE_APP_SETTINGS = Symbol('HIDE_APP_SETTINGS');
10
+
11
+/**
12
+ * The type of (redux) action which signals the request
13
+ * to show the app settings modal where available.
14
+ *
15
+ * {
16
+ *     type: SHOW_APP_SETTINGS
17
+ * }
18
+ */
19
+export const SHOW_APP_SETTINGS = Symbol('SHOW_APP_SETTINGS');

+ 32
- 0
react/features/app-settings/actions.js 查看文件

@@ -0,0 +1,32 @@
1
+/* @flow */
2
+
3
+import {
4
+    HIDE_APP_SETTINGS,
5
+    SHOW_APP_SETTINGS
6
+} from './actionTypes';
7
+
8
+/**
9
+* Redux-signals the request to open the app settings modal.
10
+*
11
+* @returns {{
12
+*     type: SHOW_APP_SETTINGS
13
+* }}
14
+*/
15
+export function showAppSettings() {
16
+    return {
17
+        type: SHOW_APP_SETTINGS
18
+    };
19
+}
20
+
21
+/**
22
+* Redux-signals the request to hide the app settings modal.
23
+*
24
+* @returns {{
25
+*     type: HIDE_APP_SETTINGS
26
+* }}
27
+*/
28
+export function hideAppSettings() {
29
+    return {
30
+        type: HIDE_APP_SETTINGS
31
+    };
32
+}

+ 311
- 0
react/features/app-settings/components/AbstractAppSettings.js 查看文件

@@ -0,0 +1,311 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import { hideAppSettings } from '../actions';
6
+import { getProfile, updateProfile } from '../../base/profile';
7
+
8
+/**
9
+* The type of the React {@code Component} props of {@link AbstractAppSettings}
10
+*/
11
+type Props = {
12
+
13
+    /**
14
+    * The current profile object.
15
+    */
16
+    _profile: Object,
17
+
18
+    /**
19
+    * The visibility prop of the settings modal.
20
+    */
21
+    _visible: boolean,
22
+
23
+    /**
24
+    * Redux store dispatch function.
25
+    */
26
+    dispatch: Dispatch<*>
27
+};
28
+
29
+/**
30
+ * The type of the React {@code Component} state of {@link AbstractAppSettings}.
31
+ */
32
+type State = {
33
+
34
+    /**
35
+    * The display name field value on the settings screen.
36
+    */
37
+    displayName: string,
38
+
39
+    /**
40
+    * The email field value on the settings screen.
41
+    */
42
+    email: string,
43
+
44
+    /**
45
+    * The server url field value on the settings screen.
46
+    */
47
+    serverURL: string,
48
+
49
+    /**
50
+    * The start audio muted switch value on the settings screen.
51
+    */
52
+    startWithAudioMuted: boolean,
53
+
54
+    /**
55
+    * The start video muted switch value on the settings screen.
56
+    */
57
+    startWithVideoMuted: boolean
58
+}
59
+
60
+/**
61
+ * Base (abstract) class for container component rendering
62
+ * the app settings page.
63
+ *
64
+ * @abstract
65
+ */
66
+export class AbstractAppSettings extends Component<Props, State> {
67
+
68
+    /**
69
+     * Initializes a new {@code AbstractAppSettings} instance.
70
+     *
71
+     * @param {Props} props - The React {@code Component} props to initialize
72
+     * the component.
73
+     */
74
+    constructor(props: Props) {
75
+        super(props);
76
+
77
+        this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
78
+        this._onChangeEmail = this._onChangeEmail.bind(this);
79
+        this._onChangeServerName = this._onChangeServerName.bind(this);
80
+        this._onRequestClose = this._onRequestClose.bind(this);
81
+        this._onSaveDisplayName = this._onSaveDisplayName.bind(this);
82
+        this._onSaveEmail = this._onSaveEmail.bind(this);
83
+        this._onSaveServerName = this._onSaveServerName.bind(this);
84
+        this._onStartAudioMutedChange
85
+            = this._onStartAudioMutedChange.bind(this);
86
+        this._onStartVideoMutedChange
87
+            = this._onStartVideoMutedChange.bind(this);
88
+    }
89
+
90
+    /**
91
+     * Invokes React's {@link Component#componentWillReceiveProps()} to make
92
+     * sure we have the state Initialized on component mount.
93
+     *
94
+     * @inheritdoc
95
+     */
96
+    componentWillMount() {
97
+        this._updateStateFromProps(this.props);
98
+    }
99
+
100
+    /**
101
+     * Implements React's {@link Component#componentWillReceiveProps()}. Invoked
102
+     * before this mounted component receives new props.
103
+     *
104
+     * @inheritdoc
105
+     * @param {Props} nextProps - New props component will receive.
106
+     */
107
+    componentWillReceiveProps(nextProps: Props) {
108
+        this._updateStateFromProps(nextProps);
109
+    }
110
+
111
+    _onChangeDisplayName: (string) => void;
112
+
113
+    /**
114
+    * Handles the display name field value change.
115
+    *
116
+    * @protected
117
+    * @param {string} text - The value typed in the name field.
118
+    * @returns {void}
119
+    */
120
+    _onChangeDisplayName(text) {
121
+        this.setState({
122
+            displayName: text
123
+        });
124
+    }
125
+
126
+    _onChangeEmail: (string) => void;
127
+
128
+    /**
129
+    * Handles the email field value change.
130
+    *
131
+    * @protected
132
+    * @param {string} text - The value typed in the email field.
133
+    * @returns {void}
134
+    */
135
+    _onChangeEmail(text) {
136
+        this.setState({
137
+            email: text
138
+        });
139
+    }
140
+
141
+    _onChangeServerName: (string) => void;
142
+
143
+    /**
144
+    * Handles the server name field value change.
145
+    *
146
+    * @protected
147
+    * @param {string} text - The server URL typed in the server field.
148
+    * @returns {void}
149
+    */
150
+    _onChangeServerName(text) {
151
+        this.setState({
152
+            serverURL: text
153
+        });
154
+    }
155
+
156
+    _onRequestClose: () => void;
157
+
158
+    /**
159
+    * Handles the hardware back button.
160
+    *
161
+    * @returns {void}
162
+    */
163
+    _onRequestClose() {
164
+        this.props.dispatch(hideAppSettings());
165
+    }
166
+
167
+    _onSaveDisplayName: () => void;
168
+
169
+    /**
170
+    * Handles the display name field onEndEditing.
171
+    *
172
+    * @protected
173
+    * @returns {void}
174
+    */
175
+    _onSaveDisplayName() {
176
+        this._updateProfile({
177
+            displayName: this.state.displayName
178
+        });
179
+    }
180
+
181
+    _onSaveEmail: () => void;
182
+
183
+    /**
184
+    * Handles the email field onEndEditing.
185
+    *
186
+    * @protected
187
+    * @returns {void}
188
+    */
189
+    _onSaveEmail() {
190
+        this._updateProfile({
191
+            email: this.state.email
192
+        });
193
+    }
194
+
195
+    _onSaveServerName: () => void;
196
+
197
+    /**
198
+    * Handles the server name field onEndEditing.
199
+    *
200
+    * @protected
201
+    * @returns {void}
202
+    */
203
+    _onSaveServerName() {
204
+        let serverURL;
205
+
206
+        if (this.state.serverURL.endsWith('/')) {
207
+            serverURL = this.state.serverURL.substr(
208
+                0, this.state.serverURL.length - 1
209
+            );
210
+        } else {
211
+            serverURL = this.state.serverURL;
212
+        }
213
+
214
+        this._updateProfile({
215
+            defaultURL: serverURL
216
+        });
217
+        this.setState({
218
+            serverURL
219
+        });
220
+    }
221
+
222
+    _onStartAudioMutedChange: (boolean) => void;
223
+
224
+    /**
225
+    * Handles the start audio muted change event.
226
+    *
227
+    * @protected
228
+    * @param {boolean} newValue - The new value for the
229
+    * start audio muted option.
230
+    * @returns {void}
231
+    */
232
+    _onStartAudioMutedChange(newValue) {
233
+        this.setState({
234
+            startWithAudioMuted: newValue
235
+        });
236
+
237
+        this._updateProfile({
238
+            startWithAudioMuted: newValue
239
+        });
240
+    }
241
+
242
+    _onStartVideoMutedChange: (boolean) => void;
243
+
244
+    /**
245
+    * Handles the start video muted change event.
246
+    *
247
+    * @protected
248
+    * @param {boolean} newValue - The new value for the
249
+    * start video muted option.
250
+    * @returns {void}
251
+    */
252
+    _onStartVideoMutedChange(newValue) {
253
+        this.setState({
254
+            startWithVideoMuted: newValue
255
+        });
256
+
257
+        this._updateProfile({
258
+            startWithVideoMuted: newValue
259
+        });
260
+    }
261
+
262
+    _updateProfile: (Object) => void;
263
+
264
+    /**
265
+    * Updates the persisted profile on any change.
266
+    *
267
+    * @private
268
+    * @param {Object} updateObject - The partial update object for the profile.
269
+    * @returns {void}
270
+    */
271
+    _updateProfile(updateObject: Object) {
272
+        this.props.dispatch(updateProfile({
273
+            ...this.props._profile,
274
+            ...updateObject
275
+        }));
276
+    }
277
+
278
+    _updateStateFromProps: (Object) => void;
279
+
280
+    /**
281
+    * Updates the component state when (new) props are received.
282
+    *
283
+    * @private
284
+    * @param {Object} props - The component's props.
285
+    * @returns {void}
286
+    */
287
+    _updateStateFromProps(props) {
288
+        this.setState({
289
+            displayName: props._profile.displayName,
290
+            email: props._profile.email,
291
+            serverURL: props._profile.defaultURL,
292
+            startWithAudioMuted: props._profile.startWithAudioMuted,
293
+            startWithVideoMuted: props._profile.startWithVideoMuted
294
+        });
295
+    }
296
+}
297
+
298
+/**
299
+ * Maps (parts of) the redux state to the React {@code Component} props of
300
+ * {@code AbstractAppSettings}.
301
+ *
302
+ * @param {Object} state - The redux state.
303
+ * @protected
304
+ * @returns {Object}
305
+ */
306
+export function _mapStateToProps(state: Object) {
307
+    return {
308
+        _profile: getProfile(state),
309
+        _visible: state['features/app-settings'].visible
310
+    };
311
+}

+ 99
- 0
react/features/app-settings/components/AppSettings.native.js 查看文件

@@ -0,0 +1,99 @@
1
+import React from 'react';
2
+import {
3
+    Modal,
4
+    Switch,
5
+    Text,
6
+    TextInput,
7
+    View } from 'react-native';
8
+import { connect } from 'react-redux';
9
+
10
+import {
11
+    _mapStateToProps,
12
+    AbstractAppSettings
13
+} from './AbstractAppSettings';
14
+import FormRow from './FormRow';
15
+import styles from './styles';
16
+
17
+import { translate } from '../../base/i18n';
18
+
19
+/**
20
+ * The native container rendering the app settings page.
21
+ *
22
+ * @extends AbstractAppSettings
23
+ */
24
+class AppSettings extends AbstractAppSettings {
25
+
26
+    /**
27
+     * Implements React's {@link Component#render()}, renders the settings page.
28
+     *
29
+     * @inheritdoc
30
+     * @returns {ReactElement}
31
+     */
32
+    render() {
33
+        const { t } = this.props;
34
+
35
+        return (
36
+            <Modal
37
+                animationType = 'slide'
38
+                onRequestClose = { this._onRequestClose }
39
+                presentationStyle = 'fullScreen'
40
+                style = { styles.modal }
41
+                visible = { this.props._visible }>
42
+                <View style = { styles.headerContainer } >
43
+                    <Text style = { [ styles.text, styles.headerTitle ] } >
44
+                        { t('profileModal.header') }
45
+                    </Text>
46
+                </View>
47
+                <View style = { styles.settingsContainer } >
48
+                    <FormRow
49
+                        fieldSeparator = { true }
50
+                        i18nLabel = 'profileModal.serverURL' >
51
+                        <TextInput
52
+                            autoCapitalize = 'none'
53
+                            onChangeText = { this._onChangeServerName }
54
+                            onEndEditing = { this._onSaveServerName }
55
+                            placeholder = 'https://jitsi.example.com'
56
+                            value = { this.state.serverURL } />
57
+                    </FormRow>
58
+                    <FormRow
59
+                        fieldSeparator = { true }
60
+                        i18nLabel = 'profileModal.displayName' >
61
+                        <TextInput
62
+                            onChangeText = { this._onChangeDisplayName }
63
+                            onEndEditing = { this._onSaveDisplayName }
64
+                            placeholder = 'John Doe'
65
+                            value = { this.state.displayName } />
66
+                    </FormRow>
67
+                    <FormRow
68
+                        fieldSeparator = { true }
69
+                        i18nLabel = 'profileModal.email' >
70
+                        <TextInput
71
+                            onChangeText = { this._onChangeEmail }
72
+                            onEndEditing = { this._onSaveEmail }
73
+                            placeholder = 'email@example.com'
74
+                            value = { this.state.email } />
75
+                    </FormRow>
76
+                    <FormRow
77
+                        fieldSeparator = { true }
78
+                        i18nLabel = 'profileModal.startWithAudioMuted' >
79
+                        <Switch
80
+                            onValueChange = {
81
+                                this._onStartAudioMutedChange
82
+                            }
83
+                            value = { this.state.startWithAudioMuted } />
84
+                    </FormRow>
85
+                    <FormRow
86
+                        i18nLabel = 'profileModal.startWithVideoMuted' >
87
+                        <Switch
88
+                            onValueChange = {
89
+                                this._onStartVideoMutedChange
90
+                            }
91
+                            value = { this.state.startWithVideoMuted } />
92
+                    </FormRow>
93
+                </View>
94
+            </Modal>
95
+        );
96
+    }
97
+}
98
+
99
+export default translate(connect(_mapStateToProps)(AppSettings));

+ 0
- 0
react/features/app-settings/components/AppSettings.web.js 查看文件


+ 138
- 0
react/features/app-settings/components/FormRow.native.js 查看文件

@@ -0,0 +1,138 @@
1
+/* @flow */
2
+
3
+import React, { Component } from 'react';
4
+import {
5
+    Text,
6
+    View } from 'react-native';
7
+import { connect } from 'react-redux';
8
+
9
+import styles, { ANDROID_UNDERLINE_COLOR } from './styles';
10
+
11
+import { translate } from '../../base/i18n';
12
+
13
+/**
14
+* The type of the React {@code Component} props of {@link FormRow}
15
+*/
16
+type Props = {
17
+
18
+    /**
19
+    */
20
+    children: Object,
21
+
22
+    /**
23
+    * Prop to decide if a row separator is to be rendered.
24
+    */
25
+    fieldSeparator: boolean,
26
+
27
+    /**
28
+    * The i18n key of the text label of the form field.
29
+    */
30
+    i18nLabel: string,
31
+
32
+    /**
33
+     * Invoked to obtain translated strings.
34
+     */
35
+    t: Function
36
+}
37
+
38
+/**
39
+ * Implements a React {@code Component} which renders a standardized row
40
+ * on a form. The component should have exactly one child component.
41
+ */
42
+class FormRow extends Component<Props> {
43
+
44
+    /**
45
+     * Initializes a new {@code FormRow} instance.
46
+     *
47
+     * @param {Object} props - Component properties.
48
+     */
49
+    constructor(props) {
50
+        super(props);
51
+
52
+        React.Children.only(this.props.children);
53
+        this._getDefaultFieldProps = this._getDefaultFieldProps.bind(this);
54
+        this._getRowStyle = this._getRowStyle.bind(this);
55
+    }
56
+
57
+    /**
58
+     * Implements React's {@link Component#render()}.
59
+     *
60
+     * @inheritdoc
61
+     * @override
62
+     * @returns {ReactElement}
63
+     */
64
+    render() {
65
+        const { t } = this.props;
66
+
67
+        // Some field types need additional props to look good and standardized
68
+        // on a form.
69
+        const newChild = React.cloneElement(
70
+            this.props.children,
71
+            this._getDefaultFieldProps(this.props.children)
72
+        );
73
+
74
+        return (
75
+            <View
76
+                style = { this._getRowStyle() } >
77
+                <View style = { styles.fieldLabelContainer } >
78
+                    <Text style = { styles.text } >
79
+                        { t(this.props.i18nLabel) }
80
+                    </Text>
81
+                </View>
82
+                <View style = { styles.fieldValueContainer } >
83
+                    { newChild }
84
+                </View>
85
+            </View>
86
+        );
87
+    }
88
+
89
+    _getDefaultFieldProps: (field: Component<*, *>) => Object;
90
+
91
+    /**
92
+    * Assembles the default props to the field child component of
93
+    * this form row.
94
+    *
95
+    * Currently tested/supported field types:
96
+    *       - TextInput
97
+    *       - Switch (needs no addition props ATM).
98
+    *
99
+    * @private
100
+    * @param {Object} field - The field (child) component.
101
+    * @returns {Object}
102
+    */
103
+    _getDefaultFieldProps(field: Object) {
104
+        if (field && field.type) {
105
+            switch (field.type.displayName) {
106
+            case 'TextInput':
107
+                return {
108
+                    style: styles.textInputField,
109
+                    underlineColorAndroid: ANDROID_UNDERLINE_COLOR
110
+                };
111
+            }
112
+        }
113
+
114
+        return {};
115
+    }
116
+
117
+    _getRowStyle: () => Array<Object>;
118
+
119
+    /**
120
+    * Assembles the row style array based on the row's props.
121
+    *
122
+    * @private
123
+    * @returns {Array<Object>}
124
+    */
125
+    _getRowStyle() {
126
+        const rowStyle = [
127
+            styles.fieldContainer
128
+        ];
129
+
130
+        if (this.props.fieldSeparator) {
131
+            rowStyle.push(styles.fieldSeparator);
132
+        }
133
+
134
+        return rowStyle;
135
+    }
136
+}
137
+
138
+export default translate(connect()(FormRow));

+ 1
- 0
react/features/app-settings/components/index.js 查看文件

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

+ 98
- 0
react/features/app-settings/components/styles.js 查看文件

@@ -0,0 +1,98 @@
1
+import {
2
+    BoxModel,
3
+    ColorPalette,
4
+    createStyleSheet
5
+} from '../../base/styles';
6
+
7
+const LABEL_TAB = 300;
8
+
9
+export const ANDROID_UNDERLINE_COLOR = 'transparent';
10
+
11
+/**
12
+ * The styles of the React {@code Components} of the feature welcome including
13
+ * {@code WelcomePage} and {@code BlankPage}.
14
+ */
15
+export default createStyleSheet({
16
+
17
+    /**
18
+    * Standardized style for a field container {@code View}.
19
+    */
20
+    fieldContainer: {
21
+        flexDirection: 'row',
22
+        alignItems: 'center',
23
+        minHeight: 65
24
+    },
25
+
26
+    /**
27
+    * Standard container for a {@code View} containing a field label.
28
+    */
29
+    fieldLabelContainer: {
30
+        flexDirection: 'row',
31
+        alignItems: 'center',
32
+        width: LABEL_TAB
33
+    },
34
+
35
+    /**
36
+    * Field container style for all but last row {@code View}.
37
+    */
38
+    fieldSeparator: {
39
+        borderBottomWidth: 1
40
+    },
41
+
42
+    /**
43
+    * Style for the {@code View} containing each
44
+    * field values (the actual field).
45
+    */
46
+    fieldValueContainer: {
47
+        flex: 1,
48
+        justifyContent: 'flex-end',
49
+        flexDirection: 'row',
50
+        alignItems: 'center'
51
+    },
52
+
53
+    /**
54
+    * Page header {@code View}.
55
+    */
56
+    headerContainer: {
57
+        backgroundColor: ColorPalette.blue,
58
+        flexDirection: 'row',
59
+        alignItems: 'center',
60
+        padding: 2 * BoxModel.margin
61
+    },
62
+
63
+    /**
64
+    * The title {@code Text} of the header.
65
+    */
66
+    headerTitle: {
67
+        color: ColorPalette.white,
68
+        fontSize: 25
69
+    },
70
+
71
+    /**
72
+    * The top level container {@code View}.
73
+    */
74
+    settingsContainer: {
75
+        backgroundColor: ColorPalette.white,
76
+        flex: 1,
77
+        flexDirection: 'column',
78
+        margin: 0,
79
+        padding: 2 * BoxModel.padding
80
+    },
81
+
82
+    /**
83
+    * Global {@code Text} color for the page.
84
+    */
85
+    text: {
86
+        color: ColorPalette.black,
87
+        fontSize: 20
88
+    },
89
+
90
+    /**
91
+    * Standard text input field style.
92
+    */
93
+    textInputField: {
94
+        fontSize: 20,
95
+        flex: 1,
96
+        textAlign: 'right'
97
+    }
98
+});

+ 0
- 0
react/features/app-settings/functions.js 查看文件


+ 5
- 0
react/features/app-settings/index.js 查看文件

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

+ 31
- 0
react/features/app-settings/reducer.js 查看文件

@@ -0,0 +1,31 @@
1
+// @flow
2
+
3
+import {
4
+    HIDE_APP_SETTINGS,
5
+    SHOW_APP_SETTINGS
6
+} from './actionTypes';
7
+
8
+import { ReducerRegistry } from '../base/redux';
9
+
10
+const DEFAULT_STATE = {
11
+    visible: false
12
+};
13
+
14
+ReducerRegistry.register(
15
+    'features/app-settings', (state = DEFAULT_STATE, action) => {
16
+        switch (action.type) {
17
+        case HIDE_APP_SETTINGS:
18
+            return {
19
+                ...state,
20
+                visible: false
21
+            };
22
+
23
+        case SHOW_APP_SETTINGS:
24
+            return {
25
+                ...state,
26
+                visible: true
27
+            };
28
+        }
29
+
30
+        return state;
31
+    });

+ 26
- 1
react/features/app/actions.js 查看文件

@@ -4,6 +4,7 @@ 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';
7 8
 import { parseURIString } from '../base/util';
8 9
 
9 10
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
@@ -82,7 +83,11 @@ function _appNavigateToMandatoryLocation(
82 83
             });
83 84
         }
84 85
 
85
-        return promise.then(() => dispatch(setConfig(config)));
86
+        const profile = getProfile(getState());
87
+
88
+        return promise.then(() => dispatch(setConfig(
89
+            _mergeConfigWithProfile(config, profile)
90
+        )));
86 91
     }
87 92
 }
88 93
 
@@ -245,3 +250,23 @@ function _loadConfig({ contextRoot, host, protocol, room }) {
245 250
             throw error;
246 251
         });
247 252
 }
253
+
254
+/**
255
+ * Merges the downloaded config with the current profile values. The profile
256
+ * values are named the same way as the config values in the config.js so
257
+ * a clean merge is possible.
258
+ *
259
+ * @param {Object|undefined} config - The downloaded config.
260
+ * @param {Object} profile - The persisted profile.
261
+ * @returns {Object}
262
+ */
263
+function _mergeConfigWithProfile(config, profile) {
264
+    if (!config) {
265
+        return;
266
+    }
267
+
268
+    return {
269
+        ...config,
270
+        ...profile
271
+    };
272
+}

+ 120
- 50
react/features/app/components/AbstractApp.js 查看文件

@@ -13,7 +13,12 @@ import {
13 13
     localParticipantLeft
14 14
 } from '../../base/participants';
15 15
 import { Fragment, RouteRegistry } from '../../base/react';
16
-import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
16
+import {
17
+    getPersistedState,
18
+    MiddlewareRegistry,
19
+    ReducerRegistry
20
+} from '../../base/redux';
21
+import { getProfile } from '../../base/profile';
17 22
 import { toURLString } from '../../base/util';
18 23
 import { OverlayContainer } from '../../overlay';
19 24
 import { BlankPage } from '../../welcome';
@@ -72,6 +77,7 @@ export class AbstractApp extends Component {
72 77
         super(props);
73 78
 
74 79
         this.state = {
80
+
75 81
             /**
76 82
              * The Route rendered by this {@code AbstractApp}.
77 83
              *
@@ -79,13 +85,35 @@ export class AbstractApp extends Component {
79 85
              */
80 86
             route: undefined,
81 87
 
88
+            /**
89
+             * The state of the »possible« async initialization of
90
+             * the {@code AbstractApp}.
91
+             */
92
+            appAsyncInitialized: false,
93
+
82 94
             /**
83 95
              * The redux store used by this {@code AbstractApp}.
84 96
              *
85 97
              * @type {Store}
86 98
              */
87
-            store: this._maybeCreateStore(props)
99
+            store: undefined
88 100
         };
101
+
102
+        /**
103
+         * This way we make the mobile version wait until the
104
+         * {@code AsyncStorage} implementation of {@code Storage}
105
+         * properly initializes. On web it does actually nothing, see
106
+         * {@link #_initStorage}.
107
+         */
108
+        this.init = new Promise(resolve => {
109
+            this._initStorage().then(() => {
110
+                this.setState({
111
+                    route: undefined,
112
+                    store: this._maybeCreateStore(props)
113
+                });
114
+                resolve();
115
+            });
116
+        });
89 117
     }
90 118
 
91 119
     /**
@@ -95,29 +123,48 @@ export class AbstractApp extends Component {
95 123
      * @inheritdoc
96 124
      */
97 125
     componentWillMount() {
98
-        const { dispatch } = this._getStore();
126
+        this.init.then(() => {
127
+            const { dispatch } = this._getStore();
128
+
129
+            dispatch(appWillMount(this));
130
+
131
+            // FIXME I believe it makes more sense for a middleware to dispatch
132
+            // localParticipantJoined on APP_WILL_MOUNT because the order of
133
+            // actions is important, not the call site. Moreover, we've got
134
+            // localParticipant business logic in the React Component
135
+            // (i.e. UI) AbstractApp now.
136
+            let localParticipant = {};
137
+
138
+            if (typeof APP === 'object') {
139
+                localParticipant = {
140
+                    avatarID: APP.settings.getAvatarId(),
141
+                    avatarURL: APP.settings.getAvatarUrl(),
142
+                    email: APP.settings.getEmail(),
143
+                    name: APP.settings.getDisplayName()
144
+                };
145
+            }
99 146
 
100
-        dispatch(appWillMount(this));
101
-
102
-        // FIXME I believe it makes more sense for a middleware to dispatch
103
-        // localParticipantJoined on APP_WILL_MOUNT because the order of actions
104
-        // is important, not the call site. Moreover, we've got localParticipant
105
-        // business logic in the React Component (i.e. UI) AbstractApp now.
106
-        let localParticipant;
107
-
108
-        if (typeof APP === 'object') {
109
-            localParticipant = {
110
-                avatarID: APP.settings.getAvatarId(),
111
-                avatarURL: APP.settings.getAvatarUrl(),
112
-                email: APP.settings.getEmail(),
113
-                name: APP.settings.getDisplayName()
114
-            };
115
-        }
116
-        dispatch(localParticipantJoined(localParticipant));
147
+            // Profile is the new React compatible settings.
148
+            const profile = getProfile(this._getStore().getState());
149
+
150
+            Object.assign(localParticipant, {
151
+                email: profile.email,
152
+                name: profile.displayName
153
+            });
117 154
 
118
-        // If a URL was explicitly specified to this React Component, then open
119
-        // it; otherwise, use a default.
120
-        this._openURL(toURLString(this.props.url) || this._getDefaultURL());
155
+            // We set the initialized state here and not in the contructor to
156
+            // make sure that {@code componentWillMount} gets invoked before
157
+            // the app tries to render the actual app content.
158
+            this.setState({
159
+                appAsyncInitialized: true
160
+            });
161
+
162
+            dispatch(localParticipantJoined(localParticipant));
163
+
164
+            // If a URL was explicitly specified to this React Component,
165
+            // then open it; otherwise, use a default.
166
+            this._openURL(toURLString(this.props.url) || this._getDefaultURL());
167
+        });
121 168
     }
122 169
 
123 170
     /**
@@ -130,32 +177,34 @@ export class AbstractApp extends Component {
130 177
      * @returns {void}
131 178
      */
132 179
     componentWillReceiveProps(nextProps) {
133
-        // The consumer of this AbstractApp did not provide a redux store.
134
-        if (typeof nextProps.store === 'undefined'
135
-
136
-                // The consumer of this AbstractApp did provide a redux store
137
-                // before. Which means that the consumer changed their mind. In
138
-                // such a case this instance should create its own internal
139
-                // redux store. If the consumer did not provide a redux store
140
-                // before, then this instance is using its own internal redux
141
-                // store already.
142
-                && typeof this.props.store !== 'undefined') {
143
-            this.setState({
144
-                store: this._maybeCreateStore(nextProps)
145
-            });
146
-        }
180
+        this.init.then(() => {
181
+            // The consumer of this AbstractApp did not provide a redux store.
182
+            if (typeof nextProps.store === 'undefined'
183
+
184
+                    // The consumer of this AbstractApp did provide a redux
185
+                    // store before. Which means that the consumer changed
186
+                    // their mind. In such a case this instance should create
187
+                    // its own internal redux store. If the consumer did not
188
+                    // provide a redux store before, then this instance is
189
+                    // using its own internal redux store already.
190
+                    && typeof this.props.store !== 'undefined') {
191
+                this.setState({
192
+                    store: this._maybeCreateStore(nextProps)
193
+                });
194
+            }
147 195
 
148
-        // Deal with URL changes.
149
-        let { url } = nextProps;
196
+            // Deal with URL changes.
197
+            let { url } = nextProps;
150 198
 
151
-        url = toURLString(url);
152
-        if (toURLString(this.props.url) !== url
199
+            url = toURLString(url);
200
+            if (toURLString(this.props.url) !== url
153 201
 
154
-                // XXX Refer to the implementation of loadURLObject: in
155
-                // ios/sdk/src/JitsiMeetView.m for further information.
156
-                || this.props.timestamp !== nextProps.timestamp) {
157
-            this._openURL(url || this._getDefaultURL());
158
-        }
202
+                    // XXX Refer to the implementation of loadURLObject: in
203
+                    // ios/sdk/src/JitsiMeetView.m for further information.
204
+                    || this.props.timestamp !== nextProps.timestamp) {
205
+                this._openURL(url || this._getDefaultURL());
206
+            }
207
+        });
159 208
     }
160 209
 
161 210
     /**
@@ -188,6 +237,23 @@ export class AbstractApp extends Component {
188 237
         return undefined;
189 238
     }
190 239
 
240
+    /**
241
+     * Delays app start until the {@code Storage} implementation initialises.
242
+     * This is instantaneous on web, but is async on mobile.
243
+     *
244
+     * @private
245
+     * @returns {ReactElement}
246
+     */
247
+    _initStorage() {
248
+        return new Promise(resolve => {
249
+            if (window.localStorage._initializing) {
250
+                window.localStorage._inited.then(resolve);
251
+            } else {
252
+                resolve();
253
+            }
254
+        });
255
+    }
256
+
191 257
     /**
192 258
      * Implements React's {@link Component#render()}.
193 259
      *
@@ -195,10 +261,10 @@ export class AbstractApp extends Component {
195 261
      * @returns {ReactElement}
196 262
      */
197 263
     render() {
198
-        const { route } = this.state;
264
+        const { appAsyncInitialized, route } = this.state;
199 265
         const component = (route && route.component) || BlankPage;
200 266
 
201
-        if (component) {
267
+        if (appAsyncInitialized && component) {
202 268
             return (
203 269
                 <I18nextProvider i18n = { i18next }>
204 270
                     <Provider store = { this._getStore() }>
@@ -281,7 +347,7 @@ export class AbstractApp extends Component {
281 347
             middleware = compose(middleware, devToolsExtension());
282 348
         }
283 349
 
284
-        return createStore(reducer, middleware);
350
+        return createStore(reducer, getPersistedState(), middleware);
285 351
     }
286 352
 
287 353
     /**
@@ -305,7 +371,11 @@ export class AbstractApp extends Component {
305 371
             }
306 372
         }
307 373
 
308
-        return this.props.defaultURL || DEFAULT_URL;
374
+        const profileDefaultURL = getProfile(
375
+            this._getStore().getState()
376
+        ).defaultURL;
377
+
378
+        return this.props.defaultURL || profileDefaultURL || DEFAULT_URL;
309 379
     }
310 380
 
311 381
     /**

+ 46
- 27
react/features/base/lib-jitsi-meet/native/Storage.js 查看文件

@@ -33,34 +33,53 @@ export default class Storage {
33 33
         if (typeof this._keyPrefix !== 'undefined') {
34 34
             // Load all previously persisted data items from React Native's
35 35
             // AsyncStorage.
36
-            AsyncStorage.getAllKeys().then((...getAllKeysCallbackArgs) => {
37
-                // XXX The keys argument of getAllKeys' callback may or may not
38
-                // be preceded by an error argument.
39
-                const keys
40
-                    = getAllKeysCallbackArgs[getAllKeysCallbackArgs.length - 1]
41
-                        .filter(key => key.startsWith(this._keyPrefix));
42
-
43
-                AsyncStorage.multiGet(keys).then((...multiGetCallbackArgs) => {
44
-                    // XXX The result argument of multiGet may or may not be
45
-                    // preceded by an errors argument.
46
-                    const result
47
-                        = multiGetCallbackArgs[multiGetCallbackArgs.length - 1];
48
-                    const keyPrefixLength
49
-                        = this._keyPrefix && this._keyPrefix.length;
50
-
51
-                    // eslint-disable-next-line prefer-const
52
-                    for (let [ key, value ] of result) {
53
-                        key = key.substring(keyPrefixLength);
54
-
55
-                        // XXX The loading of the previously persisted data
56
-                        // items from AsyncStorage is asynchronous which means
57
-                        // that it is technically possible to invoke setItem
58
-                        // with a key before the key is loaded from
59
-                        // AsyncStorage.
60
-                        if (!this.hasOwnProperty(key)) {
61
-                            this[key] = value;
36
+
37
+            /**
38
+             * A flag to indicate that the async {@code AsyncStorage} is not
39
+             * initialized yet. This is native specific but it will work
40
+             * fine on web as well, as it will have no value (== false) there.
41
+             * This is required to be available as we need a sync way to check
42
+             * if the storage is inited or not.
43
+             */
44
+            this._initializing = true;
45
+
46
+            this._inited = new Promise(resolve => {
47
+                AsyncStorage.getAllKeys().then((...getAllKeysCallbackArgs) => {
48
+                    // XXX The keys argument of getAllKeys' callback may
49
+                    // or may not be preceded by an error argument.
50
+                    const keys
51
+                        = getAllKeysCallbackArgs[
52
+                            getAllKeysCallbackArgs.length - 1
53
+                        ].filter(key => key.startsWith(this._keyPrefix));
54
+
55
+                    AsyncStorage.multiGet(keys)
56
+                    .then((...multiGetCallbackArgs) => {
57
+                        // XXX The result argument of multiGet may or may not be
58
+                        // preceded by an errors argument.
59
+                        const result
60
+                            = multiGetCallbackArgs[
61
+                                multiGetCallbackArgs.length - 1
62
+                            ];
63
+                        const keyPrefixLength
64
+                            = this._keyPrefix && this._keyPrefix.length;
65
+
66
+                        // eslint-disable-next-line prefer-const
67
+                        for (let [ key, value ] of result) {
68
+                            key = key.substring(keyPrefixLength);
69
+
70
+                            // XXX The loading of the previously persisted data
71
+                            // items from AsyncStorage is asynchronous which
72
+                            // means that it is technically possible to invoke
73
+                            // setItem with a key before the key is loaded from
74
+                            // AsyncStorage.
75
+                            if (!this.hasOwnProperty(key)) {
76
+                                this[key] = value;
77
+                            }
62 78
                         }
63
-                    }
79
+
80
+                        this._initializing = false;
81
+                        resolve();
82
+                    });
64 83
                 });
65 84
             });
66 85
         }

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

@@ -0,0 +1,15 @@
1
+/**
2
+ * Create an action for when the local profile is updated.
3
+ *
4
+ * {
5
+ *     type: PROFILE_UPDATED,
6
+ *     profile: {
7
+ *         displayName: string,
8
+ *         defaultURL: URL,
9
+ *         email: string,
10
+ *         startWithAudioMuted: boolean,
11
+ *         startWithVideoMuted: boolean
12
+ *     }
13
+ * }
14
+ */
15
+export const PROFILE_UPDATED = Symbol('PROFILE_UPDATED');

+ 23
- 0
react/features/base/profile/actions.js 查看文件

@@ -0,0 +1,23 @@
1
+import { PROFILE_UPDATED } from './actionTypes';
2
+
3
+/**
4
+ * Create an action for when the local profile is updated.
5
+ *
6
+ * @param {Object} profile - The new profile data.
7
+ * @returns {{
8
+ *     type: UPDATE_PROFILE,
9
+ *     profile: {
10
+ *         displayName: string,
11
+ *         defaultURL: URL,
12
+ *         email: string,
13
+ *         startWithAudioMuted: boolean,
14
+ *         startWithVideoMuted: boolean
15
+ *     }
16
+ * }}
17
+ */
18
+export function updateProfile(profile) {
19
+    return {
20
+        type: PROFILE_UPDATED,
21
+        profile
22
+    };
23
+}

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

@@ -0,0 +1,15 @@
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 ? profileStateSlice.profile || {} : {};
15
+}

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

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

+ 43
- 0
react/features/base/profile/middleware.js 查看文件

@@ -0,0 +1,43 @@
1
+/* @flow */
2
+import { PROFILE_UPDATED } from './actionTypes';
3
+import MiddlewareRegistry from '../redux/MiddlewareRegistry';
4
+
5
+import { participantUpdated } from '../participants';
6
+import { getProfile } from '../profile';
7
+import { toState } from '../redux';
8
+
9
+/**
10
+ * A MiddleWare to update the local participant when the profile
11
+ * is updated.
12
+ *
13
+ * @param {Store} store - The redux store.
14
+ * @returns {Function}
15
+ */
16
+MiddlewareRegistry.register(store => next => action => {
17
+    const result = next(action);
18
+
19
+    switch (action.type) {
20
+    case PROFILE_UPDATED:
21
+        _updateLocalParticipant(store);
22
+    }
23
+
24
+    return result;
25
+});
26
+
27
+/**
28
+ * Updates the local participant according to profile changes.
29
+ *
30
+ * @param {Store} store - The redux store.
31
+ * @returns {void}
32
+ */
33
+function _updateLocalParticipant(store) {
34
+    const profile = getProfile(toState(store));
35
+
36
+    const newLocalParticipant = {
37
+        email: profile.email,
38
+        local: true,
39
+        name: profile.displayName
40
+    };
41
+
42
+    store.dispatch(participantUpdated(newLocalParticipant));
43
+}

+ 25
- 0
react/features/base/profile/reducer.js 查看文件

@@ -0,0 +1,25 @@
1
+// @flow
2
+
3
+import {
4
+    PROFILE_UPDATED
5
+} from './actionTypes';
6
+
7
+import { ReducerRegistry } from '../redux';
8
+
9
+const DEFAULT_STATE = {
10
+    profile: {}
11
+};
12
+
13
+const STORE_NAME = 'features/base/profile';
14
+
15
+ReducerRegistry.register(
16
+    STORE_NAME, (state = DEFAULT_STATE, action) => {
17
+        switch (action.type) {
18
+        case PROFILE_UPDATED:
19
+            return {
20
+                profile: action.profile
21
+            };
22
+        }
23
+
24
+        return state;
25
+    });

+ 93
- 0
react/features/base/redux/functions.js 查看文件

@@ -1,6 +1,12 @@
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';
4 10
 
5 11
 /**
6 12
  * Sets specific properties of a specific state to specific values and prevents
@@ -38,6 +44,93 @@ export function equals(a: any, b: any) {
38 44
     return _.isEqual(a, b);
39 45
 }
40 46
 
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
+
41 134
 /**
42 135
  * Sets a specific property of a specific state to a specific value. Prevents
43 136
  * unnecessary state changes (when the specified {@code value} is equal to the

+ 2
- 0
react/features/base/redux/index.js 查看文件

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

+ 36
- 0
react/features/base/redux/middleware.js 查看文件

@@ -0,0 +1,36 @@
1
+/* @flow */
2
+import _ from 'lodash';
3
+
4
+import { persistState } from './functions';
5
+import MiddlewareRegistry from './MiddlewareRegistry';
6
+
7
+import { toState } from '../redux';
8
+
9
+/**
10
+ * The delay that passes between the last state change and the state to be
11
+ * persisted in the storage.
12
+ */
13
+const PERSIST_DELAY = 2000;
14
+
15
+/**
16
+ * A throttled function to avoid repetitive state persisting.
17
+ */
18
+const throttledFunc = _.throttle(state => {
19
+    persistState(state);
20
+}, PERSIST_DELAY);
21
+
22
+/**
23
+ * A master MiddleWare to selectively persist state. Please use the
24
+ * {@link persisterconfig.json} to set which subtrees of the Redux state
25
+ * should be persisted.
26
+ *
27
+ * @param {Store} store - The redux store.
28
+ * @returns {Function}
29
+ */
30
+MiddlewareRegistry.register(store => next => action => {
31
+    const result = next(action);
32
+
33
+    throttledFunc(toState(store));
34
+
35
+    return result;
36
+});

+ 5
- 0
react/features/base/redux/persisterconfig.json 查看文件

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

+ 36
- 0
react/features/base/redux/readme.md 查看文件

@@ -0,0 +1,36 @@
1
+Jitsi Meet - redux state persistency
2
+====================================
3
+Jitsi Meet has a persistency layer that persist a subtree (or specific subtrees) into window.localStorage (on web) or
4
+AsyncStorage (on mobile).
5
+
6
+Usage
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
10
+
11
+```
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.
28
+
29
+Throttling
30
+==========
31
+To avoid too frequent write operations in the storage, we utilise throttling in the persistency layer, meaning that the storage
32
+gets persisted only once in every 2 seconds, even if multiple redux state changes occur during this period. This throttling timeout
33
+can be configured in
34
+```
35
+react/features/base/redux/middleware.js#PERSIST_DELAY
36
+```

+ 14
- 0
react/features/welcome/components/AbstractWelcomePage.js 查看文件

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
4 4
 import { Component } from 'react';
5 5
 
6 6
 import { appNavigate } from '../../app';
7
+import { showAppSettings } from '../../app-settings';
7 8
 import { isRoomValid } from '../../base/conference';
8 9
 
9 10
 import { generateRoomWithoutSeparator } from '../functions';
@@ -70,6 +71,7 @@ export class AbstractWelcomePage extends Component<*, *> {
70 71
             = this._animateRoomnameChanging.bind(this);
71 72
         this._onJoin = this._onJoin.bind(this);
72 73
         this._onRoomChange = this._onRoomChange.bind(this);
74
+        this._onSettingsOpen = this._onSettingsOpen.bind(this);
73 75
         this._updateRoomname = this._updateRoomname.bind(this);
74 76
     }
75 77
 
@@ -196,6 +198,18 @@ export class AbstractWelcomePage extends Component<*, *> {
196 198
         this.setState({ room: value });
197 199
     }
198 200
 
201
+    _onSettingsOpen: () => void;
202
+
203
+    /**
204
+    * Sets the app settings modal visible.
205
+    *
206
+    * @protected
207
+    * @returns {void}
208
+    */
209
+    _onSettingsOpen() {
210
+        this.props.dispatch(showAppSettings());
211
+    }
212
+
199 213
     _updateRoomname: () => void;
200 214
 
201 215
     /**

+ 18
- 4
react/features/welcome/components/WelcomePage.native.js 查看文件

@@ -2,6 +2,8 @@ import React from 'react';
2 2
 import { TextInput, TouchableHighlight, View } from 'react-native';
3 3
 import { connect } from 'react-redux';
4 4
 
5
+import { AppSettings } from '../../app-settings';
6
+import { Icon } from '../../base/font-icons';
5 7
 import { translate } from '../../base/i18n';
6 8
 import { MEDIA_TYPE } from '../../base/media';
7 9
 import { Link, LoadingIndicator, Text } from '../../base/react';
@@ -80,11 +82,23 @@ class WelcomePage extends AbstractWelcomePage {
80 82
                         style = { styles.textInput }
81 83
                         underlineColorAndroid = 'transparent'
82 84
                         value = { this.state.room } />
83
-                    {
84
-                        this._renderJoinButton()
85
-                    }
85
+                    <View style = { styles.buttonRow }>
86
+                        <TouchableHighlight
87
+                            accessibilityLabel = { 'Tap for Settings.' }
88
+                            onPress = { this._onSettingsOpen }
89
+                            style = { [ styles.button, styles.settingsButton ] }
90
+                            underlayColor = { ColorPalette.white }>
91
+                            <Icon
92
+                                name = 'settings'
93
+                                style = { styles.settingsIcon } />
94
+                        </TouchableHighlight>
95
+                        {
96
+                            this._renderJoinButton()
97
+                        }
98
+                    </View>
86 99
                     <RecentList />
87 100
                 </View>
101
+                <AppSettings />
88 102
                 {
89 103
                     this._renderLegalese()
90 104
                 }
@@ -127,7 +141,7 @@ class WelcomePage extends AbstractWelcomePage {
127 141
                 accessibilityLabel = { 'Tap to Join.' }
128 142
                 disabled = { this._isJoinDisabled() }
129 143
                 onPress = { this._onJoin }
130
-                style = { styles.button }
144
+                style = { [ styles.button, styles.joinButton ] }
131 145
                 underlayColor = { ColorPalette.white }>
132 146
                 {
133 147
                     children

+ 30
- 0
react/features/welcome/components/styles.js 查看文件

@@ -37,6 +37,13 @@ export default createStyleSheet({
37 37
         marginTop: BoxModel.margin
38 38
     },
39 39
 
40
+    /**
41
+    * Layout of the button container.
42
+    */
43
+    buttonRow: {
44
+        flexDirection: 'row'
45
+    },
46
+
40 47
     /**
41 48
      * Join button text style.
42 49
      */
@@ -46,6 +53,13 @@ export default createStyleSheet({
46 53
         fontSize: 18
47 54
     },
48 55
 
56
+    /**
57
+    * Style of the join button.
58
+    */
59
+    joinButton: {
60
+        flex: 1
61
+    },
62
+
49 63
     /**
50 64
      * The style of the legal-related content such as (hyper)links to Privacy
51 65
      * Policy and Terms of Service displayed on the WelcomePage.
@@ -111,6 +125,22 @@ export default createStyleSheet({
111 125
         marginTop: 5 * BoxModel.margin
112 126
     },
113 127
 
128
+    /**
129
+    * Style of the settings button.
130
+    */
131
+    settingsButton: {
132
+        width: 65,
133
+        marginRight: BoxModel.margin
134
+    },
135
+
136
+    /**
137
+    * Style of the settings icon on the settings button.
138
+    */
139
+    settingsIcon: {
140
+        fontSize: 24,
141
+        alignSelf: 'center'
142
+    },
143
+
114 144
     /**
115 145
      * Room input style.
116 146
      */

Loading…
取消
儲存