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
This commit is contained in:
parent
fe90e5aa8f
commit
6d16e087d9
|
@ -526,16 +526,20 @@
|
|||
"title": "Settings"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Advanced",
|
||||
"alertOk": "OK",
|
||||
"alertTitle": "Warning",
|
||||
"alertURLText": "The entered server URL is invalid",
|
||||
"buildInfoSection": "Build Information",
|
||||
"conferenceSection": "Conference",
|
||||
"disableCallIntegration": "Disable native call integration",
|
||||
"disableP2P": "Disable Peer-To-Peer mode",
|
||||
"displayName": "Display name",
|
||||
"email": "Email",
|
||||
"header": "Settings",
|
||||
"profileSection": "Profile",
|
||||
"serverURL": "Server URL",
|
||||
"showAdvanced": "Show advanced settings",
|
||||
"startWithAudioMuted": "Start with audio muted",
|
||||
"startWithVideoMuted": "Start with video muted",
|
||||
"version": "Version"
|
||||
|
|
|
@ -6,7 +6,7 @@ import '../../analytics';
|
|||
import '../../authentication';
|
||||
import { setColorScheme } from '../../base/color-scheme';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { updateFlags } from '../../base/flags';
|
||||
import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
|
||||
import '../../base/jwt';
|
||||
import { Platform } from '../../base/react';
|
||||
import {
|
||||
|
@ -100,6 +100,13 @@ export class App extends AbstractApp {
|
|||
dispatch(setColorScheme(this.props.colorScheme));
|
||||
dispatch(updateFlags(this.props.flags));
|
||||
dispatch(updateSettings(this.props.userInfo || {}));
|
||||
|
||||
// Update settings with feature-flag.
|
||||
const callIntegrationEnabled = this.props.flags[CALL_INTEGRATION_ENABLED];
|
||||
|
||||
if (typeof callIntegrationEnabled !== 'undefined') {
|
||||
dispatch(updateSettings({ disableCallIntegration: !callIntegrationEnabled }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,3 +34,17 @@ export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
|
|||
* }
|
||||
*/
|
||||
export const SET_CONFIG = 'SET_CONFIG';
|
||||
|
||||
/**
|
||||
* The redux action which updates the configuration represented by the feature
|
||||
* base/config. The configuration is defined and consumed by the library
|
||||
* lib-jitsi-meet but some of its properties are consumed by the application
|
||||
* jitsi-meet as well. A merge operation is performed between the existing config
|
||||
* and the passed object.
|
||||
*
|
||||
* {
|
||||
* type: _UPDATE_CONFIG,
|
||||
* config: Object
|
||||
* }
|
||||
*/
|
||||
export const _UPDATE_CONFIG = '_UPDATE_CONFIG';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { addKnownDomains } from '../known-domains';
|
|||
import { MiddlewareRegistry } from '../redux';
|
||||
import { parseURIString } from '../util';
|
||||
|
||||
import { SET_CONFIG } from './actionTypes';
|
||||
import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
|
||||
import { _CONFIG_STORE_PREFIX } from './constants';
|
||||
|
||||
/**
|
||||
|
@ -93,11 +93,25 @@ function _appWillMount(store, next, action) {
|
|||
* @private
|
||||
* @returns {*} The return value of {@code next(action)}.
|
||||
*/
|
||||
function _setConfig({ getState }, next, action) {
|
||||
function _setConfig({ dispatch, getState }, next, action) {
|
||||
// The reducer is doing some alterations to the config passed in the action,
|
||||
// so make sure it's the final state by waiting for the action to be
|
||||
// reduced.
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
|
||||
// Update the config with user defined settings.
|
||||
const settings = state['features/base/settings'];
|
||||
const config = {};
|
||||
|
||||
if (typeof settings.disableP2P !== 'undefined') {
|
||||
config.p2p = { enabled: !settings.disableP2P };
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: _UPDATE_CONFIG,
|
||||
config
|
||||
});
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
// multiple times, before it makes it to the reducer. At some point it may
|
||||
|
@ -105,7 +119,7 @@ function _setConfig({ getState }, next, action) {
|
|||
// different merge methods being used along the way. The global variable
|
||||
// must be synchronized with the final state resolved by the reducer.
|
||||
if (typeof window.config !== 'undefined') {
|
||||
window.config = getState()['features/base/config'];
|
||||
window.config = state['features/base/config'];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -5,7 +5,7 @@ import _ from 'lodash';
|
|||
import Platform from '../react/Platform';
|
||||
import { equals, ReducerRegistry, set } from '../redux';
|
||||
|
||||
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
|
||||
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
|
||||
import { _cleanupConfig } from './functions';
|
||||
|
||||
/**
|
||||
|
@ -56,49 +56,50 @@ const INITIAL_RN_STATE = {
|
|||
}
|
||||
};
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/base/config',
|
||||
(state = _getInitialState(), action) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_WILL_LOAD:
|
||||
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
|
||||
switch (action.type) {
|
||||
case _UPDATE_CONFIG:
|
||||
return _updateConfig(state, action);
|
||||
|
||||
case CONFIG_WILL_LOAD:
|
||||
return {
|
||||
error: undefined,
|
||||
|
||||
/**
|
||||
* The URL of the location associated with/configured by this
|
||||
* configuration.
|
||||
*
|
||||
* @type URL
|
||||
*/
|
||||
locationURL: action.locationURL
|
||||
};
|
||||
|
||||
case LOAD_CONFIG_ERROR:
|
||||
// XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
|
||||
// the asynchronous "loadConfig procedure/process" started with
|
||||
// CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
|
||||
// is settling the process needs to provide proof that they have
|
||||
// started it and that the iteration of the process being completed
|
||||
// now is still of interest to the app.
|
||||
if (state.locationURL === action.locationURL) {
|
||||
return {
|
||||
error: undefined,
|
||||
|
||||
/**
|
||||
* The URL of the location associated with/configured by this
|
||||
* configuration.
|
||||
*
|
||||
* @type URL
|
||||
*/
|
||||
locationURL: action.locationURL
|
||||
* The {@link Error} which prevented the loading of the
|
||||
* configuration of the associated {@code locationURL}.
|
||||
*
|
||||
* @type Error
|
||||
*/
|
||||
error: action.error
|
||||
};
|
||||
|
||||
case LOAD_CONFIG_ERROR:
|
||||
// XXX LOAD_CONFIG_ERROR is one of the settlement execution paths of
|
||||
// the asynchronous "loadConfig procedure/process" started with
|
||||
// CONFIG_WILL_LOAD. Due to the asynchronous nature of it, whoever
|
||||
// is settling the process needs to provide proof that they have
|
||||
// started it and that the iteration of the process being completed
|
||||
// now is still of interest to the app.
|
||||
if (state.locationURL === action.locationURL) {
|
||||
return {
|
||||
/**
|
||||
* The {@link Error} which prevented the loading of the
|
||||
* configuration of the associated {@code locationURL}.
|
||||
*
|
||||
* @type Error
|
||||
*/
|
||||
error: action.error
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case SET_CONFIG:
|
||||
return _setConfig(state, action);
|
||||
}
|
||||
break;
|
||||
|
||||
return state;
|
||||
});
|
||||
case SET_CONFIG:
|
||||
return _setConfig(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets the initial state of the feature base/config. The mandatory
|
||||
|
@ -120,11 +121,10 @@ function _getInitialState() {
|
|||
* Reduces a specific Redux action SET_CONFIG of the feature
|
||||
* base/lib-jitsi-meet.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/lib-jitsi-meet.
|
||||
* @param {Object} state - The Redux state of the feature base/config.
|
||||
* @param {Action} action - The Redux action SET_CONFIG to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/lib-jitsi-meet after the
|
||||
* reduction of the specified action.
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _setConfig(state, { config }) {
|
||||
// The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
|
||||
|
@ -207,3 +207,19 @@ function _translateLegacyConfig(oldValue: Object) {
|
|||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stored configuration with the given extra options.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/config.
|
||||
* @param {Action} action - The Redux action to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _updateConfig(state, { config }) {
|
||||
const newState = _.merge({}, state, config);
|
||||
|
||||
_cleanupConfig(newState);
|
||||
|
||||
return equals(state, newState) ? state : newState;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
*/
|
||||
export const CALENDAR_ENABLED = 'calendar.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if call integration (CallKit on iOS, ConnectionService on Android)
|
||||
* should be enabled.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if chat should be enabled.
|
||||
* Default: enabled (true).
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
|
||||
/**
|
||||
* Handles changes to the `disableCallIntegration` setting.
|
||||
* On Android (where `AudioMode.setUseConnectionService` is defined) we must update
|
||||
* the native side too, since audio routing works differently.
|
||||
*
|
||||
* @param {boolean} disabled - Whether call integration is disabled or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handleCallIntegrationChange(disabled: boolean) {
|
||||
if (AudioMode.setUseConnectionService) {
|
||||
AudioMode.setUseConnectionService(!disabled);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Handles changes to the `disableCallIntegration` setting.
|
||||
* Noop on web.
|
||||
*
|
||||
* @param {boolean} disabled - Whether call integration is disabled or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handleCallIntegrationChange(disabled: boolean) { // eslint-disable-line no-unused-vars
|
||||
}
|
|
@ -5,6 +5,7 @@ import { getLocalParticipant, participantUpdated } from '../participants';
|
|||
import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
import { handleCallIntegrationChange } from './functions';
|
||||
|
||||
/**
|
||||
* The middleware of the feature base/settings. Distributes changes to the state
|
||||
|
@ -19,6 +20,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
switch (action.type) {
|
||||
case SETTINGS_UPDATED:
|
||||
_maybeHandleCallIntegrationChange(action);
|
||||
_maybeSetAudioOnly(store, action);
|
||||
_updateLocalParticipant(store, action);
|
||||
}
|
||||
|
@ -43,6 +45,19 @@ function _mapSettingsFieldToParticipant(settingsField) {
|
|||
return settingsField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@code startAudioOnly} flag if it's updated in the settings.
|
||||
*
|
||||
* @param {Object} action - The redux action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeHandleCallIntegrationChange({ settings: { disableCallIntegration } }) {
|
||||
if (typeof disableCallIntegration === 'boolean') {
|
||||
handleCallIntegrationChange(disableCallIntegration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@code startAudioOnly} flag if it's updated in the settings.
|
||||
*
|
||||
|
|
|
@ -22,6 +22,8 @@ const DEFAULT_STATE = {
|
|||
avatarID: undefined,
|
||||
avatarURL: undefined,
|
||||
cameraDeviceId: undefined,
|
||||
disableCallIntegration: undefined,
|
||||
disableP2P: undefined,
|
||||
displayName: undefined,
|
||||
email: undefined,
|
||||
localFlipX: true,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import { CALL_INTEGRATION_ENABLED, getFeatureFlag } from '../../base/flags';
|
||||
import { toState } from '../../base/redux';
|
||||
|
||||
/**
|
||||
* Checks if call integration is enabled or not.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {string} - Default URL for the app.
|
||||
*/
|
||||
export function isCallIntegrationEnabled(stateful: Function | Object) {
|
||||
const state = toState(stateful);
|
||||
const { disableCallIntegration } = state['features/base/settings'];
|
||||
const flag = getFeatureFlag(state, CALL_INTEGRATION_ENABLED);
|
||||
|
||||
// The feature flag has precedence.
|
||||
return flag ?? !disableCallIntegration;
|
||||
}
|
|
@ -34,6 +34,7 @@ import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
|
|||
|
||||
import CallKit from './CallKit';
|
||||
import ConnectionService from './ConnectionService';
|
||||
import { isCallIntegrationEnabled } from './functions';
|
||||
|
||||
const CallIntegration = CallKit || ConnectionService;
|
||||
|
||||
|
@ -139,9 +140,13 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
|||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceFailed(store, next, action) {
|
||||
function _conferenceFailed({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
if (!isCallIntegrationEnabled(getState)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may be
|
||||
// able to eventually join the conference.
|
||||
|
@ -173,6 +178,10 @@ function _conferenceFailed(store, next, action) {
|
|||
function _conferenceJoined({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
if (!isCallIntegrationEnabled(getState)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
|
@ -201,9 +210,13 @@ function _conferenceJoined({ getState }, next, action) {
|
|||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceLeft(store, next, action) {
|
||||
function _conferenceLeft({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
if (!isCallIntegrationEnabled(getState)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
|
@ -230,6 +243,10 @@ function _conferenceLeft(store, next, action) {
|
|||
function _conferenceWillJoin({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
if (!isCallIntegrationEnabled(getState)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { conference } = action;
|
||||
const state = getState();
|
||||
const { callHandle, callUUID } = state['features/base/config'];
|
||||
|
@ -341,6 +358,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
|
|||
function _setAudioOnly({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
|
||||
if (!isCallIntegrationEnabled(state)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
|
@ -393,6 +415,11 @@ function _setCallKitSubscriptions({ getState }, next, action) {
|
|||
*/
|
||||
function _syncTrackState({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
if (!isCallIntegrationEnabled(getState)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { jitsiTrack } = action.track;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
|
|
@ -48,7 +48,7 @@ export type Props = {
|
|||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class AbstractSettingsView<P: Props> extends Component<P> {
|
||||
export class AbstractSettingsView<P: Props, S: *> extends Component<P, S> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractSettingsView} instance.
|
||||
|
|
|
@ -32,12 +32,20 @@ type Props = AbstractProps & {
|
|||
_headerStyles: Object
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether to show advanced settings or not.
|
||||
*/
|
||||
showAdvanced: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The native container rendering the app settings page.
|
||||
*
|
||||
* @extends AbstractSettingsView
|
||||
*/
|
||||
class SettingsView extends AbstractSettingsView<Props> {
|
||||
class SettingsView extends AbstractSettingsView<Props, State> {
|
||||
_urlField: Object;
|
||||
|
||||
/**
|
||||
|
@ -48,9 +56,16 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showAdvanced: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onBlurServerURL = this._onBlurServerURL.bind(this);
|
||||
this._onDisableCallIntegration = this._onDisableCallIntegration.bind(this);
|
||||
this._onDisableP2P = this._onDisableP2P.bind(this);
|
||||
this._onRequestClose = this._onRequestClose.bind(this);
|
||||
this._onShowAdvanced = this._onShowAdvanced.bind(this);
|
||||
this._setURLFieldReference = this._setURLFieldReference.bind(this);
|
||||
this._showURLAlert = this._showURLAlert.bind(this);
|
||||
}
|
||||
|
@ -94,6 +109,38 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
|
||||
_onChangeServerURL: (string) => void;
|
||||
|
||||
_onDisableCallIntegration: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Handles the disable call integration change event.
|
||||
*
|
||||
* @param {boolean} newValue - The new value
|
||||
* option.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisableCallIntegration(newValue) {
|
||||
this._updateSettings({
|
||||
disableCallIntegration: newValue
|
||||
});
|
||||
}
|
||||
|
||||
_onDisableP2P: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Handles the disable P2P change event.
|
||||
*
|
||||
* @param {boolean} newValue - The new value
|
||||
* option.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisableP2P(newValue) {
|
||||
this._updateSettings({
|
||||
disableP2P: newValue
|
||||
});
|
||||
}
|
||||
|
||||
_onRequestClose: () => void;
|
||||
|
||||
/**
|
||||
|
@ -103,9 +150,21 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onRequestClose() {
|
||||
this.setState({ showAdvanced: false });
|
||||
this._processServerURL(true /* hideOnSuccess */);
|
||||
}
|
||||
|
||||
_onShowAdvanced: () => void;
|
||||
|
||||
/**
|
||||
* Handles the advanced settings button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShowAdvanced() {
|
||||
this.setState({ showAdvanced: !this.state.showAdvanced });
|
||||
}
|
||||
|
||||
_onStartAudioMutedChange: (boolean) => void;
|
||||
|
||||
_onStartVideoMutedChange: (boolean) => void;
|
||||
|
@ -133,6 +192,48 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the advanced settings options.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderAdvancedSettings() {
|
||||
const { _settings } = this.props;
|
||||
const { showAdvanced } = this.state;
|
||||
|
||||
if (!showAdvanced) {
|
||||
return (
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
label = 'settingsView.showAdvanced'>
|
||||
<Switch
|
||||
onValueChange = { this._onShowAdvanced }
|
||||
value = { showAdvanced } />
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
label = 'settingsView.disableCallIntegration'>
|
||||
<Switch
|
||||
onValueChange = { this._onDisableCallIntegration }
|
||||
value = { _settings.disableCallIntegration } />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
label = 'settingsView.disableP2P'>
|
||||
<Switch
|
||||
onValueChange = { this._onDisableP2P }
|
||||
value = { _settings.disableP2P } />
|
||||
</FormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the body (under the header) of {@code SettingsView}.
|
||||
*
|
||||
|
@ -193,12 +294,14 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
<FormSectionHeader
|
||||
label = 'settingsView.buildInfoSection' />
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
label = 'settingsView.version'>
|
||||
<Text>
|
||||
{ `${AppInfo.version} build ${AppInfo.buildNumber}` }
|
||||
</Text>
|
||||
</FormRow>
|
||||
<FormSectionHeader
|
||||
label = 'settingsView.advanced' />
|
||||
{ this._renderAdvancedSettings() }
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
@ -252,6 +355,8 @@ class SettingsView extends AbstractSettingsView<Props> {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
_updateSettings: (Object) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue