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,61 @@
/**
* The redux action which signals that a configuration (commonly known in Jitsi
* Meet as config.js) will be loaded for a specific locationURL.
*
* {
* type: CONFIG_WILL_LOAD,
* locationURL: URL,
* room: string
* }
*/
export const CONFIG_WILL_LOAD = 'CONFIG_WILL_LOAD';
/**
* The redux action which signals that a configuration (commonly known in Jitsi
* Meet as config.js) could not be loaded due to a specific error.
*
* {
* type: LOAD_CONFIG_ERROR,
* error: Error,
* locationURL: URL
* }
*/
export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR';
/**
* The redux action which sets 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.
*
* {
* type: SET_CONFIG,
* config: Object
* }
*/
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';
/**
* The redux action which overwrites configurations represented by the feature
* base/config. The passed on config values overwrite the current values for given props.
*
* {
* type: OVERWRITE_CONFIG,
* config: Object
* }
*/
export const OVERWRITE_CONFIG = 'OVERWRITE_CONFIG';

View File

@@ -0,0 +1,185 @@
// @ts-expect-error
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { IStore } from '../../app/types';
import { addKnownDomains } from '../known-domains/actions';
import { parseURIString } from '../util/uri';
import {
CONFIG_WILL_LOAD,
LOAD_CONFIG_ERROR,
OVERWRITE_CONFIG,
SET_CONFIG,
UPDATE_CONFIG
} from './actionTypes';
import { IConfig } from './configType';
import { _CONFIG_STORE_PREFIX } from './constants';
import { setConfigFromURLParams } from './functions.any';
/**
* Updates the config with new options.
*
* @param {Object} config - The new options (to add).
* @returns {Function}
*/
export function updateConfig(config: IConfig) {
return {
type: UPDATE_CONFIG,
config
};
}
/**
* Signals that the configuration (commonly known in Jitsi Meet as config.js)
* for a specific locationURL will be loaded now.
*
* @param {URL} locationURL - The URL of the location which necessitated the
* loading of a configuration.
* @param {string} room - The name of the room (conference) for which we're loading the config for.
* @returns {{
* type: CONFIG_WILL_LOAD,
* locationURL: URL,
* room: string
* }}
*/
export function configWillLoad(locationURL: URL, room: string) {
return {
type: CONFIG_WILL_LOAD,
locationURL,
room
};
}
/**
* Signals that a configuration (commonly known in Jitsi Meet as config.js)
* could not be loaded due to a specific error.
*
* @param {Error} error - The {@code Error} which prevented the successful
* loading of a configuration.
* @param {URL} locationURL - The URL of the location which necessitated the
* loading of a configuration.
* @returns {{
* type: LOAD_CONFIG_ERROR,
* error: Error,
* locationURL: URL
* }}
*/
export function loadConfigError(error: Error, locationURL: URL) {
return {
type: LOAD_CONFIG_ERROR,
error,
locationURL
};
}
/**
* Overwrites some config values.
*
* @param {Object} config - The new options (to overwrite).
* @returns {{
* type: OVERWRITE_CONFIG,
* config: Object
* }}
*/
export function overwriteConfig(config: Object) {
return {
type: OVERWRITE_CONFIG,
config
};
}
/**
* Sets 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.
*
* @param {Object} config - The configuration to be represented by the feature
* base/config.
* @returns {Function}
*/
export function setConfig(config: IConfig = {}) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { locationURL } = getState()['features/base/connection'];
// Now that the loading of the config was successful override the values
// with the parameters passed in the hash part of the location URI.
// TODO We're still in the middle ground between old Web with config,
// and interfaceConfig used via global variables and new
// Web and mobile reading the respective values from the redux store.
// Only the config will be overridden on React Native, as the other
// globals will be undefined here. It's intentional - we do not care to
// override those configs yet.
locationURL
&& setConfigFromURLParams(
// On Web the config also comes from the window.config global,
// but it is resolved in the loadConfig procedure.
config,
window.interfaceConfig,
locationURL);
let { bosh } = config;
if (bosh) {
// Normalize the BOSH URL.
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL?.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL?.href);
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
config.bosh = bosh;
}
dispatch({
type: SET_CONFIG,
config
});
};
}
/**
* Stores a specific Jitsi Meet config.js object into {@code localStorage}.
*
* @param {string} baseURL - The base URL from which the config.js was
* downloaded.
* @param {Object} config - The Jitsi Meet config.js to store.
* @returns {Function}
*/
export function storeConfig(baseURL: string, config: Object) {
return (dispatch: IStore['dispatch']) => {
// Try to store the configuration in localStorage. If the deployment
// specified 'getroom' as a function, for example, it does not make
// sense to and it will not be stored.
let b = false;
try {
if (typeof window.config === 'undefined' || window.config !== config) {
jitsiLocalStorage.setItem(`${_CONFIG_STORE_PREFIX}/${baseURL}`, JSON.stringify(config));
b = true;
}
} catch (e) {
// Ignore the error because the caching is optional.
}
// If base/config knows a domain, then the app knows it.
if (b) {
try {
dispatch(addKnownDomains(parseURIString(baseURL)?.host));
} catch (e) {
// Ignore the error because the fiddling with "known domains" is
// a side effect here.
}
}
return b;
};
}

View File

