Derive the component's defaults from an intent, not the host's URL

The component seeded its parameters with `computeUrlParams()`, which reads
`window.location`. Element Call is not the page any more, so what it found
there was the host's URL: no `widgetId`, therefore not a widget, therefore no
intent, therefore the standalone app's preset.

The visible symptom was the Element Call logo in the footer of a call embedded
in someone else's application, since `showLogo` follows `header ===
HeaderStyle.Standard`. The rest of that preset mattered more. A hosted call
defaulted to `perParticipantE2EE: false` and `confineToRoom: false` — so
unencrypted, and willing to take the user out of the room a host had put them
in — and a host could not correct either without naming every parameter
itself, nor even name `header`, since the enums were not exported.

So the intent presets move out of `computeUrlParams` into
`configurationForIntent`, and the component builds its parameters from an
intent plus the properties a host has no URL to supply. `intent` becomes a
prop, defaulting to joining an existing group call: the lobby first, confined
to the room, encrypted per participant, and no Element Call branding in
someone else's interface. A host that knows which button the user pressed
should say which.

One deliberate difference from the widget presets: the background defaults to
solid rather than the gradient, which is drawn by a `position: fixed`
pseudo-element and would escape the container to cover the host.

`UserIntent.Unknown` still means the standalone preset, so the app and widget
are unchanged — including a widget that names no intent, which has always been
given those defaults.
This commit is contained in:
Valere
2026-09-03 16:39:14 +02:00
parent 9f5fa6049f
commit c0a54dcaf9
3 changed files with 215 additions and 78 deletions
+48
View File
@@ -11,10 +11,14 @@ import { logger } from "matrix-js-sdk/lib/logger";
import * as PlatformMod from "../src/Platform";
import {
BackgroundStyle,
configurationForIntent,
getRoomIdentifierFromUrl,
computeUrlParams,
HeaderStyle,
getUrlParams,
hostedProperties,
UserIntent,
} from "../src/UrlParams";
import { mockConfig } from "./utils/test";
@@ -424,4 +428,48 @@ describe("UrlParams", () => {
);
});
});
// What Element Call runs with when a host embeds it as a component, which
// has no URL of its own for any of this to come from
describe("hosted defaults", () => {
it("assume nothing about a session or a page", () => {
expect(hostedProperties).toMatchObject({
// The host is not a widget host, and supplies the client itself, so
// none of the widget or session plumbing applies
isWidget: false,
widgetId: null,
parentUrl: null,
userId: null,
deviceId: null,
baseUrl: null,
homeserver: null,
// The gradient is drawn by a `position: fixed` pseudo-element, which
// would escape the container and cover the host's own interface
background: BackgroundStyle.Solid,
});
});
it("keep a hosted call inside its room", () => {
const hosted = configurationForIntent(UserIntent.JoinExistingCall);
expect(hosted).toMatchObject({
// A host owns navigation, so Element Call must not offer a way out of
// the room
confineToRoom: true,
perParticipantE2EE: true,
// The lobby first, so that the user picks their devices rather than
// being thrown into the call by the act of being rendered
skipLobby: false,
});
// No Element Call branding inside someone else's application
expect(hosted.header).not.toBe(HeaderStyle.Standard);
});
it("fall back to the standalone app's when no intent is stated", () => {
expect(configurationForIntent(UserIntent.Unknown)).toMatchObject({
confineToRoom: false,
header: HeaderStyle.Standard,
perParticipantE2EE: false,
});
});
});
});
+130 -72
View File
@@ -354,6 +354,135 @@ export const getUrlParams = (
return params;
};
/**
* The configuration implied by what the user meant to do — if they pressed a
* Start Call button this would be `start_call`, and if they pressed Join Call,
* `join_existing`.
*
* These are platform-specific defaults, so that a host can start a call by
* saying what the user asked for rather than by setting every parameter itself,
* and so that what each intent means is Element Call's decision, made in one
* place. A host that wants something else states it alongside the intent.
*
* {@link UserIntent.Unknown} means no intent was stated, and gives the
* standalone app's defaults: Element Call owns the whole page, so it offers the
* way out of the room that a hosted call must not.
*/
export function configurationForIntent(intent: UserIntent): UrlConfiguration {
// Only constants and `platform` here, so that this depends on nothing but
// the intent.
let preset: UrlConfiguration = {
confineToRoom: true,
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:
preset.skipLobby = false;
preset.callIntent = "video";
break;
case UserIntent.JoinExistingCall:
// On desktop this will be overridden based on which button was used to join the call
preset.skipLobby = false;
preset.callIntent = "video";
break;
case UserIntent.StartNewCallVoice:
preset.skipLobby = false;
preset.callIntent = "audio";
break;
case UserIntent.JoinExistingCallVoice:
// On desktop this will be overridden based on which button was used to join the call
preset.skipLobby = false;
preset.callIntent = "audio";
break;
case UserIntent.StartNewCallDMVoice:
preset.callIntent = "audio";
// Fall through
case UserIntent.StartNewCallDM:
preset.skipLobby = true;
preset.sendNotificationType = "ring";
preset.autoLeaveWhenOthersLeft = true;
preset.waitForCallPickup = true;
preset.callIntent = preset.callIntent ?? "video";
break;
case UserIntent.JoinExistingCallDMVoice:
preset.callIntent = "audio";
// Fall through
case UserIntent.JoinExistingCallDM:
// On desktop this will be overridden based on which button was used to join the call
preset.skipLobby = true;
preset.autoLeaveWhenOthersLeft = true;
preset.callIntent = preset.callIntent ?? "video";
break;
// Non widget usecase defaults
default:
preset = {
confineToRoom: false,
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,
};
}
return preset;
}
/**
* The {@link UrlProperties} for Element Call embedded in a host application.
*
* It has no URL of its own to read these from, and it does not need most of
* them: the widget plumbing does not apply, the Matrix client and the analytics
* configuration come from the host by other routes, and what is left is either
* the host's to state through the component's props or Element Call's own
* default.
*/
export const hostedProperties: UrlProperties = {
widgetId: null,
parentUrl: null,
isWidget: false,
roomId: null,
userId: null,
displayName: null,
deviceId: null,
baseUrl: null,
lang: null,
fonts: [],
fontScale: null,
posthogUserId: null,
posthogApiHost: null,
posthogApiKey: null,
e2eEnabled: true,
password: null,
viaServers: null,
homeserver: null,
rageshakeSubmitUrl: null,
sentryDsn: null,
sentryEnvironment: null,
theme: null,
// Solid rather than the gradient the standalone app defaults to: the gradient
// is drawn by a `position: fixed` pseudo-element, which would escape the
// container Element Call was given and cover the host's own interface.
background: BackgroundStyle.Solid,
};
/**
* Gets the app parameters for the current URL.
* @param search The URL search string
@@ -383,78 +512,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
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,
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.StartNewCallVoice:
intentPreset.skipLobby = false;
intentPreset.callIntent = "audio";
break;
case UserIntent.JoinExistingCallVoice:
// On desktop this will be overridden based on which button was used to join the call
intentPreset.skipLobby = false;
intentPreset.callIntent = "audio";
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,
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 intentPreset = configurationForIntent(intent);
const properties: UrlProperties = {
widgetId,