diff --git a/lang/main.json b/lang/main.json index 4f5c98b0a..7d2bcfdc6 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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" diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index d12f31fac..299e16f02 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -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 })); + } }); } diff --git a/react/features/base/config/actionTypes.js b/react/features/base/config/actionTypes.js index 6e00c11d7..d768f55fe 100644 --- a/react/features/base/config/actionTypes.js +++ b/react/features/base/config/actionTypes.js @@ -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'; diff --git a/react/features/base/config/middleware.js b/react/features/base/config/middleware.js index 610016e77..4bfc222a5 100644 --- a/react/features/base/config/middleware.js +++ b/react/features/base/config/middleware.js @@ -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; diff --git a/react/features/base/config/reducer.js b/react/features/base/config/reducer.js index 6d28c0e32..bbca86adc 100644 --- a/react/features/base/config/reducer.js +++ b/react/features/base/config/reducer.js @@ -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; +} diff --git a/react/features/base/flags/constants.js b/react/features/base/flags/constants.js index a5dedf682..41187a07b 100644 --- a/react/features/base/flags/constants.js +++ b/react/features/base/flags/constants.js @@ -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). diff --git a/react/features/base/settings/functions.js b/react/features/base/settings/functions.any.js similarity index 100% rename from react/features/base/settings/functions.js rename to react/features/base/settings/functions.any.js diff --git a/react/features/base/settings/functions.native.js b/react/features/base/settings/functions.native.js new file mode 100644 index 000000000..5337417b9 --- /dev/null +++ b/react/features/base/settings/functions.native.js @@ -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); + } +} diff --git a/react/features/base/settings/functions.web.js b/react/features/base/settings/functions.web.js new file mode 100644 index 000000000..62cfab05e --- /dev/null +++ b/react/features/base/settings/functions.web.js @@ -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 +} diff --git a/react/features/base/settings/middleware.js b/react/features/base/settings/middleware.js index e178cac74..eef08431a 100644 --- a/react/features/base/settings/middleware.js +++ b/react/features/base/settings/middleware.js @@ -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. * diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index 9220cafe6..3d96831e4 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -22,6 +22,8 @@ const DEFAULT_STATE = { avatarID: undefined, avatarURL: undefined, cameraDeviceId: undefined, + disableCallIntegration: undefined, + disableP2P: undefined, displayName: undefined, email: undefined, localFlipX: true, diff --git a/react/features/mobile/call-integration/functions.js b/react/features/mobile/call-integration/functions.js new file mode 100644 index 000000000..6602c031f --- /dev/null +++ b/react/features/mobile/call-integration/functions.js @@ -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; +} diff --git a/react/features/mobile/call-integration/middleware.js b/react/features/mobile/call-integration/middleware.js index 54bbe7a2a..101fde9fb 100644 --- a/react/features/mobile/call-integration/middleware.js +++ b/react/features/mobile/call-integration/middleware.js @@ -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); diff --git a/react/features/settings/components/AbstractSettingsView.js b/react/features/settings/components/AbstractSettingsView.js index 2c81c717c..35173f921 100644 --- a/react/features/settings/components/AbstractSettingsView.js +++ b/react/features/settings/components/AbstractSettingsView.js @@ -48,7 +48,7 @@ export type Props = { * * @abstract */ -export class AbstractSettingsView extends Component

{ +export class AbstractSettingsView extends Component { /** * Initializes a new {@code AbstractSettingsView} instance. diff --git a/react/features/settings/components/native/SettingsView.js b/react/features/settings/components/native/SettingsView.js index 7486533af..6f8473dbe 100644 --- a/react/features/settings/components/native/SettingsView.js +++ b/react/features/settings/components/native/SettingsView.js @@ -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 { +class SettingsView extends AbstractSettingsView { _urlField: Object; /** @@ -48,9 +56,16 @@ class SettingsView extends AbstractSettingsView { 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 { _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 { * @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 { } } + /** + * Renders the advanced settings options. + * + * @private + * @returns {React$Element} + */ + _renderAdvancedSettings() { + const { _settings } = this.props; + const { showAdvanced } = this.state; + + if (!showAdvanced) { + return ( + + + + ); + } + + return ( + <> + + + + + + + + ); + } + /** * Renders the body (under the header) of {@code SettingsView}. * @@ -193,12 +294,14 @@ class SettingsView extends AbstractSettingsView { { `${AppInfo.version} build ${AppInfo.buildNumber}` } + + { this._renderAdvancedSettings() } ); @@ -252,6 +355,8 @@ class SettingsView extends AbstractSettingsView { ] ); } + + _updateSettings: (Object) => void; } /**