From 762e5937c5281887073e3ff571680cd04569a16d Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 30 Jun 2025 18:28:23 +0200 Subject: [PATCH 01/12] refactor UrlParams to use a preset intent system --- src/UrlParams.ts | 309 ++++++++++++++++++++++++++++++----------------- 1 file changed, 196 insertions(+), 113 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index b3940acb7..9acddd1e7 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -9,10 +9,12 @@ import { useMemo } from "react"; import { useLocation } from "react-router-dom"; import { logger } from "matrix-js-sdk/lib/logger"; import { 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; @@ -32,12 +34,12 @@ export enum HeaderStyle { AppBar = "app_bar", } -// 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 { +/** + * 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; @@ -49,45 +51,11 @@ export interface UrlParams { * is also not validated, where it is in useRoomIdentifier(). */ roomId: string | null; - /** - * 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 to use end-to-end encryption. - */ - e2eEnabled: boolean; /** * The user's ID (only used in matryoshka mode). */ userId: string | null; + /** * The display name to use for auto-registration. */ @@ -125,46 +93,20 @@ export interface UrlParams { */ posthogApiKey: string | null; /** - * Whether the app is allowed to use fallback STUN servers for ICE in case the - * user's homeserver doesn't provide any. + * Whether to use end-to-end encryption. */ - allowIceFallback: boolean; + e2eEnabled: boolean; /** * E2EE password */ password: string | null; - /** - * 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; - /** - * The theme to use for element call. - * can be "light", "dark", "light-high-contrast" or "dark-high-contrast". - */ - theme: 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. @@ -173,13 +115,6 @@ export interface UrlParams { */ homeserver: string | null; - /** - * 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`. - */ - intent: string | null; - /** * The rageshake submit URL. This is only used in the embedded package of Element Call. */ @@ -194,12 +129,94 @@ export interface UrlParams { * 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. It can be set via URL parameters. + * Those parameters are different to the UrlProperties, since they are all optional + * and configure the behavior of the app. There value is the same if EC is used in + * the same context but different accoutns/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; } +// 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 {} + // 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 @@ -251,6 +268,10 @@ class ParamParser { const param = this.getParam(name); return param === null ? defaultValue : param !== "false"; } + public getFlag(name: string): boolean | undefined { + const param = this.getParam(name); + return param !== null ? param !== "false" : undefined; + } } /** @@ -267,18 +288,70 @@ export const getUrlParams = ( const fontScale = parseFloat(parser.getParam("fontScale") ?? ""); - let intent = parser.getParam("intent"); + /** + * 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). + */ + let intent = parser.getParam("intent") as UserIntent | null; + if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) { intent = UserIntent.Unknown; } - - // Check hideHeader for backwards compatibility. If header is set, hideHeader - // is ignored. - const header = - parser.getParam("header") ?? - (parser.getFlagParam("hideHeader") - ? HeaderStyle.None - : HeaderStyle.Standard); + // Here we only use constants and `platform` to determine the intent preset. + let intentPreset: UrlConfiguration; + switch (intent) { + case UserIntent.StartNewCall: + intentPreset = { + confineToRoom: true, + appPrompt: false, + preload: true, + header: HeaderStyle.None, + showControls: true, + hideScreensharing: false, + allowIceFallback: true, + perParticipantE2EE: true, + controlledAudioDevices: platform === "desktop" ? false : true, + skipLobby: true, + returnToLobby: false, + }; + break; + case UserIntent.JoinExistingCall: + intentPreset = { + confineToRoom: true, + appPrompt: false, + preload: true, + header: HeaderStyle.None, + showControls: true, + hideScreensharing: false, + allowIceFallback: true, + perParticipantE2EE: true, + controlledAudioDevices: platform === "desktop" ? false : true, + skipLobby: false, + returnToLobby: false, + }; + break; + 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, + }; + } const sendNotificationType = ["ring", "notification"].includes( parser.getParam("sendNotificationType") ?? "", @@ -288,25 +361,14 @@ export const getUrlParams = ( const widgetId = parser.getParam("widgetId"); const parentUrl = parser.getParam("parentUrl"); const isWidget = !!widgetId && !!parentUrl; - - return { + 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"), - // This flag has 'embed' as an alias for historical reasons - confineToRoom: - parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"), - appPrompt: parser.getFlagParam("appPrompt", true), - preload: isWidget ? parser.getFlagParam("preload") : false, - header: header as HeaderStyle, - showControls: parser.getFlagParam("showControls", true), - hideScreensharing: parser.getFlagParam("hideScreensharing"), - e2eEnabled: parser.getFlagParam("enableE2EE", true), userId: isWidget ? parser.getParam("userId") : null, displayName: parser.getParam("displayName"), deviceId: isWidget ? parser.getParam("deviceId") : null, @@ -314,24 +376,9 @@ export const getUrlParams = ( lang: parser.getParam("lang"), fonts: parser.getAllParams("font"), fontScale: Number.isNaN(fontScale) ? null : fontScale, - allowIceFallback: parser.getFlagParam("allowIceFallback"), - perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"), - controlledAudioDevices: parser.getFlagParam( - "controlledAudioDevices", - // the deprecated property name - parser.getFlagParam("controlledMediaDevices"), - ), - skipLobby: parser.getFlagParam( - "skipLobby", - isWidget && intent === UserIntent.StartNewCall, - ), - // 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.getFlagParam("returnToLobby") : false, theme: parser.getParam("theme"), viaServers: !isWidget ? parser.getParam("viaServers") : null, homeserver: !isWidget ? parser.getParam("homeserver") : null, - intent, posthogApiHost: parser.getParam("posthogApiHost"), posthogApiKey: parser.getParam("posthogApiKey"), posthogUserId: @@ -339,8 +386,44 @@ export const getUrlParams = ( rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"), sentryDsn: parser.getParam("sentryDsn"), sentryEnvironment: parser.getParam("sentryEnvironment"), + e2eEnabled: parser.getFlagParam("enableE2EE", true), + }; + + const configuration: Partial = { + // This flag has 'embed' as an alias for historical reasons + confineToRoom: parser.getFlag("confineToRoom") ?? parser.getFlag("embed"), + appPrompt: parser.getFlag("appPrompt"), + preload: isWidget ? parser.getFlag("preload") : undefined, + // Check hideHeader for backwards compatibility. If header is set, hideHeader + // is ignored. + header: + (parser.getParam("header") as HeaderStyle) ?? + (parser.getFlag("hideHeader") !== undefined + ? parser.getFlagParam("hideHeader") + ? HeaderStyle.None + : HeaderStyle.Standard + : undefined), + showControls: parser.getFlag("showControls"), + hideScreensharing: parser.getFlag("hideScreensharing"), + allowIceFallback: parser.getFlag("allowIceFallback"), + perParticipantE2EE: parser.getFlag("perParticipantE2EE"), + controlledAudioDevices: + parser.getFlag( + "controlledAudioDevices", + // the deprecated property name + ) ?? parser.getFlag("controlledMediaDevices"), + 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, }; + + return { + ...properties, + ...intentPreset, + ...pickBy(configuration, (v) => v !== undefined), + }; }; /** From 980502711de14dfa4689e75efde575aeed7ab327 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 30 Jun 2025 18:47:48 +0200 Subject: [PATCH 02/12] change defaults for intend headers --- src/UrlParams.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 9acddd1e7..e2ceee144 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -312,7 +312,7 @@ export const getUrlParams = ( confineToRoom: true, appPrompt: false, preload: true, - header: HeaderStyle.None, + header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, showControls: true, hideScreensharing: false, allowIceFallback: true, @@ -327,7 +327,7 @@ export const getUrlParams = ( confineToRoom: true, appPrompt: false, preload: true, - header: HeaderStyle.None, + header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, showControls: true, hideScreensharing: false, allowIceFallback: true, From 16b2420bbf6c0d1c28cc4c28779d1a76aea2bfb5 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 30 Jun 2025 18:48:30 +0200 Subject: [PATCH 03/12] add: getEnumParam to ParamParser --- src/UrlParams.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index e2ceee144..b61831a73 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -257,6 +257,17 @@ class ParamParser { return this.fragmentParams.get(name) ?? this.queryParams.get(name); } + public getEnumParam( + name: string, + type: { [s: string]: T } | ArrayLike, + ): T | undefined { + const value = this.getParam(name); + if (value && Object.values(type).includes(value as T)) { + return value as T; + } + return undefined; + } + public getAllParams(name: string): string[] { return [ ...this.fragmentParams.getAll(name), @@ -299,11 +310,9 @@ export const getUrlParams = ( * In short: either provide url query parameters of UrlConfiguration or set the intent * (or the global defaults will be used). */ - let intent = parser.getParam("intent") as UserIntent | null; + const intent = + parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown; - if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) { - intent = UserIntent.Unknown; - } // Here we only use constants and `platform` to determine the intent preset. let intentPreset: UrlConfiguration; switch (intent) { From bed69c08396ac1f524ae757057efd21a41e6dd7e Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 30 Jun 2025 18:48:47 +0200 Subject: [PATCH 04/12] remove deprecated url params --- src/UrlParams.test.ts | 4 ---- src/UrlParams.ts | 17 +++-------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index 495f79264..e16bd71bd 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -260,9 +260,5 @@ describe("UrlParams", () => { ); expect(getUrlParams("?header=none&hideHeader=false").header).toBe("none"); }); - it("converts hideHeader to the correct header value", () => { - expect(getUrlParams("?hideHeader=true").header).toBe("none"); - expect(getUrlParams("?hideHeader=false").header).toBe("standard"); - }); }); }); diff --git a/src/UrlParams.ts b/src/UrlParams.ts index b61831a73..67381520c 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -399,28 +399,17 @@ export const getUrlParams = ( }; const configuration: Partial = { - // This flag has 'embed' as an alias for historical reasons - confineToRoom: parser.getFlag("confineToRoom") ?? parser.getFlag("embed"), + 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.getParam("header") as HeaderStyle) ?? - (parser.getFlag("hideHeader") !== undefined - ? parser.getFlagParam("hideHeader") - ? HeaderStyle.None - : HeaderStyle.Standard - : undefined), + 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", - // the deprecated property name - ) ?? parser.getFlag("controlledMediaDevices"), + 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 From 2496b1f03d4079cc36d23704015aee587bfee761 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 30 Jun 2025 19:10:15 +0200 Subject: [PATCH 05/12] only allow skip lobby in widget (more strict needs test adjustment) --- src/UrlParams.ts | 28 ++++++++++++++++------------ src/room/MuteStates.test.tsx | 6 +++++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 67381520c..d93cea25c 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -23,6 +23,7 @@ interface RoomIdentifier { } export enum UserIntent { + // TODO: add DM vs room call StartNewCall = "start_call", JoinExistingCall = "join_existing", Unknown = "unknown", @@ -299,6 +300,10 @@ export const getUrlParams = ( 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`. @@ -310,9 +315,9 @@ export const getUrlParams = ( * In short: either provide url query parameters of UrlConfiguration or set the intent * (or the global defaults will be used). */ - const intent = - parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown; - + 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; switch (intent) { @@ -329,6 +334,7 @@ export const getUrlParams = ( controlledAudioDevices: platform === "desktop" ? false : true, skipLobby: true, returnToLobby: false, + sendNotificationType: "notification", }; break; case UserIntent.JoinExistingCall: @@ -344,8 +350,10 @@ export const getUrlParams = ( controlledAudioDevices: platform === "desktop" ? false : true, skipLobby: false, returnToLobby: false, + sendNotificationType: undefined, }; break; + // Non widget usecase defaults default: intentPreset = { confineToRoom: false, @@ -359,17 +367,10 @@ export const getUrlParams = ( controlledAudioDevices: false, skipLobby: false, returnToLobby: false, + sendNotificationType: undefined, }; } - const sendNotificationType = ["ring", "notification"].includes( - parser.getParam("sendNotificationType") ?? "", - ) - ? (parser.getParam("sendNotificationType") as RTCNotificationType) - : undefined; - const widgetId = parser.getParam("widgetId"); - const parentUrl = parser.getParam("parentUrl"); - const isWidget = !!widgetId && !!parentUrl; const properties: UrlProperties = { widgetId, parentUrl, @@ -414,7 +415,10 @@ export const getUrlParams = ( // 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, + sendNotificationType: parser.getEnumParam("sendNotificationType", [ + "ring", + "notification", + ]), }; return { diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 13dc8ee04..d349a5c67 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -191,7 +191,11 @@ describe("useMuteStates", () => { mockConfig(); render( - + From c91ee8872312db1a1d1b66c835eb2c20507ba953 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 1 Jul 2025 10:27:06 +0200 Subject: [PATCH 06/12] fix tests that now require the url to be a widget url --- src/UrlParams.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index e16bd71bd..f75b66363 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -220,15 +220,17 @@ describe("UrlParams", () => { }); it("accepts start_call", () => { - expect(getUrlParams("?intent=start_call").intent).toBe( - UserIntent.StartNewCall, - ); + expect( + getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org") + .intent, + ).toBe(UserIntent.StartNewCall); }); it("accepts join_existing", () => { - expect(getUrlParams("?intent=join_existing").intent).toBe( - UserIntent.JoinExistingCall, - ); + expect( + getUrlParams("?intent=join_existing&widgetId=1234&parentUrl=parent.org") + .intent, + ).toBe(UserIntent.JoinExistingCall); }); }); From 4a9e97071d7d10cdecd6a569c4f590b6efcde95a Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:26:43 +0200 Subject: [PATCH 07/12] Update src/UrlParams.ts Co-authored-by: Robin --- src/UrlParams.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index d93cea25c..001ae3892 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -138,10 +138,10 @@ export interface UrlProperties { } /** - * The configuration for the app. It can be set via URL parameters. - * Those parameters are different to the UrlProperties, since they are all optional - * and configure the behavior of the app. There value is the same if EC is used in - * the same context but different accoutns/users. + * 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. */ From 586bbb81b85028506c4121175ffadde8ea0f8f38 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:26:50 +0200 Subject: [PATCH 08/12] Update src/UrlParams.ts Co-authored-by: Robin --- src/UrlParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 001ae3892..71679771a 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -263,7 +263,7 @@ class ParamParser { type: { [s: string]: T } | ArrayLike, ): T | undefined { const value = this.getParam(name); - if (value && Object.values(type).includes(value as T)) { + if (value !== null && Object.values(type).includes(value as T)) { return value as T; } return undefined; From d9d99370ff00f33377f933a24a7d1d084eb01afb Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 24 Jul 2025 13:04:11 +0200 Subject: [PATCH 09/12] fix tests --- src/UrlParams.test.ts | 64 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index f75b66363..c0fe52417 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -5,13 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { getRoomIdentifierFromUrl, getUrlParams, + HeaderStyle, UserIntent, } from "../src/UrlParams"; +import { platform } from "os"; const ROOM_NAME = "roomNameHere"; const ROOM_ID = "!d45f138fsd"; @@ -211,26 +213,68 @@ describe("UrlParams", () => { }); describe("intent", () => { - it("defaults to unknown", () => { - expect(getUrlParams().intent).toBe(UserIntent.Unknown); + const noIntentDefaults = { + 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, + }; + const startNewCallDefaults = (platform: string): object => ({ + confineToRoom: true, + appPrompt: false, + preload: true, + 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", + }); + const joinExistingCallDefaults = (platform: string): object => ({ + confineToRoom: true, + appPrompt: false, + preload: true, + header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, + showControls: true, + hideScreensharing: false, + allowIceFallback: true, + perParticipantE2EE: true, + controlledAudioDevices: platform === "desktop" ? false : true, + skipLobby: false, + returnToLobby: false, + sendNotificationType: undefined, + }); + it("use no-intent-defaults with unknown intent", () => { + expect(getUrlParams()).toMatchObject(noIntentDefaults); }); it("ignores intent if it is not a valid value", () => { - expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown); + expect(getUrlParams("?intent=foo")).toMatchObject(noIntentDefaults); }); it("accepts start_call", () => { expect( - getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org") - .intent, - ).toBe(UserIntent.StartNewCall); + getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org"), + ).toMatchObject(startNewCallDefaults("desktop")); }); it("accepts join_existing", () => { expect( - getUrlParams("?intent=join_existing&widgetId=1234&parentUrl=parent.org") - .intent, - ).toBe(UserIntent.JoinExistingCall); + getUrlParams( + "?intent=join_existing&widgetId=1234&parentUrl=parent.org", + ), + ).toMatchObject(joinExistingCallDefaults("desktop")); }); }); From 206bd44a2694965f5e3e9f95e95a6ab7ce107167 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 24 Jul 2025 13:05:32 +0200 Subject: [PATCH 10/12] lint --- src/UrlParams.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index c0fe52417..87d31f93f 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -5,15 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { getRoomIdentifierFromUrl, getUrlParams, HeaderStyle, - UserIntent, } from "../src/UrlParams"; -import { platform } from "os"; const ROOM_NAME = "roomNameHere"; const ROOM_ID = "!d45f138fsd"; From 3b106f15d9b6bf5e19c6da1c7fb0ab06019f8c03 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 24 Jul 2025 14:30:23 +0200 Subject: [PATCH 11/12] review --- src/UrlParams.ts | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 71679771a..65e3d901e 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -320,37 +320,31 @@ export const getUrlParams = ( : (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown); // Here we only use constants and `platform` to determine the intent preset. let intentPreset: UrlConfiguration; + const inAppDefault = { + confineToRoom: true, + appPrompt: false, + preload: true, + 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" as RTCNotificationType, + }; switch (intent) { case UserIntent.StartNewCall: intentPreset = { - confineToRoom: true, - appPrompt: false, - preload: true, - header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, - showControls: true, - hideScreensharing: false, - allowIceFallback: true, - perParticipantE2EE: true, - controlledAudioDevices: platform === "desktop" ? false : true, + ...inAppDefault, skipLobby: true, - returnToLobby: false, - sendNotificationType: "notification", }; break; case UserIntent.JoinExistingCall: intentPreset = { - confineToRoom: true, - appPrompt: false, - preload: true, - header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, - showControls: true, - hideScreensharing: false, - allowIceFallback: true, - perParticipantE2EE: true, - controlledAudioDevices: platform === "desktop" ? false : true, + ...inAppDefault, skipLobby: false, - returnToLobby: false, - sendNotificationType: undefined, }; break; // Non widget usecase defaults From 61a9c2b58dd036b46185a9f21fde1f29a3f3bed7 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 24 Jul 2025 16:06:48 +0200 Subject: [PATCH 12/12] fix tests --- src/UrlParams.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index 87d31f93f..fbf0c0953 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -251,7 +251,7 @@ describe("UrlParams", () => { controlledAudioDevices: platform === "desktop" ? false : true, skipLobby: false, returnToLobby: false, - sendNotificationType: undefined, + sendNotificationType: "notification", }); it("use no-intent-defaults with unknown intent", () => { expect(getUrlParams()).toMatchObject(noIntentDefaults);