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:
Saúl Ibarra Corretgé 2019-10-18 16:30:59 +02:00 committed by Saúl Ibarra Corretgé
parent fe90e5aa8f
commit 6d16e087d9
15 changed files with 316 additions and 51 deletions

View File

@ -526,16 +526,20 @@
"title": "Settings" "title": "Settings"
}, },
"settingsView": { "settingsView": {
"advanced": "Advanced",
"alertOk": "OK", "alertOk": "OK",
"alertTitle": "Warning", "alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid", "alertURLText": "The entered server URL is invalid",
"buildInfoSection": "Build Information", "buildInfoSection": "Build Information",
"conferenceSection": "Conference", "conferenceSection": "Conference",
"disableCallIntegration": "Disable native call integration",
"disableP2P": "Disable Peer-To-Peer mode",
"displayName": "Display name", "displayName": "Display name",
"email": "Email", "email": "Email",
"header": "Settings", "header": "Settings",
"profileSection": "Profile", "profileSection": "Profile",
"serverURL": "Server URL", "serverURL": "Server URL",
"showAdvanced": "Show advanced settings",
"startWithAudioMuted": "Start with audio muted", "startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted", "startWithVideoMuted": "Start with video muted",
"version": "Version" "version": "Version"

View File

@ -6,7 +6,7 @@ import '../../analytics';
import '../../authentication'; import '../../authentication';
import { setColorScheme } from '../../base/color-scheme'; import { setColorScheme } from '../../base/color-scheme';
import { DialogContainer } from '../../base/dialog'; import { DialogContainer } from '../../base/dialog';
import { updateFlags } from '../../base/flags'; import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
import '../../base/jwt'; import '../../base/jwt';
import { Platform } from '../../base/react'; import { Platform } from '../../base/react';
import { import {
@ -100,6 +100,13 @@ export class App extends AbstractApp {
dispatch(setColorScheme(this.props.colorScheme)); dispatch(setColorScheme(this.props.colorScheme));
dispatch(updateFlags(this.props.flags)); dispatch(updateFlags(this.props.flags));
dispatch(updateSettings(this.props.userInfo || {})); 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 }));
}
}); });
} }

View File

@ -34,3 +34,17 @@ export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
* } * }
*/ */
export const SET_CONFIG = 'SET_CONFIG'; 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';

View File