@@ -0,0 +1,665 @@
import { ToolbarButton } from '../../toolbox/types';
import { ILoggingConfig } from '../logging/types';
import { DesktopSharingSourceType } from '../tracks/types';
type ButtonsWithNotifyClick = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
'download' |
'embedmeeting' |
'end-meeting' |
'etherpad' |
'feedback' |
'filmstrip' |
'fullscreen' |
'hangup' |
'hangup-menu' |
'help' |
'invite' |
'livestreaming' |
'microphone' |
'mute-everyone' |
'mute-video-everyone' |
'participants-pane' |
'profile' |
'raisehand' |
'recording' |
'security' |
'select-background' |
'settings' |
'shareaudio' |
'sharedvideo' |
'shortcuts' |
'stats' |
'tileview' |
'toggle-camera' |
'videoquality' |
'add-passcode' |
'__end';
type ParticipantMenuButtonsWithNotifyClick = 'allow-video' |
'ask-unmute' |
'conn-status' |
'flip-local-video' |
'grant-moderator' |
'hide-self-view' |
'kick' |
'mute' |
'mute-others' |
'mute-others-video' |
'mute-video' |
'pinToStage' |
'privateMessage' |
'remote-control' |
'send-participant-to-room' |
'verify';
type NotifyClickButtonKey = string |
ButtonsWithNotifyClick |
ParticipantMenuButtonsWithNotifyClick;
export type NotifyClickButton = NotifyClickButtonKey |
{
key: NotifyClickButtonKey;
preventExecution: boolean;
};
export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
'E2EE_OFF_SOUND' |
'E2EE_ON_SOUND' |
'INCOMING_MSG_SOUND' |
'KNOCKING_PARTICIPANT_SOUND' |
'LIVE_STREAMING_OFF_SOUND' |
'LIVE_STREAMING_ON_SOUND' |
'NO_AUDIO_SIGNAL_SOUND' |
'NOISY_AUDIO_INPUT_SOUND' |
'OUTGOING_CALL_EXPIRED_SOUND' |
'OUTGOING_CALL_REJECTED_SOUND' |
'OUTGOING_CALL_RINGING_SOUND' |
'OUTGOING_CALL_START_SOUND' |
'PARTICIPANT_JOINED_SOUND' |
'PARTICIPANT_LEFT_SOUND' |
'RAISE_HAND_SOUND' |
'REACTION_SOUND' |
'RECORDING_OFF_SOUND' |
'RECORDING_ON_SOUND' |
'TALK_WHILE_MUTED_SOUND';
export interface IDeeplinkingPlatformConfig {
appName: string;
appScheme: string;
}
export interface IDeeplinkingMobileConfig extends IDeeplinkingPlatformConfig {
appPackage?: string;
downloadLink: string;
fDroidUrl?: string;
}
export interface IDesktopDownloadConfig {
linux?: string;
macos?: string;
windows?: string;
}
export interface IDeeplinkingDesktopConfig extends IDeeplinkingPlatformConfig {
download?: IDesktopDownloadConfig;
enabled: boolean;
}
export interface IDeeplinkingConfig {
android?: IDeeplinkingMobileConfig;
desktop?: IDeeplinkingDesktopConfig;
disabled?: boolean;
hideLogo?: boolean;
ios?: IDeeplinkingMobileConfig;
}
export type PartialRecord<K extends keyof any, T> = {
[P in K]?: T;
};
export interface INoiseSuppressionConfig {
krisp?: {
bufferOverflowMS?: number;
bvc?: {
allowedDevices?: string;
allowedDevicesExt?: string;
};
debugLogs: boolean;
enableSessionStats?: boolean;
enabled: boolean;
inboundModels?: PartialRecord<string, string>;
logProcessStats?: boolean;
models?: PartialRecord<string, string>;
preloadInboundModels?: PartialRecord<string, string>;
preloadModels?: PartialRecord<string, string>;
useBVC?: boolean;
useSharedArrayBuffer?: boolean;
};
}
export interface IWhiteboardConfig {
collabServerBaseUrl?: string;
enabled?: boolean;
limitUrl?: string;
userLimit?: number;
}
export interface IWatchRTCConfiguration {
allowBrowserLogCollection?: boolean;
collectionInterval?: number;
console?: {
level: string;
override: boolean;
};
debug?: boolean;
keys?: any;
logGetStats?: boolean;
proxyUrl?: string;
rtcApiKey: string;
rtcPeerId?: string;
rtcRoomId?: string;
rtcTags?: string[];
rtcToken?: string;
wsUrl?: string;
}
export interface IConfig {
_desktopSharingSourceDevice?: string;
_immediateReloadThreshold?: string;
_screenshotHistoryRegionUrl?: number;
analytics?: {
amplitudeAPPKey?: string;
blackListedEvents?: string[];
disabled?: boolean;
matomoEndpoint?: string;
matomoSiteID?: string;
obfuscateRoomName?: boolean;
rtcstatsEnabled?: boolean;
rtcstatsEndpoint?: string;
rtcstatsLogFlushSizeBytes?: number;
rtcstatsPollInterval?: number;
rtcstatsSendSdp?: boolean;
rtcstatsStoreLogs?: boolean;
scriptURLs?: Array<string>;
watchRTCEnabled?: boolean;
whiteListedEvents?: string[];
};
apiLogLevels?: Array<'warn' | 'log' | 'error' | 'info' | 'debug'>;
appId?: string;
audioLevelsInterval?: number;
audioQuality?: {
opusMaxAverageBitrate?: number | null;
stereo?: boolean;
};
autoCaptionOnRecord?: boolean;
autoKnockLobby?: boolean;
backgroundAlpha?: number;
bosh?: string;
brandingDataUrl?: string;
brandingRoomAlias?: string;
breakoutRooms?: {
hideAddRoomButton?: boolean;
hideAutoAssignButton?: boolean;
hideJoinRoomButton?: boolean;
};
bridgeChannel?: {
ignoreDomain?: string;
preferSctp?: boolean;
};
buttonsWithNotifyClick?: Array<ButtonsWithNotifyClick | {
key: ButtonsWithNotifyClick;
preventExecution: boolean;
}>;
callDisplayName?: string;
callFlowsEnabled?: boolean;
callHandle?: string;
callUUID?: string;
cameraFacingMode?: string;
channelLastN?: number;
chromeExtensionBanner?: {
chromeExtensionsInfo?: Array<{ id: string; path: string; }>;
edgeUrl?: string;
url?: string;
};
conferenceInfo?: {
alwaysVisible?: Array<string>;
autoHide?: Array<string>;
};
conferenceRequestUrl?: string;
connectionIndicators?: {
autoHide?: boolean;
autoHideTimeout?: number;
disableDetails?: boolean;
disabled?: boolean;
inactiveDisabled?: boolean;
};
constraints?: {
video?: {
height?: {
ideal?: number;
max?: number;
min?: number;
};
};
};
corsAvatarURLs?: Array<string>;
customParticipantMenuButtons?: Array<{ icon: string; id: string; text: string; }>;
customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
deeplinking?: IDeeplinkingConfig;
defaultLanguage?: string;
defaultLocalDisplayName?: string;
defaultLogoUrl?: string;
defaultRemoteDisplayName?: string;
deploymentInfo?: {
envType?: string;
environment?: string;
product?: string;
region?: string;
shard?: string;
userRegion?: string;
};
deploymentUrls?: {
downloadAppsUrl?: string;
userDocumentationURL?: string;
};
desktopSharingFrameRate?: {
max?: number;
min?: number;
};
desktopSharingSources?: Array<DesktopSharingSourceType>;
dialInConfCodeUrl?: string;
dialInNumbersUrl?: string;
dialOutAuthUrl?: string;
dialOutRegionUrl?: string;
disable1On1Mode?: boolean | null;
disableAEC?: boolean;
disableAGC?: boolean;
disableAP?: boolean;
disableAddingBackgroundImages?: boolean;
disableAudioLevels?: boolean;
disableBeforeUnloadHandlers?: boolean;
disableCameraTintForeground?: boolean;
disableChatSmileys?: boolean;
disableDeepLinking?: boolean;
disableFilmstripAutohiding?: boolean;
disableFocus?: boolean;
disableIframeAPI?: boolean;
disableIncomingMessageSound?: boolean;
disableInitialGUM?: boolean;
disableInviteFunctions?: boolean;
disableJoinLeaveSounds?: boolean;
disableLocalVideoFlip?: boolean;
disableModeratorIndicator?: boolean;
disableNS?: boolean;
disablePolls?: boolean;
disableProfile?: boolean;
disableReactions?: boolean;
disableReactionsInChat?: boolean;
disableReactionsModeration?: boolean;
disableRecordAudioNotification?: boolean;
disableRemoteControl?: boolean;
disableRemoteMute?: boolean;
disableRemoveRaisedHandOnFocus?: boolean;
disableResponsiveTiles?: boolean;
disableRtx?: boolean;
disableSelfDemote?: boolean;
disableSelfView?: boolean;
disableSelfViewSettings?: boolean;
disableShortcuts?: boolean;
disableShowMoreStats?: boolean;
disableSimulcast?: boolean;
disableSpeakerStatsSearch?: boolean;
disableThirdPartyRequests?: boolean;
disableTileEnlargement?: boolean;
disableTileView?: boolean;
disableVirtualBackground?: boolean;
disabledNotifications?: Array<string>;
disabledSounds?: Array<Sounds>;
displayJids?: boolean;
doNotFlipLocalVideo?: boolean;
doNotStoreRoom?: boolean;
dropbox?: {
appKey: string;
redirectURI?: string;
};
dynamicBrandingUrl?: string;
e2ee?: {
disabled?: boolean;
externallyManagedKey?: boolean;
labels?: {
description?: string;
label?: string;
tooltip?: string;
warning?: string;
};
};
e2eeLabels?: {
description?: string;
label?: string;
tooltip?: string;
warning?: string;
};
e2eping?: {
enabled?: boolean;
maxConferenceSize?: number;
maxMessagesPerSecond?: number;
numRequests?: number;
};
enableCalendarIntegration?: boolean;
enableClosePage?: boolean;
enableDisplayNameInStats?: boolean;
enableEmailInStats?: boolean;
enableEncodedTransformSupport?: boolean;
enableForcedReload?: boolean;
enableInsecureRoomNameWarning?: boolean;
enableLobbyChat?: boolean;
enableNoAudioDetection?: boolean;
enableNoisyMicDetection?: boolean;
enableOpusRed?: boolean;
enableRemb?: boolean;
enableSaveLogs?: boolean;
enableTalkWhileMuted?: boolean;
enableTcc?: boolean;
enableWebHIDFeature?: boolean;
enableWelcomePage?: boolean;
etherpad_base?: string;
faceLandmarks?: {
captureInterval?: number;
enableDisplayFaceExpressions?: boolean;
enableFaceCentering?: boolean;
enableFaceExpressionsDetection?: boolean;
enableRTCStats?: boolean;
faceCenteringThreshold?: number;
};
feedbackPercentage?: number;
fileRecordingsServiceEnabled?: boolean;
fileRecordingsServiceSharingEnabled?: boolean;
fileSharing?: {
apiUrl?: string;
enabled?: boolean;
maxFileSize?: number;
};
filmstrip?: {
alwaysShowResizeBar?: boolean;
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
disabled?: boolean;
initialWidth?: number;
minParticipantCountForTopPanel?: number;
};
flags?: {
ssrcRewritingEnabled: boolean;
};
focusUserJid?: string;
forceTurnRelay?: boolean;
gatherStats?: boolean;
giphy?: {
displayMode?: 'all' | 'tile' | 'chat';
enabled?: boolean;
rating?: 'g' | 'pg' | 'pg-13' | 'r';
sdkKey?: string;
tileTime?: number;
};
googleApiApplicationClientID?: string;
gravatar?: {
baseUrl?: string;
disabled?: boolean;
};
gravatarBaseURL?: string;
guestDialOutStatusUrl?: string;
guestDialOutUrl?: string;
helpCentreURL?: string;
hiddenDomain?: string;
hiddenPremeetingButtons?: Array<'microphone' | 'camera' | 'select-background' | 'invite' | 'settings'>;
hideAddRoomButton?: boolean;
hideConferenceSubject?: boolean;
hideConferenceTimer?: boolean;
hideDisplayName?: boolean;
hideDominantSpeakerBadge?: boolean;
hideEmailInSettings?: boolean;
hideLobbyButton?: boolean;
hideLoginButton?: boolean;
hideParticipantsStats?: boolean;
hideRecordingLabel?: boolean;
hosts?: {
anonymousdomain?: string;
authdomain?: string;
domain: string;
focus?: string;
muc: string;
visitorFocus?: string;
};
iAmRecorder?: boolean;
iAmSipGateway?: boolean;
iAmSpot?: boolean;
ignoreStartMuted?: boolean;
inviteAppName?: string | null;
inviteServiceCallFlowsUrl?: string;
inviteServiceUrl?: string;
jaasActuatorUrl?: string;
jaasConferenceCreatorUrl?: string;
jaasFeedbackMetadataURL?: string;
jaasTokenUrl?: string;
legalUrls?: {
helpCentre: string;
privacy: string;
security: string;
terms: string;
};
liveStreaming?: {
dataPrivacyLink?: string;
enabled?: boolean;
helpLink?: string;
termsLink?: string;
validatorRegExpString?: string;
};
liveStreamingEnabled?: boolean;
lobby?: {
autoKnock?: boolean;
enableChat?: boolean;
};
localRecording?: {
disable?: boolean;
disableSelfRecording?: boolean;
notifyAllParticipants?: boolean;
};
localSubject?: string;
locationURL?: URL;
logging?: ILoggingConfig;
mainToolbarButtons?: Array<Array<string>>;
maxFullResolutionParticipants?: number;
microsoftApiApplicationClientID?: string;
moderatedRoomServiceUrl?: string;
mouseMoveCallbackInterval?: number;
noiseSuppression?: INoiseSuppressionConfig;
noticeMessage?: string;
notificationTimeouts?: {
extraLong?: number;
long?: number;
medium?: number;
short?: number;
sticky?: number;
};
notifications?: Array<string>;
notifyOnConferenceDestruction?: boolean;
openSharedDocumentOnJoin?: boolean;
opusMaxAverageBitrate?: number;
p2p?: {
backToP2PDelay?: number;
codecPreferenceOrder?: Array<string>;
enabled?: boolean;
iceTransportPolicy?: string;
mobileCodecPreferenceOrder?: Array<string>;
mobileScreenshareCodec?: string;
stunServers?: Array<{ urls: string; }>;
};
participantMenuButtonsWithNotifyClick?: Array<string | ParticipantMenuButtonsWithNotifyClick | {
key: string | ParticipantMenuButtonsWithNotifyClick;
preventExecution: boolean;
}>;
participantsPane?: {
enabled?: boolean;
hideModeratorSettingsTab?: boolean;
hideMoreActionsButton?: boolean;
hideMuteAllButton?: boolean;
};
pcStatsInterval?: number;
peopleSearchQueryTypes?: string[];
peopleSearchTokenLocation?: string;
peopleSearchUrl?: string;
preferBosh?: boolean;
preferVisitor?: boolean;
preferredTranscribeLanguage?: string;
prejoinConfig?: {
enabled?: boolean;
hideDisplayName?: boolean;
hideExtraJoinButtons?: Array<string>;
preCallTestEnabled?: boolean;
preCallTestICEUrl?: string;
};
raisedHands?: {
disableLowerHandByModerator?: boolean;
disableLowerHandNotification?: boolean;
disableNextSpeakerNotification?: boolean;
disableRemoveRaisedHandOnFocus?: boolean;
};
readOnlyName?: boolean;
recordingLimit?: {
appName?: string;
appURL?: string;
limit?: number;
};
recordingService?: {
enabled?: boolean;
hideStorageWarning?: boolean;
sharingEnabled?: boolean;
};
recordingSharingUrl?: string;
recordings?: {
consentLearnMoreLink?: string;
recordAudioAndVideo?: boolean;
requireConsent?: boolean;
showPrejoinWarning?: boolean;
showRecordingLink?: boolean;
skipConsentInMeeting?: boolean;
suggestRecording?: boolean;
};
remoteVideoMenu?: {
disableDemote?: boolean;
disableGrantModerator?: boolean;
disableKick?: boolean;
disablePrivateChat?: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat';
disabled?: boolean;
};
replaceParticipant?: string;
requireDisplayName?: boolean;
resolution?: number;
roomPasswordNumberOfDigits?: number;
salesforceUrl?: string;
screenshotCapture?: {
enabled?: boolean;
mode?: 'always' | 'recording';
};
securityUi?: {
disableLobbyPassword?: boolean;
hideLobbyButton?: boolean;
};
serviceUrl?: string;
sharedVideoAllowedURLDomains?: Array<string>;
sipInviteUrl?: string;
speakerStats?: {
disableSearch?: boolean;
disabled?: boolean;
order?: Array<'role' | 'name' | 'hasLeft'>;
};
speakerStatsOrder?: Array<'role' | 'name' | 'hasLeft'>;
startAudioMuted?: number;
startAudioOnly?: boolean;
startLastN?: number;
startScreenSharing?: boolean;
startSilent?: boolean;
startVideoMuted?: number;
startWithAudioMuted?: boolean;
startWithVideoMuted?: boolean;
stereo?: boolean;
subject?: string;
testing?: {
assumeBandwidth?: boolean;
debugAudioLevels?: boolean;
dumpTranscript?: boolean;
failICE?: boolean;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;
showSpotConsentDialog?: boolean;
skipInterimTranscriptions?: boolean;
testMode?: boolean;
};
tileView?: {
disabled?: boolean;
numberOfVisibleTiles?: number;
};
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenGetUserInfoOutOfContext?: boolean;
tokenLogoutUrl?: string;
tokenRespectTenant?: boolean;
toolbarButtons?: Array<ToolbarButton>;
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;
initialTimeout?: number;
timeout?: number;
};
transcribeWithAppLanguage?: boolean;
transcribingEnabled?: boolean;
transcription?: {
autoCaptionOnTranscribe?: boolean;
autoTranscribeOnRecord?: boolean;
disableClosedCaptions?: boolean;
enabled?: boolean;
inviteJigasiOnBackendTranscribing?: boolean;
preferredLanguage?: string;
translationLanguages?: Array<string>;
translationLanguagesHead?: Array<string>;
useAppLanguage?: boolean;
};
useHostPageLocalStorage?: boolean;
useTurnUdp?: boolean;
videoQuality?: {
codecPreferenceOrder?: Array<string>;
maxBitratesVideo?: {
[key: string]: {
high?: number;
low?: number;
standard?: number;
};
};
minHeightForQualityLvl?: {
[key: number]: string;
};
mobileCodecPreferenceOrder?: Array<string>;
persist?: boolean;
};
visitors?: {
enableMediaOnPromote?: {
audio?: boolean;
video?: boolean;
};
queueService: string;
};
watchRTCConfigParams?: IWatchRTCConfiguration;
webhookProxyUrl?: string;
webrtcIceTcpDisable?: boolean;
webrtcIceUdpDisable?: boolean;
websocket?: string;
websocketKeepAliveUrl?: string;
welcomePage?: {
customUrl?: string;
disabled?: boolean;
};
whiteboard?: IWhiteboardConfig;
}

