This commit is contained in:
23
react/features/base/settings/actionTypes.ts
Normal file
23
react/features/base/settings/actionTypes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Create an action for when the settings are updated.
|
||||
*
|
||||
* {
|
||||
* type: SETTINGS_UPDATED,
|
||||
* settings: {
|
||||
* audioOutputDeviceId: string,
|
||||
* avatarURL: string,
|
||||
* cameraDeviceId: string,
|
||||
* displayName: string,
|
||||
* email: string,
|
||||
* localFlipX: boolean,
|
||||
* micDeviceId: string,
|
||||
* serverURL: string,
|
||||
* showSubtitlesOnStage: boolean,
|
||||
* startAudioOnly: boolean,
|
||||
* startWithAudioMuted: boolean,
|
||||
* startWithVideoMuted: boolean,
|
||||
* startWithReactionsMuted: boolean
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||
32
react/features/base/settings/actions.ts
Normal file
32
react/features/base/settings/actions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
import { ISettingsState } from './reducer';
|
||||
|
||||
/**
|
||||
* Create an action for when the settings are updated.
|
||||
*
|
||||
* @param {Object} settings - The new (partial) settings properties.
|
||||
* @returns {{
|
||||
* type: SETTINGS_UPDATED,
|
||||
* settings: {
|
||||
* audioOutputDeviceId: string,
|
||||
* avatarURL: string,
|
||||
* cameraDeviceId: string,
|
||||
* displayName: string,
|
||||
* email: string,
|
||||
* localFlipX: boolean,
|
||||
* micDeviceId: string,
|
||||
* serverURL: string,
|
||||
* soundsReactions: boolean,
|
||||
* startAudioOnly: boolean,
|
||||
* startWithAudioMuted: boolean,
|
||||
* startWithVideoMuted: boolean,
|
||||
* startWithReactionsMuted: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function updateSettings(settings: Partial<ISettingsState>) {
|
||||
return {
|
||||
type: SETTINGS_UPDATED,
|
||||
settings
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { IconGear } from '../../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../../base/toolbox/components/AbstractButton';
|
||||
import { navigate }
|
||||
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../../mobile/navigation/routes';
|
||||
import { SETTINGS_ENABLED } from '../../../flags/constants';
|
||||
import { getFeatureFlag } from '../../../flags/functions';
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractButton} to open the carmode.
|
||||
*/
|
||||
class SettingsButton extends AbstractButton<AbstractButtonProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.Settings';
|
||||
override icon = IconGear;
|
||||
override label = 'settings.buttonLabel';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the carmode mode.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
return navigate(screen.settings.main);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the component's props.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const enabled = getFeatureFlag(state, SETTINGS_ENABLED, true);
|
||||
|
||||
return {
|
||||
visible: enabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(SettingsButton));
|
||||
4
react/features/base/settings/constants.ts
Normal file
4
react/features/base/settings/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* The default server URL to open if no other was specified.
|
||||
*/
|
||||
export const DEFAULT_SERVER_URL = 'https://meet.jit.si';
|
||||
126
react/features/base/settings/functions.any.ts
Normal file
126
react/features/base/settings/functions.any.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import CONFIG_WHITELIST from '../config/configWhitelist';
|
||||
import { IConfigState } from '../config/reducer';
|
||||
import { IJwtState } from '../jwt/reducer';
|
||||
import { toState } from '../redux/functions';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { DEFAULT_SERVER_URL } from './constants';
|
||||
import { ISettingsState } from './reducer';
|
||||
|
||||
/**
|
||||
* Returns the effective value of a configuration/preference/setting by applying
|
||||
* a precedence among the values specified by JWT, URL, settings,
|
||||
* and config.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or {@code getState} function.
|
||||
* @param {string} propertyName - The name of the
|
||||
* configuration/preference/setting (property) to retrieve.
|
||||
* @param {Object} sources - Flags indicating the configuration/preference/setting sources to
|
||||
* consider/retrieve values from.
|
||||
* @param {boolean} sources.config - Config.
|
||||
* @param {boolean} jwt - JWT.
|
||||
* @param {boolean} settings - Settings.
|
||||
* @param {boolean} urlParams - URL parameters.
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getPropertyValue(
|
||||
stateful: IStateful,
|
||||
propertyName: string,
|
||||
sources?: any
|
||||
) {
|
||||
// Default values don't play nicely with partial objects and we want to make
|
||||
// the function easy to use without exhaustively defining all flags:
|
||||
sources = { // eslint-disable-line no-param-reassign
|
||||
// Defaults:
|
||||
config: true,
|
||||
jwt: true,
|
||||
settings: true,
|
||||
urlParams: true,
|
||||
|
||||
...sources
|
||||
};
|
||||
|
||||
// Precedence: jwt -> urlParams -> settings -> config.
|
||||
|
||||
const state = toState(stateful);
|
||||
|
||||
// jwt
|
||||
if (sources.jwt) {
|
||||
const value = state['features/base/jwt'][propertyName as keyof IJwtState];
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
return value[propertyName as keyof typeof value];
|
||||
}
|
||||
}
|
||||
|
||||
// urlParams
|
||||
if (sources.urlParams) {
|
||||
if (CONFIG_WHITELIST.indexOf(propertyName) !== -1) {
|
||||
const urlParams
|
||||
= parseURLParams(state['features/base/connection'].locationURL ?? '');
|
||||
const value = urlParams[`config.${propertyName}`];
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// settings
|
||||
if (sources.settings) {
|
||||
const value = state['features/base/settings'][propertyName as keyof ISettingsState];
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// config
|
||||
if (sources.config) {
|
||||
const value = state['features/base/config'][propertyName as keyof IConfigState];
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently configured server URL.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string} - The currently configured server URL.
|
||||
*/
|
||||
export function getServerURL(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we hide the helper dialog when a user tries to do audio only screen sharing.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldHideShareAudioHelper(state: IReduxState): boolean | undefined {
|
||||
|
||||
return state['features/base/settings'].hideShareAudioHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the disabled self view setting.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getHideSelfView(state: IReduxState) {
|
||||
return state['features/base/config'].disableSelfView || state['features/base/settings'].disableSelfView
|
||||
|| iAmVisitor(state);
|
||||
}
|
||||
34
react/features/base/settings/functions.native.ts
Normal file
34
react/features/base/settings/functions.native.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes to the `disableCrashReporting` setting.
|
||||
* Stores the value into platform specific default preference file, so at app
|
||||
* start-up time it is retrieved on the native side and the crash reporting
|
||||
* is enabled/disabled.
|
||||
*
|
||||
* @param {boolean} disabled - Whether crash reporting is disabled or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handleCrashReportingChange(disabled: boolean) {
|
||||
DefaultPreference.setName('jitsi-default-preferences').then( // @ts-ignore
|
||||
DefaultPreference.set('isCrashReportingDisabled', disabled.toString()));
|
||||
}
|
||||
216
react/features/base/settings/functions.web.ts
Normal file
216
react/features/base/settings/functions.web.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used camera.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentCameraDeviceId(state: IReduxState) {
|
||||
return getDeviceIdByType(state, 'isVideoTrack');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used microphone.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentMicDeviceId(state: IReduxState) {
|
||||
return getDeviceIdByType(state, 'isAudioTrack');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used speaker.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentOutputDeviceId(state: IReduxState) {
|
||||
return state['features/base/settings'].audioOutputDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the corresponding local track type.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @param {string} isType - Can be 'isVideoTrack' | 'isAudioTrack'.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDeviceIdByType(state: IReduxState, isType: string) {
|
||||
const [ deviceId ] = state['features/base/tracks']
|
||||
.map(t => t.jitsiTrack)
|
||||
.filter(t => t?.isLocal() && t[isType as keyof typeof t]())
|
||||
.map(t => t.getDeviceId());
|
||||
|
||||
return deviceId || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the saved display name.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDisplayName(state: IReduxState): string {
|
||||
return state['features/base/settings'].displayName || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred cameraDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedCameraDeviceId(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedCameraDeviceId,
|
||||
userSelectedCameraDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { videoInput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: videoInput,
|
||||
|
||||
// Operating systems may append " #{number}" somewhere in the label so
|
||||
// find and strip that bit.
|
||||
matchRegex: /\s#\d*(?!.*\s#\d*)/,
|
||||
userSelectedDeviceId: userSelectedCameraDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
|
||||
replacement: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred micDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedMicDeviceId(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedMicDeviceId,
|
||||
userSelectedMicDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { audioInput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: audioInput,
|
||||
|
||||
// Operating systems may append " ({number}-" somewhere in the label so
|
||||
// find and strip that bit.
|
||||
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
|
||||
userSelectedDeviceId: userSelectedMicDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
|
||||
replacement: ' ('
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches known devices for a matching deviceId and fall back to matching on
|
||||
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux state object or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUserSelectedOutputDeviceId(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
userSelectedAudioOutputDeviceId,
|
||||
userSelectedAudioOutputDeviceLabel
|
||||
} = state['features/base/settings'];
|
||||
const { audioOutput } = state['features/base/devices'].availableDevices;
|
||||
|
||||
return _getUserSelectedDeviceId({
|
||||
availableDevices: audioOutput,
|
||||
matchRegex: undefined,
|
||||
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
|
||||
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
|
||||
replacement: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to abstract the logic for choosing which device ID to
|
||||
* use. Falls back to fuzzy matching on label if a device ID match is not found.
|
||||
*
|
||||
* @param {Object} options - The arguments used to match find the preferred
|
||||
* device ID from available devices.
|
||||
* @param {Array<string>} options.availableDevices - The array of currently
|
||||
* available devices to match against.
|
||||
* @param {Object} options.matchRegex - The regex to use to find strings
|
||||
* appended to the label by the operating system. The matches will be replaced
|
||||
* with options.replacement, with the intent of matching the same device that
|
||||
* might have a modified label.
|
||||
* @param {string} options.userSelectedDeviceId - The device ID the participant
|
||||
* prefers to use.
|
||||
* @param {string} options.userSelectedDeviceLabel - The label associated with the
|
||||
* device ID the participant prefers to use.
|
||||
* @param {string} options.replacement - The string to use with
|
||||
* options.matchRegex to remove identifies added to the label by the operating
|
||||
* system.
|
||||
* @private
|
||||
* @returns {string} The preferred device ID to use for media.
|
||||
*/
|
||||
function _getUserSelectedDeviceId(options: {
|
||||
availableDevices: MediaDeviceInfo[] | undefined;
|
||||
matchRegex?: RegExp;
|
||||
replacement?: string;
|
||||
userSelectedDeviceId?: string;
|
||||
userSelectedDeviceLabel?: string;
|
||||
}) {
|
||||
const {
|
||||
availableDevices,
|
||||
matchRegex = '',
|
||||
userSelectedDeviceId,
|
||||
userSelectedDeviceLabel,
|
||||
replacement = ''
|
||||
} = options;
|
||||
|
||||
if (userSelectedDeviceId) {
|
||||
const foundMatchingBasedonDeviceId = availableDevices?.find(
|
||||
candidate => candidate.deviceId === userSelectedDeviceId);
|
||||
|
||||
// Prioritize matching the deviceId
|
||||
if (foundMatchingBasedonDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no label at all, there is no need to fall back to checking
|
||||
// the label for a fuzzy match.
|
||||
if (!userSelectedDeviceLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strippedDeviceLabel
|
||||
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
|
||||
: userSelectedDeviceLabel;
|
||||
const foundMatchBasedOnLabel = availableDevices?.find(candidate => {
|
||||
const { label } = candidate;
|
||||
|
||||
if (!label) {
|
||||
return false;
|
||||
} else if (strippedDeviceLabel === label) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const strippedCandidateLabel
|
||||
= label.replace(matchRegex, replacement);
|
||||
|
||||
return strippedDeviceLabel === strippedCandidateLabel;
|
||||
});
|
||||
|
||||
return foundMatchBasedOnLabel?.deviceId;
|
||||
}
|
||||
3
react/features/base/settings/logger.ts
Normal file
3
react/features/base/settings/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/settings');
|
||||
117
react/features/base/settings/middleware.any.ts
Normal file
117
react/features/base/settings/middleware.any.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { escape } from 'lodash-es';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_LOCATION_URL } from '../connection/actionTypes';
|
||||
import { participantUpdated } from '../participants/actions';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
import { updateSettings } from './actions';
|
||||
|
||||
/**
|
||||
* The middleware of the feature base/settings. Distributes changes to the state
|
||||
* of base/settings to the states of other features computed from the state of
|
||||
* base/settings.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case SETTINGS_UPDATED:
|
||||
_updateLocalParticipant(store, action);
|
||||
break;
|
||||
case SET_LOCATION_URL:
|
||||
_updateLocalParticipantFromUrl(store);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Maps the settings field names to participant names where they don't match.
|
||||
* Currently there is only one such field, but may be extended in the future.
|
||||
*
|
||||
* @private
|
||||
* @param {string} settingsField - The name of the settings field to map.
|
||||
* @returns {string}
|
||||
*/
|
||||
function _mapSettingsFieldToParticipant(settingsField: string) {
|
||||
switch (settingsField) {
|
||||
case 'displayName':
|
||||
return 'name';
|
||||
}
|
||||
|
||||
return settingsField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local participant according to settings changes.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} action - The dispatched action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateLocalParticipant({ dispatch, getState }: IStore, action: AnyAction) {
|
||||
const { settings } = action;
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
const newLocalParticipant = {
|
||||
...localParticipant
|
||||
};
|
||||
|
||||
for (const key in settings) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
newLocalParticipant[_mapSettingsFieldToParticipant(key) as keyof typeof newLocalParticipant]
|
||||
= settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(participantUpdated({
|
||||
...newLocalParticipant,
|
||||
id: newLocalParticipant.id ?? ''
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the userInfo set in the URL.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateLocalParticipantFromUrl({ dispatch, getState }: IStore) {
|
||||
const urlParams
|
||||
= parseURLParams(getState()['features/base/connection'].locationURL ?? '');
|
||||
const urlEmail = urlParams['userInfo.email'];
|
||||
const urlDisplayName = urlParams['userInfo.displayName'];
|
||||
|
||||
if (!urlEmail && !urlDisplayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
|
||||
if (localParticipant) {
|
||||
const displayName = escape(urlDisplayName);
|
||||
const email = escape(urlEmail);
|
||||
|
||||
dispatch(participantUpdated({
|
||||
...localParticipant,
|
||||
email,
|
||||
name: displayName
|
||||
}));
|
||||
|
||||
dispatch(updateSettings({
|
||||
displayName,
|
||||
email
|
||||
}));
|
||||
}
|
||||
}
|
||||
96
react/features/base/settings/middleware.native.ts
Normal file
96
react/features/base/settings/middleware.native.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { APP_WILL_MOUNT } from '../app/actionTypes';
|
||||
import { setAudioOnly } from '../audio-only/actions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions.native';
|
||||
import { ISettingsState } from './reducer';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* The middleware of the feature base/settings. Distributes changes to the state
|
||||
* of base/settings to the states of other features computed from the state of
|
||||
* base/settings.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_initializeCallIntegration(store);
|
||||
break;
|
||||
case SETTINGS_UPDATED:
|
||||
_maybeHandleCallIntegrationChange(action);
|
||||
_maybeCrashReportingChange(action);
|
||||
_maybeSetAudioOnly(store, action);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializes the audio device handler based on the `disableCallIntegration` setting.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _initializeCallIntegration({ getState }: IStore) {
|
||||
const { disableCallIntegration } = getState()['features/base/settings'];
|
||||
|
||||
if (typeof disableCallIntegration === 'boolean') {
|
||||
handleCallIntegrationChange(disableCallIntegration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the `disableCallIntegration` setting.
|
||||
*
|
||||
* @param {Object} action - The redux action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeHandleCallIntegrationChange({ settings: { disableCallIntegration } }: {
|
||||
settings: Partial<ISettingsState>;
|
||||
}) {
|
||||
if (typeof disableCallIntegration === 'boolean') {
|
||||
handleCallIntegrationChange(disableCallIntegration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in the `disableCrashReporting` setting.
|
||||
*
|
||||
* @param {Object} action - The redux action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeCrashReportingChange({ settings: { disableCrashReporting } }: {
|
||||
settings: Partial<ISettingsState>;
|
||||
}) {
|
||||
if (typeof disableCrashReporting === 'boolean') {
|
||||
handleCrashReportingChange(disableCrashReporting);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@code startAudioOnly} flag if it's updated in the settings.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} action - The redux action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeSetAudioOnly(
|
||||
{ dispatch }: IStore,
|
||||
{ settings: { startAudioOnly } }: { settings: Partial<ISettingsState>; }) {
|
||||
if (typeof startAudioOnly === 'boolean') {
|
||||
dispatch(setAudioOnly(startAudioOnly));
|
||||
}
|
||||
}
|
||||
85
react/features/base/settings/middleware.web.ts
Normal file
85
react/features/base/settings/middleware.web.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { PREJOIN_INITIALIZED } from '../../prejoin/actionTypes';
|
||||
import { getJwtName } from '../jwt/functions';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { TRACK_ADDED } from '../tracks/actionTypes';
|
||||
import { ITrack } from '../tracks/types';
|
||||
|
||||
import { updateSettings } from './actions';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* The middleware of the feature base/settings. Distributes changes to the state
|
||||
* of base/settings to the states of other features computed from the state of
|
||||
* base/settings.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case PREJOIN_INITIALIZED:
|
||||
_maybeUpdateDisplayName(store);
|
||||
break;
|
||||
case TRACK_ADDED:
|
||||
_maybeUpdateDeviceId(store, action.track);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the display name to the one in JWT if there is one.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeUpdateDisplayName({ dispatch, getState }: IStore) {
|
||||
const state = getState();
|
||||
const hasJwt = Boolean(state['features/base/jwt'].jwt);
|
||||
|
||||
if (hasJwt) {
|
||||
const displayName = getJwtName(state);
|
||||
|
||||
if (displayName) {
|
||||
dispatch(updateSettings({
|
||||
displayName
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe update the camera or mic device id when local track is added or updated.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {ITrack} track - The potential local track.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeUpdateDeviceId({ dispatch, getState }: IStore, track: ITrack) {
|
||||
if (track.local) {
|
||||
const { cameraDeviceId, micDeviceId } = getState()['features/base/settings'];
|
||||
const deviceId = track.jitsiTrack.getDeviceId();
|
||||
|
||||
if (track.mediaType === MEDIA_TYPE.VIDEO && track.videoType === 'camera' && cameraDeviceId !== deviceId) {
|
||||
dispatch(updateSettings({
|
||||
cameraDeviceId: track.jitsiTrack.getDeviceId()
|
||||
}));
|
||||
logger.info(`switched local video device to: ${deviceId}`);
|
||||
} else if (track.mediaType === MEDIA_TYPE.AUDIO && micDeviceId !== deviceId) {
|
||||
dispatch(updateSettings({
|
||||
micDeviceId: track.jitsiTrack.getDeviceId()
|
||||
}));
|
||||
logger.info(`switched local audio input device to: ${deviceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
166
react/features/base/settings/reducer.ts
Normal file
166
react/features/base/settings/reducer.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// @ts-expect-error
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import { escape } from 'lodash-es';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app/actionTypes';
|
||||
import PersistenceRegistry from '../redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { assignIfDefined } from '../util/helpers';
|
||||
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
|
||||
/**
|
||||
* The default/initial redux state of the feature {@code base/settings}.
|
||||
*
|
||||
* @type Object
|
||||
*/
|
||||
const DEFAULT_STATE: ISettingsState = {
|
||||
audioOutputDeviceId: undefined,
|
||||
avatarURL: undefined,
|
||||
cameraDeviceId: undefined,
|
||||
disableCallIntegration: undefined,
|
||||
disableCrashReporting: undefined,
|
||||
disableP2P: undefined,
|
||||
disableSelfView: false,
|
||||
displayName: undefined,
|
||||
email: undefined,
|
||||
localFlipX: true,
|
||||
maxStageParticipants: 1,
|
||||
micDeviceId: undefined,
|
||||
serverURL: undefined,
|
||||
hideShareAudioHelper: false,
|
||||
showSubtitlesOnStage: false,
|
||||
soundsIncomingMessage: true,
|
||||
soundsParticipantJoined: true,
|
||||
soundsParticipantKnocking: true,
|
||||
soundsParticipantLeft: true,
|
||||
soundsTalkWhileMuted: true,
|
||||
soundsReactions: true,
|
||||
startAudioOnly: false,
|
||||
startCarMode: false,
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false,
|
||||
userSelectedAudioOutputDeviceId: undefined,
|
||||
userSelectedCameraDeviceId: undefined,
|
||||
userSelectedMicDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined,
|
||||
userSelectedCameraDeviceLabel: undefined,
|
||||
userSelectedNotifications: {
|
||||
'notify.chatMessages': true
|
||||
},
|
||||
userSelectedMicDeviceLabel: undefined
|
||||
};
|
||||
|
||||
export interface ISettingsState {
|
||||
audioOutputDeviceId?: string;
|
||||
audioSettingsVisible?: boolean;
|
||||
avatarURL?: string;
|
||||
cameraDeviceId?: string | boolean;
|
||||
disableCallIntegration?: boolean;
|
||||
disableCrashReporting?: boolean;
|
||||
disableP2P?: boolean;
|
||||
disableSelfView?: boolean;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
hideShareAudioHelper?: boolean;
|
||||
localFlipX?: boolean;
|
||||
maxStageParticipants?: number;
|
||||
micDeviceId?: string | boolean;
|
||||
serverURL?: string;
|
||||
showSubtitlesOnStage?: boolean;
|
||||
soundsIncomingMessage?: boolean;
|
||||
soundsParticipantJoined?: boolean;
|
||||
soundsParticipantKnocking?: boolean;
|
||||
soundsParticipantLeft?: boolean;
|
||||
soundsReactions?: boolean;
|
||||
soundsTalkWhileMuted?: boolean;
|
||||
startAudioOnly?: boolean;
|
||||
startCarMode?: boolean;
|
||||
startWithAudioMuted?: boolean;
|
||||
startWithVideoMuted?: boolean;
|
||||
userSelectedAudioOutputDeviceId?: string;
|
||||
userSelectedAudioOutputDeviceLabel?: string;
|
||||
userSelectedCameraDeviceId?: string;
|
||||
userSelectedCameraDeviceLabel?: string;
|
||||
userSelectedMicDeviceId?: string;
|
||||
userSelectedMicDeviceLabel?: string;
|
||||
userSelectedNotifications?: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
videoSettingsVisible?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const STORE_NAME = 'features/base/settings';
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature {@code base/settings}.
|
||||
*/
|
||||
const filterSubtree: ISettingsState = {};
|
||||
|
||||
// start with the default state
|
||||
Object.keys(DEFAULT_STATE).forEach(key => {
|
||||
const key1 = key as keyof typeof filterSubtree;
|
||||
|
||||
// @ts-ignore
|
||||
filterSubtree[key1] = true;
|
||||
});
|
||||
|
||||
// we want to filter these props, to not be stored as they represent
|
||||
// what is currently opened/used as devices
|
||||
// @ts-ignore
|
||||
filterSubtree.audioOutputDeviceId = false;
|
||||
filterSubtree.cameraDeviceId = false;
|
||||
filterSubtree.micDeviceId = false;
|
||||
|
||||
PersistenceRegistry.register(STORE_NAME, filterSubtree, DEFAULT_STATE);
|
||||
|
||||
ReducerRegistry.register<ISettingsState>(STORE_NAME, (state = DEFAULT_STATE, action): ISettingsState => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _initSettings(state);
|
||||
|
||||
case SETTINGS_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
...action.settings
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Inits the settings object based on what information we have available.
|
||||
* Info taken into consideration:
|
||||
* - Old Settings.js style data.
|
||||
*
|
||||
* @private
|
||||
* @param {ISettingsState} featureState - The current state of the feature.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _initSettings(featureState: ISettingsState) {
|
||||
let settings = featureState;
|
||||
|
||||
// Old Settings.js values
|
||||
// FIXME: jibri uses old settings.js local storage values to set its display
|
||||
// name and email. Provide another way for jibri to set these values, update
|
||||
// jibri, and remove the old settings.js values.
|
||||
const savedDisplayName = jitsiLocalStorage.getItem('displayname');
|
||||
const savedEmail = jitsiLocalStorage.getItem('email');
|
||||
|
||||
// The helper _.escape will convert null to an empty strings. The empty
|
||||
// string will be saved in settings. On app re-load, because an empty string
|
||||
// is a defined value, it will override any value found in local storage.
|
||||
// The workaround is sidestepping _.escape when the value is not set in
|
||||
// local storage.
|
||||
const displayName = savedDisplayName === null ? undefined : escape(savedDisplayName);
|
||||
const email = savedEmail === null ? undefined : escape(savedEmail);
|
||||
|
||||
settings = assignIfDefined({
|
||||
displayName,
|
||||
email
|
||||
}, settings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
Reference in New Issue
Block a user