Procházet zdrojové kódy

rn: add a new advanced settings section

Currently only 2 options are implemented, mainly aimed at helping troubleshoot
audio related problems:

- Disable native call integration (it disables CallKit / ConnectionService)
- Disable P2P
master
Saúl Ibarra Corretgé před 5 roky
rodič
revize
6d16e087d9

+ 4
- 0
lang/main.json Zobrazit soubor

@@ -526,16 +526,20 @@
526 526
         "title": "Settings"
527 527
     },
528 528
     "settingsView": {
529
+        "advanced": "Advanced",
529 530
         "alertOk": "OK",
530 531
         "alertTitle": "Warning",
531 532
         "alertURLText": "The entered server URL is invalid",
532 533
         "buildInfoSection": "Build Information",
533 534
         "conferenceSection": "Conference",
535
+        "disableCallIntegration": "Disable native call integration",
536
+        "disableP2P": "Disable Peer-To-Peer mode",
534 537
         "displayName": "Display name",
535 538
         "email": "Email",
536 539
         "header": "Settings",
537 540
         "profileSection": "Profile",
538 541
         "serverURL": "Server URL",
542
+        "showAdvanced": "Show advanced settings",
539 543
         "startWithAudioMuted": "Start with audio muted",
540 544
         "startWithVideoMuted": "Start with video muted",
541 545
         "version": "Version"

+ 8
- 1
react/features/app/components/App.native.js Zobrazit soubor

@@ -6,7 +6,7 @@ import '../../analytics';
6 6
 import '../../authentication';
7 7
 import { setColorScheme } from '../../base/color-scheme';
8 8
 import { DialogContainer } from '../../base/dialog';
9
-import { updateFlags } from '../../base/flags';
9
+import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
10 10
 import '../../base/jwt';
11 11
 import { Platform } from '../../base/react';
12 12
 import {
@@ -100,6 +100,13 @@ export class App extends AbstractApp {
100 100
             dispatch(setColorScheme(this.props.colorScheme));
101 101
             dispatch(updateFlags(this.props.flags));
102 102
             dispatch(updateSettings(this.props.userInfo || {}));
103
+
104
+            // Update settings with feature-flag.
105
+            const callIntegrationEnabled = this.props.flags[CALL_INTEGRATION_ENABLED];
106
+
107
+            if (typeof callIntegrationEnabled !== 'undefined') {
108
+                dispatch(updateSettings({ disableCallIntegration: !callIntegrationEnabled }));
109
+            }
103 110
         });
104 111
     }
105 112
 

+ 14
- 0
react/features/base/config/actionTypes.js Zobrazit soubor

@@ -34,3 +34,17 @@ export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
34 34
  * }
35 35
  */
36 36
 export const SET_CONFIG = 'SET_CONFIG';
37
+
38
+/**
39
+ * The redux action which updates the configuration represented by the feature
40
+ * base/config. The configuration is defined and consumed by the library
41
+ * lib-jitsi-meet but some of its properties are consumed by the application
42
+ * jitsi-meet as well. A merge operation is performed between the existing config
43
+ * and the passed object.
44
+ *
45
+ * {
46
+ *     type: _UPDATE_CONFIG,
47
+ *     config: Object
48
+ * }
49
+ */
50
+export const _UPDATE_CONFIG = '_UPDATE_CONFIG';

+ 17
- 3
react/features/base/config/middleware.js Zobrazit soubor

@@ -5,7 +5,7 @@ import { addKnownDomains } from '../known-domains';
5 5
 import { MiddlewareRegistry } from '../redux';
6 6
 import { parseURIString } from '../util';
7 7
 
8
-import { SET_CONFIG } from './actionTypes';
8
+import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
9 9
 import { _CONFIG_STORE_PREFIX } from './constants';
10 10
 
11 11
 /**
@@ -93,11 +93,25 @@ function _appWillMount(store, next, action) {
93 93
  * @private
94 94
  * @returns {*} The return value of {@code next(action)}.
95 95
  */