View File

@@ -0,0 +1,254 @@
import { isEmbedded } from '../util/embedUtils';
import extraConfigWhitelist from './extraConfigWhitelist';
import isEmbeddedConfigWhitelist from './isEmbeddedConfigWhitelist';
/**
* The config keys to whitelist, the keys that can be overridden.
* Whitelisting a key allows all properties under that key to be overridden.
* For example whitelisting 'p2p' allows 'p2p.enabled' to be overridden, and
* overriding 'p2p.enabled' does not modify any other keys under 'p2p'.
* The whitelist is used only for config.js.
*
* @type Array
*/
export default [
'_desktopSharingSourceDevice',
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'analytics.disabled',
'analytics.rtcstatsEnabled',
'analytics.watchRTCEnabled',
'audioLevelsInterval',
'audioQuality',
'autoKnockLobby',
'apiLogLevels',
'avgRtpStatsN',
'backgroundAlpha',
'brandingRoomAlias',
'breakoutRooms',
'bridgeChannel',
'buttonsWithNotifyClick',
/**
* The display name of the CallKit call representing the conference/meeting
* associated with this config.js including while the call is ongoing in the
* UI presented by CallKit and in the system-wide call history. The property
* is meant for use cases in which the room name is not desirable as a
* display name for CallKit purposes and the desired display name is not
* provided in the form of a JWT callee. As the value is associated with a
* conference/meeting, the value makes sense not as a deployment-wide
* configuration, only as a runtime configuration override/overwrite
* provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callDisplayName',
'callFlowsEnabled',
/**
* The handle
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
* the CallKit call representing the conference/meeting associated with this
* config.js. The property is meant for use cases in which the room URL is
* not desirable as the handle for CallKit purposes. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callHandle',
/**
* The UUID of the CallKit call representing the conference/meeting
* associated with this config.js. The property is meant for use cases in
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
* clients/consumers prior to giving control to Jitsi Meet. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callUUID',
'cameraFacingMode',
'conferenceInfo',
'channelLastN',
'connectionIndicators',
'constraints',
'deeplinking.disabled',
'deeplinking.desktop.enabled',
'defaultLocalDisplayName',
'defaultRemoteDisplayName',
'desktopSharingFrameRate',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
'disableAGC',
'disableAP',
'disableAddingBackgroundImages',
'disableAudioLevels',
'disableBeforeUnloadHandlers',
'disableCameraTintForeground',
'disableChatSmileys',
'disableDeepLinking',
'disabledNotifications',
'disabledSounds',
'disableFilmstripAutohiding',
'disableInitialGUM',
'disableInviteFunctions',
'disableIncomingMessageSound',
'disableJoinLeaveSounds',
'disableLocalVideoFlip',
'disableModeratorIndicator',
'disableNS',
'disablePolls',
'disableProfile',
'disableReactions',
'disableReactionsInChat',
'disableReactionsModeration',
'disableRecordAudioNotification',
'disableRemoteControl',
'disableRemoteMute',
'disableResponsiveTiles',
'disableRtx',
'disableSelfDemote',
'disableSelfView',
'disableSelfViewSettings',
'disableShortcuts',
'disableShowMoreStats',
'disableRemoveRaisedHandOnFocus',
'disableSpeakerStatsSearch',
'speakerStatsOrder',
'disableSimulcast',
'disableThirdPartyRequests',
'disableTileView',
'disableTileEnlargement',
'disableVirtualBackground',
'displayJids',
'doNotStoreRoom',
'doNotFlipLocalVideo',
'dropbox.appKey',
'e2eeLabels',
'e2ee',
'e2eping',
'enableCalendarIntegration',
'enableDisplayNameInStats',
'enableEmailInStats',
'enableEncodedTransformSupport',
'enableInsecureRoomNameWarning',
'enableLobbyChat',
'enableOpusRed',
'enableRemb',
'enableSaveLogs',
'enableTalkWhileMuted',
'enableNoAudioDetection',
'enableNoisyMicDetection',
'enableTcc',
'faceLandmarks',
'feedbackPercentage',
'fileSharing.enabled',
'filmstrip',
'flags',
'forceTurnRelay',
'gatherStats',
'giphy',
'googleApiApplicationClientID',
'gravatar.disabled',
'hiddenPremeetingButtons',
'hideConferenceSubject',
'hideDisplayName',
'hideDominantSpeakerBadge',
'hideRecordingLabel',
'hideParticipantsStats',
'hideConferenceTimer',
'hideAddRoomButton',
'hideEmailInSettings',
'hideLobbyButton',
'iAmRecorder',
'iAmSipGateway',
'iAmSpot',
'ignoreStartMuted',
'inviteAppName',
'liveStreaming.enabled',
'liveStreamingEnabled',
'lobby',
'localRecording',
'localSubject',
'logging',
'mainToolbarButtons',
'maxFullResolutionParticipants',
'mouseMoveCallbackInterval',
'notifications',
'notificationTimeouts',
'notifyOnConferenceDestruction',
'openSharedDocumentOnJoin',
'opusMaxAverageBitrate',
'p2p.backToP2PDelay',
'p2p.codecPreferenceOrder',
'p2p.enabled',
'p2p.iceTransportPolicy',
'p2p.mobileCodecPreferenceOrder',
'p2p.mobileScreenshareCodec',
'participantMenuButtonsWithNotifyClick',
'participantsPane',
'pcStatsInterval',
'preferBosh',
'preferVisitor',
'prejoinConfig.enabled',
'prejoinConfig.hideDisplayName',
'prejoinConfig.hideExtraJoinButtons',
'raisedHands',
'recordingService',
'requireDisplayName',
'remoteVideoMenu',
'roomPasswordNumberOfDigits',
'readOnlyName',
'recordings.recordAudioAndVideo',
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'replaceParticipant',
'resolution',
'screenshotCapture',
'securityUi',
'speakerStats',
'startAudioMuted',
'startAudioOnly',
'startLastN',
'startScreenSharing',
'startSilent',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'stereo',
'subject',
'testing',
'toolbarButtons',
'toolbarConfig',
'tileView',
'transcribingEnabled',
'transcription',
'useHostPageLocalStorage',
'useTurnUdp',
'videoQuality',
'visitors.enableMediaOnPromote',
'watchRTCConfigParams.allowBrowserLogCollection',
'watchRTCConfigParams.collectionInterval',
'watchRTCConfigParams.console',
'watchRTCConfigParams.debug',
'watchRTCConfigParams.keys',
'watchRTCConfigParams.logGetStats',
'watchRTCConfigParams.rtcApiKey',
'watchRTCConfigParams.rtcPeerId',
'watchRTCConfigParams.rtcRoomId',
'watchRTCConfigParams.rtcTags',
'watchRTCConfigParams.rtcToken',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable',
'whiteboard.enabled'
].concat(extraConfigWhitelist).concat(isEmbedded() ? isEmbeddedConfigWhitelist : []);

View File

@@ -0,0 +1,43 @@
/**
* The prefix of the {@code localStorage} key into which {@link storeConfig}
* stores and from which {@link restoreConfig} restores.
*
* @protected
* @type string
*/
export const _CONFIG_STORE_PREFIX = 'config.js';
/**
* The toolbar buttons to show on premeeting screens.
*/
export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background', 'invite', 'settings' ];
/**
* The toolbar buttons to show on 3rdParty prejoin screen.
*/
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];
/**
* The set of feature flags.
*
* @enum {string}
*/
export const FEATURE_FLAGS = {
SSRC_REWRITING: 'ssrcRewritingEnabled'
};
/**
* The URL at which the terms (of service/use) are available to the user.
*/
export const DEFAULT_TERMS_URL = 'https://jitsi.org/meet/terms';
/**
* The URL at which the privacy policy is available to the user.
*/
export const DEFAULT_PRIVACY_URL = 'https://jitsi.org/meet/privacy';
/**
* The URL at which the help centre is available to the user.
*/
export const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';

