init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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';

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

View File

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

View 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';

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

View 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()));
}

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

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../logging/functions';
export default getLogger('features/base/settings');

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

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

View 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}`);
}
}
}

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