@ -5,7 +5,7 @@ import { addKnownDomains } from '../known-domains';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { parseURIString } from '../util'; import { parseURIString } from '../util';
import { SET_CONFIG } from './actionTypes'; import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
import { _CONFIG_STORE_PREFIX } from './constants'; import { _CONFIG_STORE_PREFIX } from './constants';
/** /**
@ -93,11 +93,25 @@ function _appWillMount(store, next, action) {
* @private * @private
* @returns {*} The return value of {@code next(action)}. * @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, // 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 // so make sure it's the final state by waiting for the action to be
// reduced. // reduced.
const result = next(action); 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 // 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 // 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 // different merge methods being used along the way. The global variable
// must be synchronized with the final state resolved by the reducer. // must be synchronized with the final state resolved by the reducer.
if (typeof window.config !== 'undefined') { if (typeof window.config !== 'undefined') {
window.config = getState()['features/base/config']; window.config = state['features/base/config'];
} }
return result; return result;

View File

@ -5,7 +5,7 @@ import _ from 'lodash';
import Platform from '../react/Platform'; import Platform from '../react/Platform';
import { equals, ReducerRegistry, set } from '../redux'; 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'; import { _cleanupConfig } from './functions';
/** /**
@ -56,49 +56,50 @@ const INITIAL_RN_STATE = {
} }
}; };
ReducerRegistry.register( ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
'features/base/config', switch (action.type) {
(state = _getInitialState(), action) => { case _UPDATE_CONFIG:
switch (action.type) { return _updateConfig(state, action);
case CONFIG_WILL_LOAD:
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 { return {
error: undefined,
/** /**
* The URL of the location associated with/configured by this * The {@link Error} which prevented the loading of the
* configuration. * configuration of the associated {@code locationURL}.
* *
* @type URL * @type Error
*/ */
locationURL: action.locationURL 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 * 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 * Reduces a specific Redux action SET_CONFIG of the feature
* base/lib-jitsi-meet. * 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. * @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private * @private
* @returns {Object} The new state of the feature base/lib-jitsi-meet after the * @returns {Object} The new state after the reduction of the specified action.
* reduction of the specified action.
*/ */
function _setConfig(state, { config }) { function _setConfig(state, { config }) {
// The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and // The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
@ -207,3 +207,19 @@ function _translateLegacyConfig(oldValue: Object) {
return newValue; 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;
}

View File

@ -6,6 +6,13 @@
*/ */
export const CALENDAR_ENABLED = 'calendar.enabled'; 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. * Flag indicating if chat should be enabled.
* Default: enabled (true). * Default: enabled (true).

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -5,6 +5,7 @@ import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry } from '../redux'; import { MiddlewareRegistry } from '../redux';
import { SETTINGS_UPDATED } from './actionTypes'; import { SETTINGS_UPDATED } from './actionTypes';
import { handleCallIntegrationChange } from './functions';
/** /**
* The middleware of the feature base/settings. Distributes changes to the state * The middleware of the feature base/settings. Distributes changes to the state
@ -19,6 +20,7 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case SETTINGS_UPDATED: case SETTINGS_UPDATED:
_maybeHandleCallIntegrationChange(action);
_maybeSetAudioOnly(store, action); _maybeSetAudioOnly(store, action);
_updateLocalParticipant(store, action); _updateLocalParticipant(store, action);
} }
@ -43,6 +45,19 @@ function _mapSettingsFieldToParticipant(settingsField) {
return 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. * Updates {@code startAudioOnly} flag if it's updated in the settings.
* *

View File

@ -22,6 +22,8 @@ const DEFAULT_STATE = {
avatarID: undefined, avatarID: undefined,
avatarURL: undefined, avatarURL: undefined,
cameraDeviceId: undefined, cameraDeviceId: undefined,
disableCallIntegration: undefined,
disableP2P: undefined,
displayName: undefined, displayName: undefined,
email: undefined, email: undefined,
localFlipX: true, localFlipX: true,

View File

@ -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;
}

View File

@ -34,6 +34,7 @@ import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit'; import CallKit from './CallKit';
import ConnectionService from './ConnectionService'; import ConnectionService from './ConnectionService';
import { isCallIntegrationEnabled } from './functions';
const CallIntegration = CallKit || ConnectionService; const CallIntegration = CallKit || ConnectionService;
@ -139,9 +140,13 @@ function _appWillMount({ dispatch, getState }, next, action) {
* @private * @private
* @returns {*} The value returned by {@code next(action)}. * @returns {*} The value returned by {@code next(action)}.
*/ */
function _conferenceFailed(store, next, action) { function _conferenceFailed({ getState }, next, action) {
const result = next(action); const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may be // prevented the user from joining a specific conference but the app may be
// able to eventually join the conference. // able to eventually join the conference.
@ -173,6 +178,10 @@ function _conferenceFailed(store, next, action) {
function _conferenceJoined({ getState }, next, action) { function _conferenceJoined({ getState }, next, action) {
const result = next(action); const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { callUUID } = action.conference; const { callUUID } = action.conference;
if (callUUID) { if (callUUID) {
@ -201,9 +210,13 @@ function _conferenceJoined({ getState }, next, action) {
* @private * @private
* @returns {*} The value returned by {@code next(action)}. * @returns {*} The value returned by {@code next(action)}.
*/ */
function _conferenceLeft(store, next, action) { function _conferenceLeft({ getState }, next, action) {
const result = next(action); const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { callUUID } = action.conference; const { callUUID } = action.conference;
if (callUUID) { if (callUUID) {
@ -230,6 +243,10 @@ function _conferenceLeft(store, next, action) {
function _conferenceWillJoin({ dispatch, getState }, next, action) { function _conferenceWillJoin({ dispatch, getState }, next, action) {
const result = next(action); const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { conference } = action; const { conference } = action;
const state = getState(); const state = getState();
const { callHandle, callUUID } = state['features/base/config']; const { callHandle, callUUID } = state['features/base/config'];
@ -341,6 +358,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
function _setAudioOnly({ getState }, next, action) { function _setAudioOnly({ getState }, next, action) {
const result = next(action); const result = next(action);
const state = getState(); const state = getState();
if (!isCallIntegrationEnabled(state)) {
return result;
}
const conference = getCurrentConference(state); const conference = getCurrentConference(state);
if (conference && conference.callUUID) { if (conference && conference.callUUID) {
@ -393,6 +415,11 @@ function _setCallKitSubscriptions({ getState }, next, action) {
*/ */
function _syncTrackState({ getState }, next, action) { function _syncTrackState({ getState }, next, action) {
const result = next(action); const result = next(action);
if (!isCallIntegrationEnabled(getState)) {
return result;
}
const { jitsiTrack } = action.track; const { jitsiTrack } = action.track;
const state = getState(); const state = getState();
const conference = getCurrentConference(state); const conference = getCurrentConference(state);

View File

@ -48,7 +48,7 @@ export type Props = {
* *
* @abstract * @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. * Initializes a new {@code AbstractSettingsView} instance.

View File

@ -32,12 +32,20 @@ type Props = AbstractProps & {
_headerStyles: Object _headerStyles: Object
} }
type State = {
/**
* Whether to show advanced settings or not.
*/
showAdvanced: boolean
}
/** /**
* The native container rendering the app settings page. * The native container rendering the app settings page.
* *
* @extends AbstractSettingsView * @extends AbstractSettingsView
*/ */
class SettingsView extends AbstractSettingsView<Props> { class SettingsView extends AbstractSettingsView<Props, State> {
_urlField: Object; _urlField: Object;
/** /**
@ -48,9 +56,16 @@ class SettingsView extends AbstractSettingsView<Props> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
showAdvanced: false
};
// Bind event handlers so they are only bound once per instance. // Bind event handlers so they are only bound once per instance.
this._onBlurServerURL = this._onBlurServerURL.bind(this); 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._onRequestClose = this._onRequestClose.bind(this);
this._onShowAdvanced = this._onShowAdvanced.bind(this);
this._setURLFieldReference = this._setURLFieldReference.bind(this); this._setURLFieldReference = this._setURLFieldReference.bind(this);
this._showURLAlert = this._showURLAlert.bind(this); this._showURLAlert = this._showURLAlert.bind(this);
} }
@ -94,6 +109,38 @@ class SettingsView extends AbstractSettingsView<Props> {
_onChangeServerURL: (string) => void; _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; _onRequestClose: () => void;
/** /**
@ -103,9 +150,21 @@ class SettingsView extends AbstractSettingsView<Props> {
* @returns {void} * @returns {void}
*/ */
_onRequestClose() { _onRequestClose() {
this.setState({ showAdvanced: false });
this._processServerURL(true /* hideOnSuccess */); this._processServerURL(true /* hideOnSuccess */);
} }
_onShowAdvanced: () => void;
/**
* Handles the advanced settings button.
*
* @returns {void}
*/
_onShowAdvanced() {
this.setState({ showAdvanced: !this.state.showAdvanced });
}
_onStartAudioMutedChange: (boolean) => void; _onStartAudioMutedChange: (boolean) => void;
_onStartVideoMutedChange: (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}. * Renders the body (under the header) of {@code SettingsView}.
* *
@ -193,12 +294,14 @@ class SettingsView extends AbstractSettingsView<Props> {
<FormSectionHeader <FormSectionHeader
label = 'settingsView.buildInfoSection' /> label = 'settingsView.buildInfoSection' />
<FormRow <FormRow
fieldSeparator = { true }
label = 'settingsView.version'> label = 'settingsView.version'>
<Text> <Text>
{ `${AppInfo.version} build ${AppInfo.buildNumber}` } { `${AppInfo.version} build ${AppInfo.buildNumber}` }
</Text> </Text>
</FormRow> </FormRow>
<FormSectionHeader
label = 'settingsView.advanced' />
{ this._renderAdvancedSettings() }
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
); );
@ -252,6 +355,8 @@ class SettingsView extends AbstractSettingsView<Props> {
] ]
); );
} }
_updateSettings: (Object) => void;
} }
/** /**