View File

@@ -0,0 +1,4 @@
/**
* Deploy-specific configuration whitelists.
*/
export default [];

View File

@@ -0,0 +1,4 @@
/**
* Deploy-specific interface_config whitelists.
*/
export default [];

View File

@@ -0,0 +1,453 @@
// @ts-ignore
import { jitsiLocalStorage } from '@jitsi/js-utils';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { safeJsonParse } from '@jitsi/js-utils/json';
import { isEmpty, mergeWith, pick } from 'lodash-es';
import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../participants/functions';
import { isEmbedded } from '../util/embedUtils';
import { parseURLParams } from '../util/parseURLParams';
import { IConfig } from './configType';
import CONFIG_WHITELIST from './configWhitelist';
import {
DEFAULT_HELP_CENTRE_URL,
DEFAULT_PRIVACY_URL,
DEFAULT_TERMS_URL,
FEATURE_FLAGS,
_CONFIG_STORE_PREFIX
} from './constants';
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import logger from './logger';
// XXX The function getRoomName is split out of
// functions.any.js because it is bundled in both app.bundle and
// do_external_connect, webpack 1 does not support tree shaking, and we don't
// want all functions to be bundled in do_external_connect.
export { default as getRoomName } from './getRoomName';
/**
* Create a "fake" configuration object for the given base URL. This is used in case the config
* couldn't be loaded in the welcome page, so at least we have something to try with.
*
* @param {string} baseURL - URL of the deployment for which we want the fake config.
* @returns {Object}
*/
export function createFakeConfig(baseURL: string) {
const url = new URL(baseURL);
return {
hosts: {
domain: url.hostname,
muc: `conference.${url.hostname}`
},
bosh: `${baseURL}http-bind`,
p2p: {
enabled: true
}
};
}
/**
* Selector used to get the meeting region.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getMeetingRegion(state: IReduxState) {
return state['features/base/config']?.deploymentInfo?.region || '';
}
/**
* Selector used to get the SSRC-rewriting feature flag.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getSsrcRewritingFeatureFlag(state: IReduxState) {
return getFeatureFlag(state, FEATURE_FLAGS.SSRC_REWRITING) ?? true;
}
/**
* Selector used to get a feature flag.
*
* @param {Object} state - The global state.
* @param {string} featureFlag - The name of the feature flag.
* @returns {boolean}
*/
export function getFeatureFlag(state: IReduxState, featureFlag: string) {
const featureFlags = state['features/base/config']?.flags || {};
return featureFlags[featureFlag as keyof typeof featureFlags];
}
/**
* Selector used to get the disableRemoveRaisedHandOnFocus.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableRemoveRaisedHandOnFocus(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableRemoveRaisedHandOnFocus || false;
}
/**
* Selector used to get the disableLowerHandByModerator.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableLowerHandByModerator(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableLowerHandByModerator || false;
}
/**
* Selector used to get the disableLowerHandNotification.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableLowerHandNotification(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableLowerHandNotification || true;
}
/**
* Selector used to get the disableNextSpeakerNotification.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableNextSpeakerNotification(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableNextSpeakerNotification || false;
}
/**
* Selector used to get the endpoint used for fetching the recording.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getRecordingSharingUrl(state: IReduxState) {
return state['features/base/config'].recordingSharingUrl;
}
/**
* Overrides JSON properties in {@code config} and
* {@code interfaceConfig} Objects with the values from {@code newConfig}.
* Overrides only the whitelisted keys.
*
* @param {Object} config - The config Object in which we'll be overriding
* properties.
* @param {Object} interfaceConfig - The interfaceConfig Object in which we'll
* be overriding properties.
* @param {Object} json - Object containing configuration properties.
* Destination object is selected based on root property name:
* {
* config: {
* // config.js properties here
* },
* interfaceConfig: {
* // interface_config.js properties here
* }
* }.
* @returns {void}
*/
export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json: any) {
for (const configName of Object.keys(json)) {
let configObj;
if (configName === 'config') {
configObj = config;
} else if (configName === 'interfaceConfig') {
configObj = interfaceConfig;
}
if (configObj) {
const configJSON
= getWhitelistedJSON(configName as 'interfaceConfig' | 'config', json[configName]);
if (!isEmpty(configJSON)) {
logger.info(`Extending ${configName} with: ${JSON.stringify(configJSON)}`);
// eslint-disable-next-line arrow-body-style
mergeWith(configObj, configJSON, (oldValue, newValue) => {
// XXX We don't want to merge the arrays, we want to
// overwrite them.
return Array.isArray(oldValue) ? newValue : undefined;
});
}
}
}
}
/* eslint-enable max-params, no-shadow */
/**
* Apply whitelist filtering for configs with whitelists.
* Only extracts overridden values for keys we allow to be overridden.
*
* @param {string} configName - The config name, one of config or interfaceConfig.
* @param {Object} configJSON - The object with keys and values to override.
* @returns {Object} - The result object only with the keys
* that are whitelisted.
*/
export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object {
// Disable whitelisting in dev mode.
if (typeof __DEV__ !== 'undefined' && __DEV__) {
logger.warn('Whitelisting is disabled in dev mode, accepting any overrides');
return configJSON;
}
if (configName === 'interfaceConfig') {
return pick(configJSON, INTERFACE_CONFIG_WHITELIST);
} else if (configName === 'config') {
return pick(configJSON, CONFIG_WHITELIST);
}
return configJSON;
}
/**
* Selector for determining if the display name is read only.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isNameReadOnly(state: IReduxState): boolean {
return Boolean(state['features/base/config'].disableProfile
|| state['features/base/config'].readOnlyName);
}
/**
* Selector for determining if the participant is the next one in the queue to speak.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isNextToSpeak(state: IReduxState): boolean {
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue || [];
const participantId = getLocalParticipant(state)?.id;
return participantId === raisedHandsQueue[0]?.id;
}
/**
* Selector for determining if the next to speak participant in the queue has been notified.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function hasBeenNotified(state: IReduxState): boolean {
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue;
return Boolean(raisedHandsQueue[0]?.hasBeenNotified);
}
/**
* Selector for determining if the display name is visible.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isDisplayNameVisible(state: IReduxState): boolean {
return !state['features/base/config'].hideDisplayName;
}
/**
* Restores a Jitsi Meet config.js from {@code localStorage} if it was
* previously downloaded from a specific {@code baseURL} and stored with
* {@link storeConfig}.
*
* @param {string} baseURL - The base URL from which the config.js was
* previously downloaded and stored with {@code storeConfig}.
* @returns {?Object} The Jitsi Meet config.js which was previously downloaded
* from {@code baseURL} and stored with {@code storeConfig} if it was restored;
* otherwise, {@code undefined}.
*/
export function restoreConfig(baseURL: string) {
const key = `${_CONFIG_STORE_PREFIX}/${baseURL}`;
const config = jitsiLocalStorage.getItem(key);
if (config) {
try {
return safeJsonParse(config) || undefined;
} catch (e) {
// Somehow incorrect data ended up in the storage. Clean it up.
jitsiLocalStorage.removeItem(key);
}
}
return undefined;
}
/**
* Inspects the hash part of the location URI and overrides values specified
* there in the corresponding config objects given as the arguments. The syntax
* is: {@code https://server.com/room#config.debug=true
* &interfaceConfig.showButton=false}.
*
* In the hash part each parameter will be parsed to JSON and then the root
* object will be matched with the corresponding config object given as the
* argument to this function.
*
* @param {Object} config - This is the general config.
* @param {Object} interfaceConfig - This is the interface config.
* @param {URI} location - The new location to which the app is navigating to.
* @returns {void}
*/
export function setConfigFromURLParams(
config: IConfig, interfaceConfig: any, location: string | URL) {
const params = parseURLParams(location);
const json: any = {};
// At this point we have:
// params = {
// "config.disableAudioLevels": false,
// "config.channelLastN": -1,
// "interfaceConfig.APP_NAME": "Jitsi Meet"
// }
// We want to have:
// json = {
// config: {
// "disableAudioLevels": false,
// "channelLastN": -1
// },
// interfaceConfig: {
// "APP_NAME": "Jitsi Meet"
// }
// }
config && (json.config = {});
interfaceConfig && (json.interfaceConfig = {});
for (const param of Object.keys(params)) {
let base = json;
const names = param.split('.');
const last = names.pop() ?? '';
for (const name of names) {
base = base[name] = base[name] || {};
}
base[last] = params[param];
}
overrideConfigJSON(config, interfaceConfig, json);
// Print warning about deprecated URL params
if ('interfaceConfig.SUPPORT_URL' in params) {
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
+ ' Please use supportUrl from advanced branding!');
}
if ('config.defaultLogoUrl' in params) {
logger.warn('Using defaultLogoUrl config URL overwrite is deprecated.'
+ ' Please use logoImageUrl from advanced branding!');
}
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
if ('config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|| (typeof deploymentUrlsConfig === 'object'
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig))) {
logger.warn('Using deploymentUrls config URL overwrite is deprecated.'
+ ' Please use downloadAppsUrl and/or userDocumentationURL from advanced branding!');
}
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
if (('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|| ('config.liveStreaming.termsLink' in params)
|| ('config.liveStreaming.dataPrivacyLink' in params)
|| ('config.liveStreaming.helpLink' in params)
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
&& (
'termsLink' in liveStreamingConfig
|| 'dataPrivacyLink' in liveStreamingConfig
|| 'helpLink' in liveStreamingConfig
)
)) {
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
}
// When not in an iframe, start without media if the pre-join page is not enabled.
if (!isEmbedded()
&& 'config.prejoinConfig.enabled' in params && config.prejoinConfig?.enabled === false) {
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
config.disableInitialGUM = true;
}
}
/* eslint-enable max-params */
/**
* Returns the dial out url.
*
* @param {Object} state - The state of the app.
* @returns {string}
*/
export function getDialOutStatusUrl(state: IReduxState) {
return state['features/base/config'].guestDialOutStatusUrl;
}
/**
* Returns the dial out status url.
*
* @param {Object} state - The state of the app.
* @returns {string}
*/
export function getDialOutUrl(state: IReduxState) {
return state['features/base/config'].guestDialOutUrl;
}
/**
* Selector to return the security UI config.
*
* @param {IReduxState} state - State object.
* @returns {Object}
*/
export function getSecurityUiConfig(state: IReduxState) {
return state['features/base/config']?.securityUi || {};
}
/**
* Returns the terms, privacy and help centre URL's.
*
* @param {IReduxState} state - The state of the application.
* @returns {{
* privacy: string,
* helpCentre: string,
* terms: string
* }}
*/
export function getLegalUrls(state: IReduxState) {
const helpCentreURL = state['features/base/config']?.helpCentreURL;
const configLegalUrls = state['features/base/config']?.legalUrls;
return {
privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL,
helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL,
terms: configLegalUrls?.terms || DEFAULT_TERMS_URL
};
}
/**
* Utility function to debounce the execution of a callback function.
*
* @param {Function} callback - The callback to debounce.
* @param {number} delay - The debounce delay in milliseconds.
* @returns {Function} - A debounced function that delays the execution of the callback.
*/
export function debounce(callback: (...args: any[]) => void, delay: number) {
let timerId: any;
return (...args: any[]) => {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => callback(...args), delay);
};
}