96
-function _setConfig({ getState }, next, action) {
96
+function _setConfig({ dispatch, getState }, next, action) {
97 97
     // The reducer is doing some alterations to the config passed in the action,
98 98
     // so make sure it's the final state by waiting for the action to be
99 99
     // reduced.
100 100
     const result = next(action);
101
+    const state = getState();
102
+
103
+    // Update the config with user defined settings.
104
+    const settings = state['features/base/settings'];
105
+    const config = {};
106
+
107
+    if (typeof settings.disableP2P !== 'undefined') {
108
+        config.p2p = { enabled: !settings.disableP2P };
109
+    }
110
+
111
+    dispatch({
112
+        type: _UPDATE_CONFIG,
113
+        config
114
+    });
101 115
 
102 116
     // FIXME On Web we rely on the global 'config' variable which gets altered
103 117
     // multiple times, before it makes it to the reducer. At some point it may
@@ -105,7 +119,7 @@ function _setConfig({ getState }, next, action) {
105 119
     // different merge methods being used along the way. The global variable
106 120
     // must be synchronized with the final state resolved by the reducer.
107 121
     if (typeof window.config !== 'undefined') {
108
-        window.config = getState()['features/base/config'];
122
+        window.config = state['features/base/config'];
109 123
     }
110 124
 
111 125
     return result;

+ 58
- 42
react/features/base/config/reducer.js Zobrazit soubor

@@ -5,7 +5,7 @@ import _ from 'lodash';
5 5
 import Platform from '../react/Platform';
6 6
 import { equals, ReducerRegistry, set } from '../redux';
7 7
 
8
-import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
8
+import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
9 9
 import { _cleanupConfig } from './functions';
10 10
 
11 11
 /**
@@ -56,49 +56,50 @@ const INITIAL_RN_STATE = {
56 56
     }
57 57
 };
58 58
 
59
-ReducerRegistry.register(
60
-    'features/base/config',
61
-    (state = _getInitialState(), action) => {
62
-        switch (action.type) {
63
-        case CONFIG_WILL_LOAD:
59
+ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
60
+    switch (action.type) {
61
+    case _UPDATE_CONFIG:
62
+        return _updateConfig(state, action);
63
+
64
+    case CONFIG_WILL_LOAD:
65
+        return {
66
+            error: undefined,
67
+
68
+            /**
69
+                * The URL of the location associated with/configured by this
70
+                * configuration.
71
+                *
72
+                * @type URL
73
+                */
74
+            locationURL: action.locationURL
75
+        };
76
+
77
+    case LOAD_CONFIG_ERROR:
78
+        // XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
79
+        // the asynchronous "loadConfig procedure/process" started with
80
+        // CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
81
+        // is settling the process needs to provide proof that they have
82
+        // started it and that the iteration of the process being completed
83
+        // now is still of interest to the app.
84
+        if (state.locationURL === action.locationURL) {
64 85
             return {
65
-                error: undefined,
66
-
67 86
                 /**
68
-                 * The URL of the location associated with/configured by this
69
-                 * configuration.
70
-                 *
71
-                 * @type URL
72
-                 */
73
-                locationURL: action.locationURL
87
+                    * The {@link Error} which prevented the loading of the
88
+                    * configuration of the associated {@code locationURL}.
89
+                    *
90
+                    * @type Error
91
+                    */
92
+                error: action.error
74 93
             };
75
-
76
-        case LOAD_CONFIG_ERROR:
77
-            // XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
78
-            // the asynchronous "loadConfig procedure/process" started with
79
-            // CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
80
-            // is settling the process needs to provide proof that they have
81
-            // started it and that the iteration of the process being completed
82
-            // now is still of interest to the app.
83
-            if (state.locationURL === action.locationURL) {
84
-                return {
85
-                    /**
86
-                     * The {@link Error} which prevented the loading of the
87
-                     * configuration of the associated {@code locationURL}.
88
-                     *
89
-                     * @type Error
90
-                     */
91
-                    error: action.error
92
-                };
93
-            }
94
-            break;
95
-
96
-        case SET_CONFIG:
97
-            return _setConfig(state, action);
98 94
         }
95
+        break;
99 96
 
100
-        return state;
101
-    });
97
+    case SET_CONFIG:
98
+        return _setConfig(state, action);
99
+    }
100
+
101
+    return state;
102
+});
102 103
 
