mirror of
https://github.com/element-hq/element-call.git
synced 2026-10-06 23:17:18 +00:00
"Embedded" already means something here: the embedded package is the widget build. Where a comment meant Element Call running as a React component inside a host application, it now says so, and the params it starts from are `componentProperties`. "Host" stays the word for the application on the other side, whether that is a widget container or an application rendering the component. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
/*
|
|
Copyright 2021-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { merge } from "lodash-es";
|
|
import { logger } from "matrix-js-sdk/lib/logger";
|
|
|
|
import { getUrlParams } from "../UrlParams";
|
|
import {
|
|
DEFAULT_CONFIG,
|
|
type ConfigOptions,
|
|
type ResolvedConfigOptions,
|
|
} from "./ConfigOptions";
|
|
import { isFailure } from "../utils/fetch";
|
|
import { MatrixRTCMode } from "./ConfigOptions";
|
|
|
|
const VALID_MATRIX_RTC_MODES: ReadonlySet<string> = new Set(
|
|
Object.values(MatrixRTCMode),
|
|
);
|
|
|
|
export class Config {
|
|
private static internalInstance: Config | undefined;
|
|
|
|
public static get(): ResolvedConfigOptions {
|
|
if (!this.internalInstance?.config)
|
|
throw new Error("Config instance read before config got initialized");
|
|
return this.internalInstance.config;
|
|
}
|
|
|
|
/**
|
|
* Initializes the config by fetching `config.json`, locating it relative to
|
|
* the current page.
|
|
*
|
|
* Does nothing if the config has already been initialized, including by
|
|
* {@link Config.initWith}, so that the regular startup path can run unchanged
|
|
* when a component host has already supplied the config.
|
|
*/
|
|
public static async init(): Promise<void> {
|
|
if (!Config.internalInstance?.initPromise) {
|
|
const internalInstance = new Config();
|
|
Config.internalInstance = internalInstance;
|
|
|
|
let fetchTarget: string;
|
|
|
|
if (
|
|
window.location.pathname.endsWith("/room/") ||
|
|
window.location.pathname.endsWith("/room")
|
|
) {
|
|
// it looks like we are running in standalone mode so use the config at the root
|
|
fetchTarget = new URL("/config.json", window.location.href).href;
|
|
} else {
|
|
// otherwise we are probably running as a widget so use the config in the same directory
|
|
fetchTarget = "config.json";
|
|
}
|
|
|
|
Config.internalInstance.initPromise = downloadConfig(fetchTarget).then(
|
|
(config) => {
|
|
internalInstance.config = resolveConfig(config);
|
|
},
|
|
);
|
|
}
|
|
return Config.internalInstance.initPromise;
|
|
}
|
|
|
|
/**
|
|
* Initializes the config from an object supplied by the application hosting
|
|
* the component, instead of fetching `config.json`.
|
|
*
|
|
* {@link Config.init} derives the location of `config.json` from
|
|
* `window.location`, which only makes sense while Element Call owns the page.
|
|
* As a component, the host owns the configuration and passes it in here.
|
|
*
|
|
* The config goes through the same validation and defaulting as a fetched
|
|
* one, so that a supplied config behaves identically to a fetched one.
|
|
*
|
|
* Replaces any config initialized earlier.
|
|
*/
|
|
public static initWith(config: ConfigOptions): void {
|
|
const internalInstance = new Config();
|
|
internalInstance.config = resolveConfig(config);
|
|
// Mark initialization as already done, so that a later init() resolves
|
|
// immediately rather than fetching config.json over the top of this.
|
|
internalInstance.initPromise = Promise.resolve();
|
|
Config.internalInstance = internalInstance;
|
|
}
|
|
|
|
/**
|
|
* This is a alternative initializer that does not load anything
|
|
* from a hosted config file but instead just initializes the config using the
|
|
* default config.
|
|
*
|
|
* It is supposed to only be used in tests. (It is executed in `vite.setup.js`)
|
|
*/
|
|
public static initDefault(): void {
|
|
Config.initWith({});
|
|
}
|
|
|
|
// Convenience accessors
|
|
public static defaultHomeserverUrl(): string | undefined {
|
|
return (
|
|
getUrlParams().homeserver ??
|
|
Config.get().default_server_config?.["m.homeserver"].base_url
|
|
);
|
|
}
|
|
|
|
public static defaultServerName(): string | undefined {
|
|
const homeserver = getUrlParams().homeserver;
|
|
if (homeserver) {
|
|
const url = new URL(homeserver);
|
|
return url.hostname;
|
|
}
|
|
return Config.get().default_server_config?.["m.homeserver"].server_name;
|
|
}
|
|
|
|
public config?: ResolvedConfigOptions;
|
|
private initPromise?: Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Applies validation and the built-in defaults to a config, however it was
|
|
* obtained. Deep-merges onto a fresh object so that the result never shares
|
|
* nested state with {@link DEFAULT_CONFIG}.
|
|
*/
|
|
function resolveConfig(config: ConfigOptions): ResolvedConfigOptions {
|
|
return merge({}, DEFAULT_CONFIG, validateConfig(config));
|
|
}
|
|
|
|
export function validateConfig(config: ConfigOptions): ConfigOptions {
|
|
const mode = config.matrix_rtc_mode;
|
|
if (mode !== undefined && !VALID_MATRIX_RTC_MODES.has(mode)) {
|
|
logger.warn(
|
|
`Ignoring invalid matrix_rtc_mode in config.json: ${String(mode)}`,
|
|
);
|
|
delete config.matrix_rtc_mode;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
async function downloadConfig(fetchTarget: string): Promise<ConfigOptions> {
|
|
const response = await fetch(fetchTarget);
|
|
|
|
if (isFailure(response)) {
|
|
// Lack of a config isn't an error, we should just use the defaults.
|
|
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
|
|
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
|
|
return DEFAULT_CONFIG;
|
|
}
|
|
|
|
return response.json();
|
|
}
|