View File

@@ -0,0 +1,53 @@
import { NativeModules } from 'react-native';
import { IReduxState } from '../../app/types';
import { REPLACE_PARTICIPANT } from '../flags/constants';
import { getFeatureFlag } from '../flags/functions';
import { IConfig, IDeeplinkingConfig } from './configType';
export * from './functions.any';
/**
* Removes all analytics related options from the given configuration, in case of a libre build.
*
* @param {*} config - The configuration which needs to be cleaned up.
* @returns {void}
*/
export function _cleanupConfig(config: IConfig) {
config.analytics = config.analytics ?? {};
config.analytics.scriptURLs = [];
if (NativeModules.AppInfo.LIBRE_BUILD) {
delete config.analytics?.amplitudeAPPKey;
delete config.analytics?.rtcstatsEnabled;
delete config.analytics?.rtcstatsEndpoint;
delete config.analytics?.rtcstatsPollInterval;
delete config.analytics?.rtcstatsSendSdp;
delete config.analytics?.obfuscateRoomName;
delete config.analytics?.watchRTCEnabled;
delete config.watchRTCConfigParams;
config.giphy = { enabled: false };
}
}
/**
* Returns the replaceParticipant config.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function getReplaceParticipant(state: IReduxState): string {
return getFeatureFlag(state, REPLACE_PARTICIPANT, false);
}
/**
* Sets the defaults for deeplinking.
*
* @param {IDeeplinkingConfig} _deeplinking - The deeplinking config.
* @returns {void}
*/
export function _setDeeplinkingDefaults(_deeplinking: IDeeplinkingConfig) {
return;
}