103 104
 /**
104 105
  * Gets the initial state of the feature base/config. The mandatory
@@ -120,11 +121,10 @@ function _getInitialState() {
120 121
  * Reduces a specific Redux action SET_CONFIG of the feature
121 122
  * base/lib-jitsi-meet.
122 123
  *
123
- * @param {Object} state - The Redux state of the feature base/lib-jitsi-meet.
124
+ * @param {Object} state - The Redux state of the feature base/config.
124 125
  * @param {Action} action - The Redux action SET_CONFIG to reduce.
125 126
  * @private
126
- * @returns {Object} The new state of the feature base/lib-jitsi-meet after the
127
- * reduction of the specified action.
127
+ * @returns {Object} The new state after the reduction of the specified action.
128 128
  */
129 129
 function _setConfig(state, { config }) {
130 130
     // The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
@@ -207,3 +207,19 @@ function _translateLegacyConfig(oldValue: Object) {
207 207
 
208 208
     return newValue;
209 209
 }
210
+
211
+/**
212
+ * Updates the stored configuration with the given extra options.
213
+ *
214
+ * @param {Object} state - The Redux state of the feature base/config.
215
+ * @param {Action} action - The Redux action to reduce.
216
+ * @private
217
+ * @returns {Object} The new state after the reduction of the specified action.
218
+ */
219
+function _updateConfig(state, { config }) {
220
+    const newState = _.merge({}, state, config);
221
+
222
+    _cleanupConfig(newState);
223
+
224
+    return equals(state, newState) ? state : newState;
225
+}

+ 7
- 0
react/features/base/flags/constants.js Zobrazit soubor

@@ -6,6 +6,13 @@
6 6
  */
7 7
 export const CALENDAR_ENABLED = 'calendar.enabled';
8 8
 
9
+/**
10
+ * Flag indicating if call integration (CallKit on iOS, ConnectionService on Android)
11
+ * should be enabled.
12
+ * Default: enabled (true).
13
+ */
14
+export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
15
+
9 16
 /**
10 17
  * Flag indicating if chat should be enabled.
11 18
  * Default: enabled (true).

react/features/base/settings/functions.js → react/features/base/settings/functions.any.js Zobrazit soubor


+ 21
- 0
react/features/base/settings/functions.native.js Zobrazit soubor

@@ -0,0 +1,21 @@
1
+// @flow
2
+
3
+import { NativeModules } from 'react-native';
4
+
5
+export * from './functions.any';
6
+
7
+const { AudioMode } = NativeModules;
8
+
9
+/**
10
+ * Handles changes to the `disableCallIntegration` setting.
11
+ * On Android (where `AudioMode.setUseConnectionService` is defined) we must update
12
+ * the native side too, since audio routing works differently.
13
+ *
14
+ * @param {boolean} disabled - Whether call integration is disabled or not.
15
+ * @returns {void}
16
+ */
17
+export function handleCallIntegrationChange(disabled: boolean) {
18
+    if (AudioMode.setUseConnectionService) {
19
+        AudioMode.setUseConnectionService(!disabled);
20
+    }
21
+}

+ 13
- 0
react/features/base/settings/functions.web.js Zobrazit soubor

@@ -0,0 +1,13 @@
1
+// @flow
2
+
3
+export * from './functions.any';
4
+
5
+/**
6
+ * Handles changes to the `disableCallIntegration` setting.
7
+ * Noop on web.
8
+ *
9
+ * @param {boolean} disabled - Whether call integration is disabled or not.
10
+ * @returns {void}
11
+ */
12
+export function handleCallIntegrationChange(disabled: boolean) { // eslint-disable-line no-unused-vars
13
+}

+ 15
- 0
react/features/base/settings/middleware.js Zobrazit soubor

@@ -5,6 +5,7 @@ import { getLocalParticipant, participantUpdated } from '../participants';
5 5
 import { MiddlewareRegistry } from '../redux';
6 6
 
7 7
 import { SETTINGS_UPDATED } from './actionTypes';
8
+import { handleCallIntegrationChange } from './functions';
8 9
 
9 10
 /**
10 11
  * The middleware of the feature base/settings. Distributes changes to the state
@@ -19,6 +20,7 @@ MiddlewareRegistry.register(store => next => action => {
19 20
 
20 21
     switch (action.type) {
21 22
     case SETTINGS_UPDATED:
23
+        _maybeHandleCallIntegrationChange(action);
22 24
         _maybeSetAudioOnly(store, action);
23 25
         _updateLocalParticipant(store, action);
24 26
     }
@@ -43,6 +45,19 @@ function _mapSettingsFieldToParticipant(settingsField) {
43 45
     return settingsField;
44 46
 }
45 47
 
48
+/**
49
+ * Updates {@code startAudioOnly} flag if it's updated in the settings.
50
+ *
51
+ * @param {Object} action - The redux action.
52
+ * @private
53
+ * @returns {void}
54
+ */
55
+function _maybeHandleCallIntegrationChange({ settings: { disableCallIntegration } }) {
56
+    if (typeof disableCallIntegration === 'boolean') {
57
+        handleCallIntegrationChange(disableCallIntegration);
58
+    }
59
+}
60
+
46 61
 /**
47 62
  * Updates {@code startAudioOnly} flag if it's updated in the settings.
48 63
  *

+ 2
- 0
react/features/base/settings/reducer.js Zobrazit soubor

@@ -22,6 +22,8 @@ const DEFAULT_STATE = {
22 22
     avatarID: undefined,
23 23
     avatarURL: undefined,
24 24
     cameraDeviceId: undefined,
25
+    disableCallIntegration: undefined,
26
+    disableP2P: undefined,
25 27
     displayName: undefined,
26 28
     email: undefined,
27 29
     localFlipX: true,

+ 20
- 0
react/features/mobile/call-integration/functions.js Zobrazit soubor

@@ -0,0 +1,20 @@
1
+// @flow
2
+
3
+import { CALL_INTEGRATION_ENABLED, getFeatureFlag } from '../../base/flags';
4
+import { toState } from '../../base/redux';
5
+
6
+/**
7
+ * Checks if call integration is enabled or not.
8
+ *
9
+ * @param {Function|Object} stateful - The redux store or {@code getState}
10
+ * function.
11
+ * @returns {string} - Default URL for the app.
12
+ */
13
+export function isCallIntegrationEnabled(stateful: Function | Object) {
14
+    const state = toState(stateful);
15
+    const { disableCallIntegration } = state['features/base/settings'];
16
+    const flag = getFeatureFlag(state, CALL_INTEGRATION_ENABLED);
17
+
18
+    // The feature flag has precedence.
19
+    return flag ?? !disableCallIntegration;
20
+}

+ 29
- 2
react/features/mobile/call-integration/middleware.js Zobrazit soubor

@@ -34,6 +34,7 @@ import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
34 34
 
35 35
 import CallKit from './CallKit';
36 36
 import ConnectionService from './ConnectionService';
37
+import { isCallIntegrationEnabled } from './functions';
37 38
 
38 39
 const CallIntegration = CallKit || ConnectionService;
39 40
 
@@ -139,9 +140,13 @@ function _appWillMount({ dispatch, getState }, next, action) {
139 140
  * @private
140 141
  * @returns {*} The value returned by {@code next(action)}.
141 142
  */
142
-function _conferenceFailed(store, next, action) {
143
+function _conferenceFailed({ getState }, next, action) {
143 144
     const result = next(action);
144 145
 
146
+    if (!isCallIntegrationEnabled(getState)) {
147
+        return result;
148
+    }
149
+
145 150
     // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
146 151
     // prevented the user from joining a specific conference but the app may be
147 152
     // able to eventually join the conference.
@@ -173,6 +178,10 @@ function _conferenceFailed(store, next, action) {
173 178
 function _conferenceJoined({ getState }, next, action) {
174 179
     const result = next(action);
175 180
 
181
+    if (!isCallIntegrationEnabled(getState)) {
182
+        return result;
183
+    }
184
+
176 185
     const { callUUID } = action.conference;
177 186
 
178 187
     if (callUUID) {
@@ -201,9 +210,13 @@ function _conferenceJoined({ getState }, next, action) {
201 210
  * @private
202 211
  * @returns {*} The value returned by {@code next(action)}.
203 212
  */
204
-function _conferenceLeft(store, next, action) {
213
+function _conferenceLeft({ getState }, next, action) {
205 214
     const result = next(action);
206 215
 
216
+    if (!isCallIntegrationEnabled(getState)) {
217
+        return result;
218
+    }
219
+
207 220
     const { callUUID } = action.conference;
208 221
 
209 222
     if (callUUID) {
@@ -230,6 +243,10 @@ function _conferenceLeft(store, next, action) {
230 243
 function _conferenceWillJoin({ dispatch, getState }, next, action) {
231 244
     const result = next(action);
232 245
 
246
+    if (!isCallIntegrationEnabled(getState)) {
247
+        return result;
248
+    }
249
+
233 250
     const { conference } = action;
234 251
     const state = getState();
235 252
     const { callHandle, callUUID } = state['features/base/config'];
@@ -341,6 +358,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
341 358
 function _setAudioOnly({ getState }, next, action) {
342 359
     const result = next(action);
343 360
     const state = getState();
361
+
362
+    if (!isCallIntegrationEnabled(state)) {
363
+        return result;
364
+    }
365
+
344 366
     const conference = getCurrentConference(state);
345 367
 
346 368
     if (conference && conference.callUUID) {
@@ -393,6 +415,11 @@ function _setCallKitSubscriptions({ getState }, next, action) {
393 415
  */
394 416
 function _syncTrackState({ getState }, next, action) {
395 417
     const result = next(action);
418
+
419
+    if (!isCallIntegrationEnabled(getState)) {
420
+        return result;
421
+    }
422
+
396 423
     const { jitsiTrack } = action.track;
397 424
     const state = getState();
398 425
     const conference = getCurrentConference(state);

+ 1
- 1
react/features/settings/components/AbstractSettingsView.js Zobrazit soubor

@@ -48,7 +48,7 @@ export type Props = {
48 48
  *
49 49
  * @abstract
50 50
  */
51
-export class AbstractSettingsView<P: Props> extends Component<P> {
51
+export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
52 52
 
53 53
     /**
54 54
      * Initializes a new {@code AbstractSettingsView} instance.

+ 107
- 2
react/features/settings/components/native/SettingsView.js Zobrazit soubor

@@ -32,12 +32,20 @@ type Props = AbstractProps & {
32 32
     _headerStyles: Object
33 33
 }
34 34
 
35
+type State = {
36
+
37
+    /**
38
+     * Whether to show advanced settings or not.
39
+     */
40
+    showAdvanced: boolean
41
+}
42
+
35 43
 /**
36 44
  * The native container rendering the app settings page.
37 45
  *
38 46
  * @extends AbstractSettingsView
39 47
  */
40
-class SettingsView extends AbstractSettingsView<Props> {
48
+class SettingsView extends AbstractSettingsView<Props, State> {
41 49
     _urlField: Object;
42 50
 
43 51
     /**
@@ -48,9 +56,16 @@ class SettingsView extends AbstractSettingsView<Props> {
48 56
     constructor(props) {
49 57
         super(props);
50 58
 
59
+        this.state = {
60
+            showAdvanced: false
61
+        };
62
+
51 63
         // Bind event handlers so they are only bound once per instance.
52 64
         this._onBlurServerURL = this._onBlurServerURL.bind(this);
65
+        this._onDisableCallIntegration = this._onDisableCallIntegration.bind(this);
66
+        this._onDisableP2P = this._onDisableP2P.bind(this);
53 67
         this._onRequestClose = this._onRequestClose.bind(this);
68
+        this._onShowAdvanced = this._onShowAdvanced.bind(this);
54 69
         this._setURLFieldReference = this._setURLFieldReference.bind(this);
55 70
         this._showURLAlert = this._showURLAlert.bind(this);
56 71
     }
@@ -94,6 +109,38 @@ class SettingsView extends AbstractSettingsView<Props> {
94 109
 
95 110
     _onChangeServerURL: (string) => void;
96 111
 
112
+    _onDisableCallIntegration: (boolean) => void;
113
+
114
+    /**
115
+     * Handles the disable call integration change event.
116
+     *
117
+     * @param {boolean} newValue - The new value
118
+     * option.
119
+     * @private
120
+     * @returns {void}
121
+     */
122
+    _onDisableCallIntegration(newValue) {
123
+        this._updateSettings({
124
+            disableCallIntegration: newValue
125
+        });
126
+    }
127
+
128
+    _onDisableP2P: (boolean) => void;
129
+
130
+    /**
131
+     * Handles the disable P2P change event.
132
+     *
133
+     * @param {boolean} newValue - The new value
134
+     * option.
135
+     * @private
136
+     * @returns {void}
137
+     */
138
+    _onDisableP2P(newValue) {
139
+        this._updateSettings({
140
+            disableP2P: newValue
141
+        });
142
+    }
143
+
97 144
     _onRequestClose: () => void;
98 145
 
99 146
     /**
@@ -103,9 +150,21 @@ class SettingsView extends AbstractSettingsView<Props> {
103 150
      * @returns {void}
104 151
      */
105 152
     _onRequestClose() {
153
+        this.setState({ showAdvanced: false });
106 154
         this._processServerURL(true /* hideOnSuccess */);
107 155
     }
108 156
 
157
+    _onShowAdvanced: () => void;
158
+
159
+    /**
160
+     * Handles the advanced settings button.
161
+     *
162
+     * @returns {void}
163
+     */
164
+    _onShowAdvanced() {
165
+        this.setState({ showAdvanced: !this.state.showAdvanced });
166
+    }
167
+
109 168
     _onStartAudioMutedChange: (boolean) => void;
110 169
 
111 170
     _onStartVideoMutedChange: (boolean) => void;
@@ -133,6 +192,48 @@ class SettingsView extends AbstractSettingsView<Props> {
133 192
         }
134 193
     }
135 194
 
195
+    /**
196
+     * Renders the advanced settings options.
197
+     *
198
+     * @private
199
+     * @returns {React$Element}
200
+     */
201
+    _renderAdvancedSettings() {
202
+        const { _settings } = this.props;
203
+        const { showAdvanced } = this.state;
204
+
205
+        if (!showAdvanced) {
206
+            return (
207
+                <FormRow
208
+                    fieldSeparator = { true }
209
+                    label = 'settingsView.showAdvanced'>
210
+                    <Switch
211
+                        onValueChange = { this._onShowAdvanced }
212
+                        value = { showAdvanced } />
213
+                </FormRow>
214
+            );
215
+        }
216
+
217
+        return (
218
+            <>
219
+                <FormRow
220
+                    fieldSeparator = { true }
221
+                    label = 'settingsView.disableCallIntegration'>
222
+                    <Switch
223
+                        onValueChange = { this._onDisableCallIntegration }
224
+                        value = { _settings.disableCallIntegration } />
225
+                </FormRow>
226
+                <FormRow
227
+                    fieldSeparator = { true }
228
+                    label = 'settingsView.disableP2P'>
229
+                    <Switch
230
+                        onValueChange = { this._onDisableP2P }
231
+                        value = { _settings.disableP2P } />
232
+                </FormRow>
233
+            </>
234
+        );
235
+    }
236
+
136 237
     /**
137 238
      * Renders the body (under the header) of {@code SettingsView}.
138 239
      *
@@ -193,12 +294,14 @@ class SettingsView extends AbstractSettingsView<Props> {
193 294
                     <FormSectionHeader
194 295
                         label = 'settingsView.buildInfoSection' />
195 296
                     <FormRow
196
-                        fieldSeparator = { true }
197 297
                         label = 'settingsView.version'>
198 298
                         <Text>
199 299
                             { `${AppInfo.version} build ${AppInfo.buildNumber}` }
200 300
                         </Text>
201 301
                     </FormRow>
302
+                    <FormSectionHeader
303
+                        label = 'settingsView.advanced' />
304
+                    { this._renderAdvancedSettings() }
202 305
                 </ScrollView>
203 306
             </SafeAreaView>
204 307
         );
@@ -252,6 +355,8 @@ class SettingsView extends AbstractSettingsView<Props> {
252 355
             ]
253 356
         );
254 357
     }
358
+
359
+    _updateSettings: (Object) => void;
255 360
 }
256 361
 
257 362
 /**

Načítá se…
Zrušit
Uložit