mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-08 04:19:11 +00:00
675 lines
22 KiB
TypeScript
675 lines
22 KiB
TypeScript
/*
|
|
Copyright 2022-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 { useMemo } from "react";
|
|
import { useLocation } from "react-router-dom";
|
|
import { logger } from "matrix-js-sdk/lib/logger";
|
|
import {
|
|
type RTCCallIntent,
|
|
type RTCNotificationType,
|
|
} from "matrix-js-sdk/lib/matrixrtc";
|
|
import { pickBy } from "lodash-es";
|
|
|
|
import { Config } from "./config/Config";
|
|
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
|
import { E2eeType } from "./e2ee/e2eeType";
|
|
import { platform } from "./Platform";
|
|
|
|
interface RoomIdentifier {
|
|
roomAlias: string | null;
|
|
roomId: string | null;
|
|
viaServers: string[];
|
|
}
|
|
|
|
export enum UserIntent {
|
|
StartNewCall = "start_call",
|
|
JoinExistingCall = "join_existing",
|
|
StartNewCallDM = "start_call_dm",
|
|
StartNewCallDMVoice = "start_call_dm_voice",
|
|
JoinExistingCallDM = "join_existing_dm",
|
|
JoinExistingCallDMVoice = "join_existing_dm_voice",
|
|
Unknown = "unknown",
|
|
}
|
|
|
|
export enum HeaderStyle {
|
|
None = "none",
|
|
Standard = "standard",
|
|
AppBar = "app_bar",
|
|
}
|
|
|
|
/**
|
|
* The UrlProperties are used to pass required data to the widget.
|
|
* Those are different in different rooms, users, devices. They do not configure the behavior of the
|
|
* widget but provide the required data to the widget.
|
|
*/
|
|
export interface UrlProperties {
|
|
// Widget api related params
|
|
widgetId: string | null;
|
|
parentUrl: string | null;
|
|
/**
|
|
* Anything about what room we're pointed to should be from useRoomIdentifier which
|
|
* parses the path and resolves alias with respect to the default server name, however
|
|
* roomId is an exception as we need the room ID in embedded (matroyska) mode, and not
|
|
* the room alias (or even the via params because we are not trying to join it). This
|
|
* is also not validated, where it is in useRoomIdentifier().
|
|
*/
|
|
roomId: string | null;
|
|
/**
|
|
* The user's ID (only used in matryoshka mode).
|
|
*/
|
|
userId: string | null;
|
|
|
|
/**
|
|
* The display name to use for auto-registration.
|
|
*/
|
|
displayName: string | null;
|
|
/**
|
|
* The device's ID (only used in matryoshka mode).
|
|
*/
|
|
deviceId: string | null;
|
|
/**
|
|
* The base URL of the homeserver to use for media lookups in matryoshka mode.
|
|
*/
|
|
baseUrl: string | null;
|
|
/**
|
|
* The BCP 47 code of the language the app should use.
|
|
*/
|
|
lang: string | null;
|
|
/**
|
|
* The fonts which the interface should use, if not empty.
|
|
*/
|
|
fonts: string[];
|
|
/**
|
|
* The factor by which to scale the interface's font size.
|
|
*/
|
|
fontScale: number | null;
|
|
/**
|
|
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
|
|
*/
|
|
posthogUserId: string | null;
|
|
/**
|
|
* The Posthog API host. This is only used in the embedded package of Element Call.
|
|
*/
|
|
posthogApiHost: string | null;
|
|
/**
|
|
* The Posthog API key. This is only used in the embedded package of Element Call.
|
|
*/
|
|
posthogApiKey: string | null;
|
|
/**
|
|
* Whether to use end-to-end encryption.
|
|
*/
|
|
e2eEnabled: boolean;
|
|
/**
|
|
* E2EE password
|
|
*/
|
|
password: string | null;
|
|
/** This defines the homeserver that is going to be used when joining a room.
|
|
* It has to be set to a non default value for links to rooms
|
|
* that are not on the default homeserver,
|
|
* that is in use for the current user.
|
|
*/
|
|
viaServers: string | null;
|
|
|
|
/**
|
|
* This defines the homeserver that is going to be used when registering
|
|
* a new (guest) user.
|
|
* This can be user to configure a non default guest user server when
|
|
* creating a spa link.
|
|
*/
|
|
homeserver: string | null;
|
|
|
|
/**
|
|
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
|
*/
|
|
rageshakeSubmitUrl: string | null;
|
|
|
|
/**
|
|
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
|
*/
|
|
sentryDsn: string | null;
|
|
|
|
/**
|
|
* The Sentry environment. This is only used in the embedded package of Element Call.
|
|
*/
|
|
sentryEnvironment: string | null;
|
|
/**
|
|
* The theme to use for element call.
|
|
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
|
*/
|
|
theme: string | null;
|
|
}
|
|
|
|
/**
|
|
* The configuration for the app, which can be set via URL parameters.
|
|
* Those property are different to the UrlProperties, since they are all optional
|
|
* and configure the behavior of the app. Their value is the same if EC is used in
|
|
* the same context but with different accounts/users.
|
|
*
|
|
* Their defaults can be controlled by the `intent` property.
|
|
*/
|
|
export interface UrlConfiguration {
|
|
/**
|
|
* Whether the app should keep the user confined to the current call/room.
|
|
*/
|
|
confineToRoom: boolean;
|
|
/**
|
|
* Whether upon entering a room, the user should be prompted to launch the
|
|
* native mobile app. (Affects only Android and iOS.)
|
|
*
|
|
* The app prompt must also be enabled in the config for this to take effect.
|
|
*/
|
|
appPrompt: boolean;
|
|
/**
|
|
* Whether the app should pause before joining the call until it sees an
|
|
* io.element.join widget action, allowing it to be preloaded.
|
|
*/
|
|
preload: boolean;
|
|
/**
|
|
* The style of headers to show. "standard" is the default arrangement, "none"
|
|
* hides the header entirely, and "app_bar" produces a header with a back
|
|
* button like you might see in mobile apps. The callback for the back button
|
|
* is window.controls.onBackButtonPressed.
|
|
*/
|
|
header: HeaderStyle;
|
|
/**
|
|
* Whether the controls should be shown. For screen recording no controls can be desired.
|
|
*/
|
|
showControls: boolean;
|
|
/**
|
|
* Whether to hide the screen-sharing button.
|
|
*/
|
|
hideScreensharing: boolean;
|
|
|
|
/**
|
|
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
|
* user's homeserver doesn't provide any.
|
|
*/
|
|
allowIceFallback: boolean;
|
|
|
|
/**
|
|
* Whether the app should use per participant keys for E2EE.
|
|
*/
|
|
perParticipantE2EE: boolean;
|
|
/**
|
|
* Whether the global JS controls for audio output devices should be enabled,
|
|
* allowing the list of output devices to be controlled by the app hosting
|
|
* Element Call.
|
|
*/
|
|
controlledAudioDevices: boolean;
|
|
/**
|
|
* Setting this flag skips the lobby and brings you in the call directly.
|
|
* In the widget this can be combined with preload to pass the device settings
|
|
* with the join widget action.
|
|
*/
|
|
skipLobby: boolean;
|
|
/**
|
|
* Setting this flag makes element call show the lobby after leaving a call.
|
|
* This is useful for video rooms.
|
|
*/
|
|
returnToLobby: boolean;
|
|
/**
|
|
* Whether and what type of notification EC should send, when the user joins the call.
|
|
*/
|
|
sendNotificationType?: RTCNotificationType;
|
|
/**
|
|
* Whether the app should automatically leave the call when there
|
|
* is no one left in the call.
|
|
* This is one part to make the call matrixRTC session behave like a telephone call.
|
|
*/
|
|
autoLeaveWhenOthersLeft: boolean;
|
|
|
|
/**
|
|
* If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up).
|
|
* This is a no-op if not combined with sendNotificationType.
|
|
*
|
|
* This entails:
|
|
* - show ui that it is awaiting an answer
|
|
* - play a sound that indicates that it is awaiting an answer
|
|
* - auto-dismiss the call widget once the notification lifetime expires on the receivers side.
|
|
*/
|
|
waitForCallPickup: boolean;
|
|
|
|
/**
|
|
* Whether to enable echo cancellation for audio capture.
|
|
* Defaults to true.
|
|
*/
|
|
echoCancellation?: boolean;
|
|
/**
|
|
* Whether to enable noise suppression for audio capture.
|
|
* Defaults to true.
|
|
*/
|
|
noiseSuppression?: boolean;
|
|
|
|
callIntent?: RTCCallIntent;
|
|
}
|
|
interface IntentAndPlatformDerivedConfiguration {
|
|
defaultAudioEnabled?: boolean;
|
|
defaultVideoEnabled?: boolean;
|
|
}
|
|
interface IntentAndPlatformDerivedConfiguration {
|
|
defaultAudioEnabled?: boolean;
|
|
defaultVideoEnabled?: boolean;
|
|
}
|
|
|
|
// If you need to add a new flag to this interface, prefer a name that describes
|
|
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
|
// the situations that call for this behavior ('isEmbedded'). This makes it
|
|
// clearer what each flag means, and helps us avoid coupling Element Call's
|
|
// behavior to the needs of specific consumers.
|
|
export interface UrlParams
|
|
extends UrlProperties,
|
|
UrlConfiguration,
|
|
IntentAndPlatformDerivedConfiguration {}
|
|
|
|
// This is here as a stopgap, but what would be far nicer is a function that
|
|
// takes a UrlParams and returns a query string. That would enable us to
|
|
// consolidate all the data about URL parameters and their meanings to this one
|
|
// file.
|
|
export function editFragmentQuery(
|
|
hash: string,
|
|
edit: (params: URLSearchParams) => URLSearchParams,
|
|
): string {
|
|
const fragmentQueryStart = hash.indexOf("?");
|
|
const fragmentParams = edit(
|
|
new URLSearchParams(
|
|
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
|
|
),
|
|
);
|
|
return `${hash.substring(
|
|
0,
|
|
fragmentQueryStart,
|
|
)}?${fragmentParams.toString()}`;
|
|
}
|
|
|
|
class ParamParser {
|
|
private fragmentParams: URLSearchParams;
|
|
private queryParams: URLSearchParams;
|
|
|
|
public constructor(search: string, hash: string) {
|
|
this.queryParams = new URLSearchParams(search);
|
|
|
|
const fragmentQueryStart = hash.indexOf("?");
|
|
this.fragmentParams = new URLSearchParams(
|
|
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
|
|
);
|
|
}
|
|
|
|
// Normally, URL params should be encoded in the fragment so as to avoid
|
|
// leaking them to the server. However, we also check the normal query
|
|
// string for backwards compatibility with versions that only used that.
|
|
public getParam(name: string): string | null {
|
|
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
|
}
|
|
|
|
public getEnumParam<T extends string>(
|
|
name: string,
|
|
type: { [s: string]: T } | ArrayLike<T>,
|
|
): T | undefined {
|
|
const value = this.getParam(name);
|
|
if (value !== null && Object.values(type).includes(value as T)) {
|
|
return value as T;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public getAllParams(name: string): string[] {
|
|
return [
|
|
...this.fragmentParams.getAll(name),
|
|
...this.queryParams.getAll(name),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns true if the flag exists and is not "false".
|
|
*/
|
|
public getFlagParam(name: string, defaultValue = false): boolean {
|
|
const param = this.getParam(name);
|
|
return param === null ? defaultValue : param !== "false";
|
|
}
|
|
/**
|
|
* Returns the value of the flag if it exists, or undefined if it does not.
|
|
*/
|
|
public getFlag(name: string): boolean | undefined {
|
|
const param = this.getParam(name);
|
|
return param !== null ? param !== "false" : undefined;
|
|
}
|
|
}
|
|
|
|
let urlParamCache: {
|
|
search?: string;
|
|
hash?: string;
|
|
params?: UrlParams;
|
|
} = {};
|
|
|
|
/**
|
|
* Gets the url params and loads them from a cache if already computed.
|
|
* @param search The URL search string
|
|
* @param hash The URL hash
|
|
* @returns The app parameters encoded in the URL
|
|
*/
|
|
export const getUrlParams = (
|
|
search = window.location.search,
|
|
hash = window.location.hash,
|
|
): UrlParams => {
|
|
if (
|
|
urlParamCache.search === search &&
|
|
urlParamCache.hash === hash &&
|
|
urlParamCache.params
|
|
) {
|
|
return urlParamCache.params;
|
|
}
|
|
const params = computeUrlParams(search, hash);
|
|
urlParamCache = { search, hash, params };
|
|
|
|
return params;
|
|
};
|
|
|
|
/**
|
|
* Gets the app parameters for the current URL.
|
|
* @param search The URL search string
|
|
* @param hash The URL hash
|
|
* @returns The app parameters encoded in the URL
|
|
*/
|
|
export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
|
const parser = new ParamParser(search, hash);
|
|
|
|
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
|
|
|
const widgetId = parser.getParam("widgetId");
|
|
const parentUrl = parser.getParam("parentUrl");
|
|
const isWidget = !!widgetId && !!parentUrl;
|
|
|
|
/**
|
|
* The user's intent with respect to the call.
|
|
* e.g. if they clicked a Start Call button, this would be `start_call`.
|
|
* If it was a Join Call button, it would be `join_existing`.
|
|
* This is a platform specific default set of parameters, that allows to minize the configuration
|
|
* needed to start a call. And empowers the EC codebase to control the platform/intent behavior in
|
|
* a central place.
|
|
*
|
|
* In short: either provide url query parameters of UrlConfiguration or set the intent
|
|
* (or the global defaults will be used).
|
|
*/
|
|
const intent = !isWidget
|
|
? UserIntent.Unknown
|
|
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
|
// Here we only use constants and `platform` to determine the intent preset.
|
|
let intentPreset: UrlConfiguration = {
|
|
confineToRoom: true,
|
|
appPrompt: false,
|
|
preload: false,
|
|
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
|
showControls: true,
|
|
hideScreensharing: false,
|
|
allowIceFallback: true,
|
|
perParticipantE2EE: true,
|
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
|
skipLobby: true,
|
|
returnToLobby: false,
|
|
sendNotificationType: "notification",
|
|
autoLeaveWhenOthersLeft: false,
|
|
waitForCallPickup: false,
|
|
};
|
|
switch (intent) {
|
|
case UserIntent.StartNewCall:
|
|
intentPreset.skipLobby = false;
|
|
intentPreset.callIntent = "video";
|
|
break;
|
|
case UserIntent.JoinExistingCall:
|
|
// On desktop this will be overridden based on which button was used to join the call
|
|
intentPreset.skipLobby = false;
|
|
intentPreset.callIntent = "video";
|
|
break;
|
|
case UserIntent.StartNewCallDMVoice:
|
|
intentPreset.callIntent = "audio";
|
|
// Fall through
|
|
case UserIntent.StartNewCallDM:
|
|
intentPreset.skipLobby = true;
|
|
intentPreset.sendNotificationType = "ring";
|
|
intentPreset.autoLeaveWhenOthersLeft = true;
|
|
intentPreset.waitForCallPickup = true;
|
|
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
|
break;
|
|
case UserIntent.JoinExistingCallDMVoice:
|
|
intentPreset.callIntent = "audio";
|
|
// Fall through
|
|
case UserIntent.JoinExistingCallDM:
|
|
// On desktop this will be overridden based on which button was used to join the call
|
|
intentPreset.skipLobby = true;
|
|
intentPreset.autoLeaveWhenOthersLeft = true;
|
|
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
|
break;
|
|
// Non widget usecase defaults
|
|
default:
|
|
intentPreset = {
|
|
confineToRoom: false,
|
|
appPrompt: true,
|
|
preload: false,
|
|
header: HeaderStyle.Standard,
|
|
showControls: true,
|
|
hideScreensharing: false,
|
|
allowIceFallback: false,
|
|
perParticipantE2EE: false,
|
|
controlledAudioDevices: false,
|
|
skipLobby: false,
|
|
returnToLobby: false,
|
|
sendNotificationType: undefined,
|
|
autoLeaveWhenOthersLeft: false,
|
|
waitForCallPickup: false,
|
|
};
|
|
}
|
|
|
|
const intentAndPlatformDerivedConfiguration: IntentAndPlatformDerivedConfiguration =
|
|
{};
|
|
// Desktop also includes web. Its anything that is not mobile.
|
|
const desktopMobile = platform === "desktop" ? "desktop" : "mobile";
|
|
switch (desktopMobile) {
|
|
case "desktop":
|
|
case "mobile":
|
|
switch (intent) {
|
|
case UserIntent.StartNewCall:
|
|
case UserIntent.JoinExistingCall:
|
|
case UserIntent.StartNewCallDM:
|
|
case UserIntent.JoinExistingCallDM:
|
|
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
|
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = true;
|
|
break;
|
|
case UserIntent.StartNewCallDMVoice:
|
|
case UserIntent.JoinExistingCallDMVoice:
|
|
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
|
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const properties: UrlProperties = {
|
|
widgetId,
|
|
parentUrl,
|
|
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
|
|
// what would we do if it were invalid? If the widget API says that's what
|
|
// the room ID is, then that's what it is.
|
|
roomId: parser.getParam("roomId"),
|
|
password: parser.getParam("password"),
|
|
userId: isWidget ? parser.getParam("userId") : null,
|
|
displayName: parser.getParam("displayName"),
|
|
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
|
baseUrl: isWidget ? parser.getParam("baseUrl") : null,
|
|
lang: parser.getParam("lang"),
|
|
fonts: parser.getAllParams("font"),
|
|
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
|
theme: parser.getParam("theme"),
|
|
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
|
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
|
posthogApiHost: parser.getParam("posthogApiHost"),
|
|
posthogApiKey: parser.getParam("posthogApiKey"),
|
|
posthogUserId:
|
|
parser.getParam("posthogUserId") ?? parser.getParam("analyticsID"),
|
|
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
|
sentryDsn: parser.getParam("sentryDsn"),
|
|
sentryEnvironment: parser.getParam("sentryEnvironment"),
|
|
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
|
};
|
|
|
|
const configuration: Partial<UrlConfiguration> = {
|
|
confineToRoom: parser.getFlag("confineToRoom"),
|
|
appPrompt: parser.getFlag("appPrompt"),
|
|
preload: isWidget ? parser.getFlag("preload") : undefined,
|
|
// Check hideHeader for backwards compatibility. If header is set, hideHeader
|
|
// is ignored.
|
|
header: parser.getEnumParam("header", HeaderStyle),
|
|
showControls: parser.getFlag("showControls"),
|
|
hideScreensharing: parser.getFlag("hideScreensharing"),
|
|
allowIceFallback: parser.getFlag("allowIceFallback"),
|
|
perParticipantE2EE: parser.getFlag("perParticipantE2EE"),
|
|
controlledAudioDevices: parser.getFlag("controlledAudioDevices"),
|
|
skipLobby: isWidget ? parser.getFlag("skipLobby") : false,
|
|
// In SPA mode the user should always exit to the home screen when hanging
|
|
// up, rather than being sent back to the lobby
|
|
returnToLobby: isWidget ? parser.getFlag("returnToLobby") : false,
|
|
sendNotificationType: parser.getEnumParam("sendNotificationType", [
|
|
"ring",
|
|
"notification",
|
|
]),
|
|
waitForCallPickup: parser.getFlag("waitForCallPickup"),
|
|
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
|
noiseSuppression: parser.getFlagParam("noiseSuppression", true),
|
|
echoCancellation: parser.getFlagParam("echoCancellation", true),
|
|
};
|
|
|
|
// Log the final configuration for debugging purposes.
|
|
// This will only log when the cache is not yet set.
|
|
logger.info(
|
|
"UrlParams: final set of url params\n",
|
|
"intent:",
|
|
intent,
|
|
"\nproperties:",
|
|
properties,
|
|
"configuration:",
|
|
configuration,
|
|
"intentAndPlatformDerivedConfiguration:",
|
|
intentAndPlatformDerivedConfiguration,
|
|
);
|
|
|
|
return {
|
|
...properties,
|
|
...intentPreset,
|
|
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
|
...intentAndPlatformDerivedConfiguration,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Hook to simplify use of getUrlParams.
|
|
* @returns The app parameters for the current URL
|
|
*/
|
|
export const useUrlParams = (): UrlParams => {
|
|
const { search, hash } = useLocation();
|
|
return useMemo(() => getUrlParams(search, hash), [search, hash]);
|
|
};
|
|
|
|
export function getRoomIdentifierFromUrl(
|
|
pathname: string,
|
|
search: string,
|
|
hash: string,
|
|
): RoomIdentifier {
|
|
let roomAlias: string | null = null;
|
|
pathname = pathname.substring(1); // Strip the "/"
|
|
const pathComponents = pathname.split("/");
|
|
const pathHasRoom = pathComponents[0] == "room";
|
|
const hasRoomAlias = pathComponents.length > 1;
|
|
|
|
// What type is our url: roomAlias in hash, room alias as the search path, roomAlias after /room/
|
|
if (hash === "" || hash.startsWith("#?")) {
|
|
if (hasRoomAlias && pathHasRoom) {
|
|
roomAlias = pathComponents[1];
|
|
}
|
|
if (!pathHasRoom) {
|
|
roomAlias = pathComponents[0];
|
|
}
|
|
} else {
|
|
roomAlias = hash;
|
|
}
|
|
|
|
// Delete "?" and what comes afterwards
|
|
roomAlias = roomAlias?.split("?")[0] ?? null;
|
|
|
|
if (roomAlias) {
|
|
// Make roomAlias is null, if it only is a "#"
|
|
if (roomAlias.length <= 1) {
|
|
roomAlias = null;
|
|
} else {
|
|
// Add "#", if not present
|
|
if (!roomAlias.startsWith("#")) {
|
|
roomAlias = `#${roomAlias}`;
|
|
}
|
|
// Add server part, if not present
|
|
if (!roomAlias.includes(":")) {
|
|
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const parser = new ParamParser(search, hash);
|
|
|
|
// Make sure roomId is valid
|
|
let roomId: string | null = parser.getParam("roomId");
|
|
if (roomId !== null) {
|
|
// Replace any non-printable characters that another client may have inserted.
|
|
// For instance on iOS, some copied links end up with zero width characters on the end which get encoded into the URL.
|
|
// This isn't valid for a roomId, so we can freely strip the content.
|
|
roomId = roomId.replaceAll(/^[^ -~]+|[^ -~]+$/g, "");
|
|
if (!roomId.startsWith("!")) {
|
|
roomId = null;
|
|
} else if (!roomId.includes("")) {
|
|
roomId = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
roomAlias,
|
|
roomId,
|
|
viaServers: parser.getAllParams("viaServers"),
|
|
};
|
|
}
|
|
|
|
export const useRoomIdentifier = (): RoomIdentifier => {
|
|
const { pathname, search, hash } = useLocation();
|
|
return useMemo(
|
|
() => getRoomIdentifierFromUrl(pathname, search, hash),
|
|
[pathname, search, hash],
|
|
);
|
|
};
|
|
|
|
export function generateUrlSearchParams(
|
|
roomId: string,
|
|
encryptionSystem: EncryptionSystem,
|
|
viaServers?: string[],
|
|
): URLSearchParams {
|
|
const params = new URLSearchParams();
|
|
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
|
|
// it in case it came from another client that generated a non url-safe one
|
|
switch (encryptionSystem?.kind) {
|
|
case E2eeType.SHARED_KEY: {
|
|
const encodedPassword = encodeURIComponent(encryptionSystem.secret);
|
|
if (encodedPassword !== encryptionSystem.secret) {
|
|
logger.info(
|
|
"Encoded call password used non URL-safe chars: buggy client?",
|
|
);
|
|
}
|
|
params.set("password", encodedPassword);
|
|
break;
|
|
}
|
|
case E2eeType.PER_PARTICIPANT:
|
|
params.set("perParticipantE2EE", "true");
|
|
break;
|
|
}
|
|
params.set("roomId", roomId);
|
|
viaServers?.forEach((s) => params.set("viaServers", s));
|
|
|
|
return params;
|
|
}
|