View File

@@ -0,0 +1,87 @@
import { IReduxState } from '../../app/types';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import {
IConfig,
IDeeplinkingConfig,
IDeeplinkingDesktopConfig,
IDeeplinkingMobileConfig
} from './configType';
export * from './functions.any';
/**
* Removes all analytics related options from the given configuration, in case of a libre build.
*
* @param {*} _config - The configuration which needs to be cleaned up.
* @returns {void}
*/
export function _cleanupConfig(_config: IConfig) {
return;
}
/**
* Returns the replaceParticipant config.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function getReplaceParticipant(state: IReduxState): string | undefined {
return state['features/base/config'].replaceParticipant;
}
/**
* Returns the configuration value of web-hid feature.
*
* @param {Object} state - The state of the app.
* @returns {boolean} True if web-hid feature should be enabled, otherwise false.
*/
export function getWebHIDFeatureConfig(state: IReduxState): boolean {
return state['features/base/config'].enableWebHIDFeature || false;
}
/**
* Returns whether audio level measurement is enabled or not.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function areAudioLevelsEnabled(state: IReduxState): boolean {
return !state['features/base/config'].disableAudioLevels && JitsiMeetJS.isCollectingLocalStats();
}
/**
* Sets the defaults for deeplinking.
*
* @param {IDeeplinkingConfig} deeplinking - The deeplinking config.
* @returns {void}
*/
export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
deeplinking.desktop = deeplinking.desktop || {} as IDeeplinkingDesktopConfig;
deeplinking.android = deeplinking.android || {} as IDeeplinkingMobileConfig;
deeplinking.ios = deeplinking.ios || {} as IDeeplinkingMobileConfig;
const { android, desktop, ios } = deeplinking;
desktop.appName = desktop.appName || 'Jitsi Meet';
desktop.appScheme = desktop.appScheme || 'jitsi-meet';
desktop.download = desktop.download || {};
desktop.download.windows = desktop.download.windows
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.exe';
desktop.download.macos = desktop.download.macos
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.dmg';
desktop.download.linux = desktop.download.linux
|| 'https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-x86_64.AppImage';
ios.appName = ios.appName || 'Jitsi Meet';
ios.appScheme = ios.appScheme || 'org.jitsi.meet';
ios.downloadLink = ios.downloadLink
|| 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905';
android.appName = android.appName || 'Jitsi Meet';
android.appScheme = android.appScheme || 'org.jitsi.meet';
android.downloadLink = android.downloadLink
|| 'https://play.google.com/store/apps/details?id=org.jitsi.meet';
android.appPackage = android.appPackage || 'org.jitsi.meet';
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/packages/org.jitsi.meet/';
}

View File

@@ -0,0 +1,15 @@
import { getBackendSafeRoomName } from '../util/uri';
/**
* Builds and returns the room name.
*
* @returns {string}
*/
export default function getRoomName(): string | undefined {
const path = window.location.pathname;
// The last non-directory component of the path (name) is the room.
const roomName = path.substring(path.lastIndexOf('/') + 1) || undefined;
return getBackendSafeRoomName(roomName);
}

View File

@@ -0,0 +1,57 @@
import { isEmbedded } from '../util/embedUtils';
import extraInterfaceConfigWhitelistCopy from './extraInterfaceConfigWhitelist';
import isEmbeddedInterfaceConfigWhitelist from './isEmbeddedInterfaceConfigWhitelist';
/**
* The interface config keys to whitelist, the keys that can be overridden.
*
* @private
* @type Array
*/
export default [
'AUDIO_LEVEL_PRIMARY_COLOR',
'AUDIO_LEVEL_SECONDARY_COLOR',
'AUTO_PIN_LATEST_SCREEN_SHARE',
'CLOSE_PAGE_GUEST_HINT',
'CONNECTION_INDICATOR_AUTO_HIDE_ENABLED',
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
'CONNECTION_INDICATOR_DISABLED',
'DEFAULT_BACKGROUND',
'DISABLE_PRESENCE_STATUS',
'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
'DEFAULT_LOCAL_DISPLAY_NAME',
'DEFAULT_REMOTE_DISPLAY_NAME',
'DISABLE_DOMINANT_SPEAKER_INDICATOR',
'DISABLE_FOCUS_INDICATOR',
'DISABLE_PRIVATE_MESSAGES',
'DISABLE_TRANSCRIPTION_SUBTITLES',
'DISABLE_VIDEO_BACKGROUND',
'DISPLAY_WELCOME_PAGE_CONTENT',
'ENABLE_DIAL_OUT',
'FILM_STRIP_MAX_HEIGHT',
'GENERATE_ROOMNAMES_ON_WELCOME_PAGE',
'INDICATOR_FONT_SIZES',
'INITIAL_TOOLBAR_TIMEOUT',
'LANG_DETECTION',
'LOCAL_THUMBNAIL_RATIO',
'MAXIMUM_ZOOMING_COEFFICIENT',
'NATIVE_APP_NAME',
'OPTIMAL_BROWSERS',
'PHONE_NUMBER_REGEX',
'PROVIDER_NAME',
'RECENT_LIST_ENABLED',
'REMOTE_THUMBNAIL_RATIO',
'SETTINGS_SECTIONS',
'SHARING_FEATURES',
'SHOW_CHROME_EXTENSION_BANNER',
'SHOW_POWERED_BY',
'TILE_VIEW_MAX_COLUMNS',
'TOOLBAR_ALWAYS_VISIBLE',
'TOOLBAR_BUTTONS',
'TOOLBAR_TIMEOUT',
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED'
].concat(extraInterfaceConfigWhitelistCopy).concat(isEmbedded() ? isEmbeddedInterfaceConfigWhitelist : []);

View File

@@ -0,0 +1,10 @@
/**
* Additional config whitelist extending the original whitelist applied when Jitsi Meet is embedded
* in another app be that with an iframe or a mobile SDK.
*/
export default [
'customToolbarButtons',
'defaultLogoUrl',
'deploymentUrls',
'liveStreaming'
];

View File

@@ -0,0 +1,6 @@
/**
* Additional interface config whitelist extending the original whitelist applied when Jitsi Meet is embedded
* in another app be that with an iframe or a mobile SDK.
*/
export default [
];

View File

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

View File

@@ -0,0 +1,225 @@
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
import { getFeatureFlag } from '../flags/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { updateSettings } from '../settings/actions';
import { OVERWRITE_CONFIG, SET_CONFIG } from './actionTypes';
import { updateConfig } from './actions';
import { IConfig } from './configType';
/**
* The middleware of the feature {@code base/config}.
*
* @param {Store} store - The redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_CONFIG:
return _setConfig(store, next, action);
case SET_DYNAMIC_BRANDING_DATA:
return _setDynamicBrandingData(store, next, action);
case OVERWRITE_CONFIG:
return _updateSettings(store, next, action);
}
return next(action);
});
/**
* Notifies the feature {@code base/config} that the {@link SET_CONFIG} redux
* action is being {@code dispatch}ed in a specific redux store.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
* @param {Action} action - The redux action which is being {@code dispatch}ed
* in the specified {@code store}.
* @private
* @returns {*} The return value of {@code next(action)}.
*/
function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
// 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: IConfig = {};
if (typeof settings.disableP2P !== 'undefined') {
config.p2p = { enabled: !settings.disableP2P };
}
const resolutionFlag = getFeatureFlag(state, 'resolution');
if (typeof resolutionFlag !== 'undefined') {
config.resolution = resolutionFlag;
}
if (action.config.doNotFlipLocalVideo === true) {
dispatch(updateSettings({
localFlipX: false
}));
}
if (action.config.disableSelfView !== undefined) {
dispatch(updateSettings({
disableSelfView: action.config.disableSelfView
}));
}
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
if (stageFilmstripParticipants !== undefined) {
dispatch(updateSettings({
maxStageParticipants: stageFilmstripParticipants
}));
}
if (initialWidth) {
dispatch(setUserFilmstripWidth(initialWidth));
}
dispatch(updateConfig(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
// not be the global variable which is being modified anymore due to
// 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 = state['features/base/config'];
}
return result;
}
/**
* Updates config based on dynamic branding data.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
* @param {Action} action - The redux action which is being {@code dispatch}ed
* in the specified {@code store}.
* @private
* @returns {*} The return value of {@code next(action)}.
*/
function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
const config: IConfig = {};
const {
customParticipantMenuButtons,
customToolbarButtons,
downloadAppsUrl,
etherpadBase,
liveStreamingDialogUrls = {},
preCallTest = {},
salesforceUrl,
userDocumentationUrl,
peopleSearchUrl,
} = action.value;
const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
if (helpUrl || termsUrl || dataPrivacyUrl) {
config.liveStreaming = {};
if (helpUrl) {
config.liveStreaming.helpLink = helpUrl;
}
if (termsUrl) {
config.liveStreaming.termsLink = termsUrl;
}
if (dataPrivacyUrl) {
config.liveStreaming.dataPrivacyLink = dataPrivacyUrl;
}
}
if (downloadAppsUrl || userDocumentationUrl) {
config.deploymentUrls = {};
if (downloadAppsUrl) {
config.deploymentUrls.downloadAppsUrl = downloadAppsUrl;
}
if (userDocumentationUrl) {
config.deploymentUrls.userDocumentationURL = userDocumentationUrl;
}
}
if (salesforceUrl) {
config.salesforceUrl = salesforceUrl;
}
if (peopleSearchUrl) {
config.peopleSearchUrl = peopleSearchUrl;
}
const { enabled, iceUrl } = preCallTest;
if (typeof enabled === 'boolean') {
config.prejoinConfig = {
preCallTestEnabled: enabled
};
}
if (etherpadBase) {
// eslint-disable-next-line camelcase
config.etherpad_base = etherpadBase;
}
if (iceUrl) {
config.prejoinConfig = config.prejoinConfig || {};
config.prejoinConfig.preCallTestICEUrl = iceUrl;
}
if (customToolbarButtons) {
config.customToolbarButtons = customToolbarButtons;
}
if (customParticipantMenuButtons) {
config.customParticipantMenuButtons = customParticipantMenuButtons;
}
dispatch(updateConfig(config));
return next(action);
}
/**
* Updates settings based on some config values.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
* @param {Action} action - The redux action which is being {@code dispatch}ed
* in the specified {@code store}.
* @private
* @returns {*} The return value of {@code next(action)}.
*/
function _updateSettings({ dispatch }: IStore, next: Function, action: AnyAction) {
const { config: { doNotFlipLocalVideo } } = action;
if (doNotFlipLocalVideo === true) {
dispatch(updateSettings({
localFlipX: false
}));
}
return next(action);
}

View File

@@ -0,0 +1,592 @@
import { merge, union } from 'lodash-es';
import { CONFERENCE_INFO } from '../../conference/components/constants';
import { TOOLBAR_BUTTONS } from '../../toolbox/constants';
import { ToolbarButton } from '../../toolbox/types';
import { CONNECTION_PROPERTIES_UPDATED } from '../connection/actionTypes';
import ReducerRegistry from '../redux/ReducerRegistry';
import { equals } from '../redux/functions';
import {
CONFIG_WILL_LOAD,
LOAD_CONFIG_ERROR,
OVERWRITE_CONFIG,
SET_CONFIG,
UPDATE_CONFIG
} from './actionTypes';
import {
IConfig,
IDeeplinkingConfig,
IDeeplinkingDesktopConfig,
IDeeplinkingMobileConfig
} from './configType';
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
/**
* The initial state of the feature base/config when executing in a
* non-React Native environment. The mandatory configuration to be passed to
* JitsiMeetJS#init(). The app will download config.js from the Jitsi Meet
* deployment and take its values into account but the values below will be
* enforced (because they are essential to the correct execution of the
* application).
*
* @type {Object}
*/
const INITIAL_NON_RN_STATE: IConfig = {
};
/**
* The initial state of the feature base/config when executing in a React Native
* environment. The mandatory configuration to be passed to JitsiMeetJS#init().
* The app will download config.js from the Jitsi Meet deployment and take its
* values into account but the values below will be enforced (because they are
* essential to the correct execution of the application).
*
* @type {Object}
*/
const INITIAL_RN_STATE: IConfig = {
};
/**
* Mapping between old configs controlling the conference info headers visibility and the
* new configs. Needed in order to keep backwards compatibility.
*/
const CONFERENCE_HEADER_MAPPING = {
hideConferenceTimer: [ 'conference-timer' ],
hideConferenceSubject: [ 'subject' ],
hideParticipantsStats: [ 'participants-count' ],
hideRecordingLabel: [ 'recording' ]
};
export interface IConfigState extends IConfig {
analysis?: {
obfuscateRoomName?: boolean;
};
error?: Error;
oldConfig?: {
bosh?: string;
focusUserJid?: string;
hosts: {
domain: string;
muc: string;
};
p2p?: object;
websocket?: string;
};
}
ReducerRegistry.register<IConfigState>('features/base/config', (state = _getInitialState(), action): IConfigState => {
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 CONNECTION_PROPERTIES_UPDATED: {
const { region, shard } = action.properties;
const { deploymentInfo } = state;
if (deploymentInfo?.region === region && deploymentInfo?.shard === shard) {
return state;
}
return {
...state,
deploymentInfo: JSON.parse(JSON.stringify({
...deploymentInfo,
region,
shard
}))
};
}
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);
case OVERWRITE_CONFIG:
return {
...state,
...action.config
};
}
return state;
});
/**
* Gets the initial state of the feature base/config. The mandatory
* configuration to be passed to JitsiMeetJS#init(). The app will download
* config.js from the Jitsi Meet deployment and take its values into account but
* the values below will be enforced (because they are essential to the correct
* execution of the application).
*
* @returns {Object}
*/
function _getInitialState() {
return (
navigator.product === 'ReactNative'
? INITIAL_RN_STATE
: INITIAL_NON_RN_STATE);
}
/**
* Reduces a specific Redux action SET_CONFIG of the feature
* base/lib-jitsi-meet.
*
* @param {IConfig} 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 after the reduction of the specified action.
*/
function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
// eslint-disable-next-line no-param-reassign
config = _translateLegacyConfig(config);
const { audioQuality } = config;
const hdAudioOptions = {};
if (audioQuality?.stereo) {
Object.assign(hdAudioOptions, {
disableAP: true,
enableNoAudioDetection: false,
enableNoisyMicDetection: false,
enableTalkWhileMuted: false
});
}
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
if (alwaysShowResizeBar && disableResizable) {
config.filmstrip = {
...config.filmstrip,
alwaysShowResizeBar: false
};
}
const newState = merge(
{},
config,
hdAudioOptions,
{ error: undefined },
// The config of _getInitialState() is meant to override the config
// downloaded from the Jitsi Meet deployment because the former contains
// values that are mandatory.
_getInitialState()
);
_cleanupConfig(newState);
return equals(state, newState) ? state : newState;
}
/**
* Processes the conferenceInfo object against the defaults.
*
* @param {IConfig} config - The old config.
* @returns {Object} The processed conferenceInfo object.
*/
function _getConferenceInfo(config: IConfig) {
const { conferenceInfo } = config;
if (conferenceInfo) {
return {
alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
};
}
return {
...CONFERENCE_INFO
};
}
/**
* Constructs a new config {@code Object}, if necessary, out of a specific
* interface_config {@code Object} which is in the latest format supported by jitsi-meet.
*
* @param {Object} oldValue - The config {@code Object} which may or may not be
* in the latest form supported by jitsi-meet and from which a new config
* {@code Object} is to be constructed if necessary.
* @returns {Object} A config {@code Object} which is in the latest format
* supported by jitsi-meet.
*/
function _translateInterfaceConfig(oldValue: IConfig) {
const newValue = oldValue;
if (!Array.isArray(oldValue.toolbarButtons)
&& typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
}
if (!oldValue.toolbarConfig) {
oldValue.toolbarConfig = {};
}
newValue.toolbarConfig = oldValue.toolbarConfig || {};
if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') {
newValue.toolbarConfig.alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE;
}
if (typeof oldValue.toolbarConfig.initialTimeout !== 'number'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.INITIAL_TOOLBAR_TIMEOUT === 'number') {
newValue.toolbarConfig.initialTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
}
if (typeof oldValue.toolbarConfig.timeout !== 'number'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.TOOLBAR_TIMEOUT === 'number') {
newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT;
}
if (!oldValue.connectionIndicators
&& typeof interfaceConfig === 'object'
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
newValue.connectionIndicators = {
disabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
autoHide: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
autoHideTimeout: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT
};
}
if (oldValue.disableModeratorIndicator === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
}
if (oldValue.defaultLocalDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_LOCAL_DISPLAY_NAME')) {
newValue.defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
}
if (oldValue.defaultRemoteDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_REMOTE_DISPLAY_NAME')) {
newValue.defaultRemoteDisplayName = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
if (oldValue.defaultLogoUrl === undefined) {
if (typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_LOGO_URL')) {
newValue.defaultLogoUrl = interfaceConfig.DEFAULT_LOGO_URL;
} else {
newValue.defaultLogoUrl = 'images/watermark.svg';
}
}
// if we have `deeplinking` defined, ignore deprecated values, except `disableDeepLinking`.
// Otherwise, compose the config.
if (oldValue.deeplinking && newValue.deeplinking) { // make TS happy
newValue.deeplinking.disabled = oldValue.deeplinking.hasOwnProperty('disabled')
? oldValue.deeplinking.disabled
: Boolean(oldValue.disableDeepLinking);
} else {
const disabled = Boolean(oldValue.disableDeepLinking);
const deeplinking: IDeeplinkingConfig = {
desktop: {} as IDeeplinkingDesktopConfig,
hideLogo: false,
disabled,
android: {} as IDeeplinkingMobileConfig,
ios: {} as IDeeplinkingMobileConfig
};
if (typeof interfaceConfig === 'object') {
if (deeplinking.desktop) {
deeplinking.desktop.appName = interfaceConfig.NATIVE_APP_NAME;
}
deeplinking.hideLogo = Boolean(interfaceConfig.HIDE_DEEP_LINKING_LOGO);
deeplinking.android = {
appName: interfaceConfig.NATIVE_APP_NAME,
appScheme: interfaceConfig.APP_SCHEME,
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_ANDROID,
appPackage: interfaceConfig.ANDROID_APP_PACKAGE,
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID
};
deeplinking.ios = {
appName: interfaceConfig.NATIVE_APP_NAME,
appScheme: interfaceConfig.APP_SCHEME,
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS
};
}
newValue.deeplinking = deeplinking;
}
return newValue;
}
/**
* Constructs a new config {@code Object}, if necessary, out of a specific
* config {@code Object} which is in the latest format supported by jitsi-meet.
* Such a translation from an old config format to a new/the latest config
* format is necessary because the mobile app bundles jitsi-meet and
* lib-jitsi-meet at build time and does not download them at runtime from the
* deployment on which it will join a conference.
*
* @param {Object} oldValue - The config {@code Object} which may or may not be
* in the latest form supported by jitsi-meet and from which a new config
* {@code Object} is to be constructed if necessary.
* @returns {Object} A config {@code Object} which is in the latest format
* supported by jitsi-meet.
*/
function _translateLegacyConfig(oldValue: IConfig) {
const newValue = _translateInterfaceConfig(oldValue);
// Translate deprecated config values to new config values.
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key as keyof IConfig]);
if (filteredConferenceInfo.length) {
newValue.conferenceInfo = _getConferenceInfo(oldValue);
filteredConferenceInfo.forEach(key => {
newValue.conferenceInfo = oldValue.conferenceInfo ?? {};
// hideRecordingLabel does not mean not render it at all, but autoHide it
if (key === 'hideRecordingLabel') {
newValue.conferenceInfo.alwaysVisible
= (newValue.conferenceInfo?.alwaysVisible ?? [])
.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
} else {
newValue.conferenceInfo.alwaysVisible
= (newValue.conferenceInfo.alwaysVisible ?? [])
.filter(c => !CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
newValue.conferenceInfo.autoHide
= (newValue.conferenceInfo.autoHide ?? []).filter(c =>
!CONFERENCE_HEADER_MAPPING[key as keyof typeof CONFERENCE_HEADER_MAPPING].includes(c));
}
});
}
newValue.welcomePage = oldValue.welcomePage || {};
if (oldValue.hasOwnProperty('enableWelcomePage')
&& !newValue.welcomePage.hasOwnProperty('disabled')
) {
newValue.welcomePage.disabled = !oldValue.enableWelcomePage;
}
newValue.disabledSounds = newValue.disabledSounds || [];
if (oldValue.disableJoinLeaveSounds) {
newValue.disabledSounds.unshift('PARTICIPANT_LEFT_SOUND', 'PARTICIPANT_JOINED_SOUND');
}
if (oldValue.disableRecordAudioNotification) {
newValue.disabledSounds.unshift(
'RECORDING_ON_SOUND',
'RECORDING_OFF_SOUND',
'LIVE_STREAMING_ON_SOUND',
'LIVE_STREAMING_OFF_SOUND'
);
}
if (oldValue.disableIncomingMessageSound) {
newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
}
newValue.raisedHands = newValue.raisedHands || {};
if (oldValue.disableRemoveRaisedHandOnFocus) {
newValue.raisedHands.disableRemoveRaisedHandOnFocus = oldValue.disableRemoveRaisedHandOnFocus;
}
if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
newValue.audioQuality = {
opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,
stereo: oldValue.audioQuality?.stereo ?? oldValue.stereo
};
}
newValue.e2ee = newValue.e2ee || {};
if (oldValue.e2eeLabels) {
newValue.e2ee.labels = oldValue.e2eeLabels;
}
newValue.defaultLocalDisplayName
= newValue.defaultLocalDisplayName || 'me';
if (oldValue.hideAddRoomButton) {
newValue.breakoutRooms = {
/* eslint-disable-next-line no-extra-parens */
...(newValue.breakoutRooms || {}),
hideAddRoomButton: oldValue.hideAddRoomButton
};
}
newValue.defaultRemoteDisplayName
= newValue.defaultRemoteDisplayName || 'Fellow Jitster';
newValue.transcription = newValue.transcription || {};
if (oldValue.transcribingEnabled !== undefined) {
newValue.transcription = {
...newValue.transcription,
enabled: oldValue.transcribingEnabled
};
}
if (oldValue.transcribeWithAppLanguage !== undefined) {
newValue.transcription = {
...newValue.transcription,
useAppLanguage: oldValue.transcribeWithAppLanguage
};
}
if (oldValue.preferredTranscribeLanguage !== undefined) {
newValue.transcription = {
...newValue.transcription,
preferredLanguage: oldValue.preferredTranscribeLanguage
};
}
if (oldValue.autoCaptionOnRecord !== undefined) {
newValue.transcription = {
...newValue.transcription,
autoTranscribeOnRecord: oldValue.autoCaptionOnRecord
};
}
newValue.recordingService = newValue.recordingService || {};
if (oldValue.fileRecordingsServiceEnabled !== undefined
&& newValue.recordingService.enabled === undefined) {
newValue.recordingService = {
...newValue.recordingService,
enabled: oldValue.fileRecordingsServiceEnabled
};
}
if (oldValue.fileRecordingsServiceSharingEnabled !== undefined
&& newValue.recordingService.sharingEnabled === undefined) {
newValue.recordingService = {
...newValue.recordingService,
sharingEnabled: oldValue.fileRecordingsServiceSharingEnabled
};
}
newValue.liveStreaming = newValue.liveStreaming || {};
// Migrate config.liveStreamingEnabled
if (oldValue.liveStreamingEnabled !== undefined) {
newValue.liveStreaming = {
...newValue.liveStreaming,
enabled: oldValue.liveStreamingEnabled
};
}
// Migrate interfaceConfig.LIVE_STREAMING_HELP_LINK
if (oldValue.liveStreaming === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('LIVE_STREAMING_HELP_LINK')) {
newValue.liveStreaming = {
...newValue.liveStreaming,
helpLink: interfaceConfig.LIVE_STREAMING_HELP_LINK
};
}
newValue.speakerStats = newValue.speakerStats || {};
if (oldValue.disableSpeakerStatsSearch !== undefined
&& newValue.speakerStats.disableSearch === undefined
) {
newValue.speakerStats = {
...newValue.speakerStats,
disableSearch: oldValue.disableSpeakerStatsSearch
};
}
if (oldValue.speakerStatsOrder !== undefined
&& newValue.speakerStats.order === undefined) {
newValue.speakerStats = {
...newValue.speakerStats,
order: oldValue.speakerStatsOrder
};
}
if (oldValue.autoKnockLobby !== undefined
&& newValue.lobby?.autoKnock === undefined) {
newValue.lobby = {
...newValue.lobby || {},
autoKnock: oldValue.autoKnockLobby
};
}
if (oldValue.enableLobbyChat !== undefined
&& newValue.lobby?.enableChat === undefined) {
newValue.lobby = {
...newValue.lobby || {},
enableChat: oldValue.enableLobbyChat
};
}
if (oldValue.hideLobbyButton !== undefined
&& newValue.securityUi?.hideLobbyButton === undefined) {
newValue.securityUi = {
...newValue.securityUi || {},
hideLobbyButton: oldValue.hideLobbyButton
};
}
// Profile button is not available on mobile
if (navigator.product !== 'ReactNative') {
if (oldValue.disableProfile) {
newValue.toolbarButtons = (newValue.toolbarButtons || TOOLBAR_BUTTONS)
.filter((button: ToolbarButton) => button !== 'profile');
}
}
_setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
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: IConfig, { config }: { config: IConfig; }) {
const newState = merge({}, state, config);
_cleanupConfig(newState);
return equals(state, newState) ? state : newState;
}