mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
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:
+37
-6
@@ -48,9 +48,11 @@ import {
|
|||||||
} from "../src/HostBridge";
|
} from "../src/HostBridge";
|
||||||
import { RootElementProvider } from "../src/RootElementContext";
|
import { RootElementProvider } from "../src/RootElementContext";
|
||||||
import {
|
import {
|
||||||
computeUrlParams,
|
configurationForIntent,
|
||||||
|
hostedProperties,
|
||||||
type UrlParams,
|
type UrlParams,
|
||||||
UrlParamsProvider,
|
UrlParamsProvider,
|
||||||
|
UserIntent,
|
||||||
} from "../src/UrlParams";
|
} from "../src/UrlParams";
|
||||||
import { MediaDevicesContext } from "../src/MediaDevicesContext";
|
import { MediaDevicesContext } from "../src/MediaDevicesContext";
|
||||||
import { MediaDevices } from "../src/state/MediaDevices";
|
import { MediaDevices } from "../src/state/MediaDevices";
|
||||||
@@ -74,10 +76,17 @@ export { type JoinCallData } from "../src/widget";
|
|||||||
// The deployment-wide configuration, as distinct from ElementCallConfiguration
|
// The deployment-wide configuration, as distinct from ElementCallConfiguration
|
||||||
// above, which is per call
|
// above, which is per call
|
||||||
export { type ConfigOptions } from "../src/config/ConfigOptions";
|
export { type ConfigOptions } from "../src/config/ConfigOptions";
|
||||||
|
// The values that appear in ElementCallConfiguration and in the intent
|
||||||
|
export {
|
||||||
|
BackgroundStyle,
|
||||||
|
HeaderStyle,
|
||||||
|
UserIntent,
|
||||||
|
type UrlConfiguration,
|
||||||
|
} from "../src/UrlParams";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How Element Call should behave. Everything is optional; anything left out
|
* How Element Call should behave. Everything is optional; anything left out
|
||||||
* takes the same default it would in the standalone app.
|
* takes the default that {@link ElementCallProps.intent} implies.
|
||||||
*/
|
*/
|
||||||
export type ElementCallConfiguration = Partial<UrlParams>;
|
export type ElementCallConfiguration = Partial<UrlParams>;
|
||||||
|
|
||||||
@@ -89,7 +98,21 @@ export interface ElementCallProps {
|
|||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
/** The room to call in. The host's client must already know about it. */
|
/** The room to call in. The host's client must already know about it. */
|
||||||
roomId: string;
|
roomId: string;
|
||||||
/** How Element Call should behave. */
|
/**
|
||||||
|
* What the user asked for — whether they started the call or joined one that
|
||||||
|
* was already running, and whether it is a call in a group or a DM. Element
|
||||||
|
* Call decides what each of those means: whether to show the lobby first,
|
||||||
|
* whether to ring, and so on.
|
||||||
|
*
|
||||||
|
* Defaults to joining an existing group call, which is the most conservative
|
||||||
|
* reading, but a host that knows which button the user pressed should say so.
|
||||||
|
*/
|
||||||
|
intent?: UserIntent;
|
||||||
|
/**
|
||||||
|
* How Element Call should behave, overriding whatever {@link intent} implies.
|
||||||
|
* A host that finds itself setting a lot of these probably wants a different
|
||||||
|
* intent instead.
|
||||||
|
*/
|
||||||
config?: ElementCallConfiguration;
|
config?: ElementCallConfiguration;
|
||||||
/**
|
/**
|
||||||
* How to reach the host while the call is running — to be told the user has
|
* How to reach the host while the call is running — to be told the user has
|
||||||
@@ -143,6 +166,7 @@ const Decoration: FC<{ children: JSX.Element }> = ({ children }) => {
|
|||||||
export const ElementCall: FC<ElementCallProps> = ({
|
export const ElementCall: FC<ElementCallProps> = ({
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
|
intent = UserIntent.JoinExistingCall,
|
||||||
config,
|
config,
|
||||||
hostBridge = nullHostBridge,
|
hostBridge = nullHostBridge,
|
||||||
}): ReactNode => {
|
}): ReactNode => {
|
||||||
@@ -150,10 +174,17 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
// inside can render until we have it.
|
// inside can render until we have it.
|
||||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// The defaults are the standalone app's, with the host's wishes over the top
|
// Element Call has no URL of its own to read any of this from, and the
|
||||||
|
// host's URL is not Element Call's business, so the defaults come from the
|
||||||
|
// intent with the host's wishes over the top.
|
||||||
const params = useMemo(
|
const params = useMemo(
|
||||||
(): UrlParams => ({ ...computeUrlParams(), ...config }),
|
(): UrlParams => ({
|
||||||
[config],
|
...hostedProperties,
|
||||||
|
roomId,
|
||||||
|
...configurationForIntent(intent),
|
||||||
|
...config,
|
||||||
|
}),
|
||||||
|
[roomId, intent, config],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaDevices = useInitial(
|
const mediaDevices = useInitial(
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import { logger } from "matrix-js-sdk/lib/logger";
|
|||||||
|
|
||||||
import * as PlatformMod from "../src/Platform";
|
import * as PlatformMod from "../src/Platform";
|
||||||
import {
|
import {
|
||||||
|
BackgroundStyle,
|
||||||
|
configurationForIntent,
|
||||||
getRoomIdentifierFromUrl,
|
getRoomIdentifierFromUrl,
|
||||||
computeUrlParams,
|
computeUrlParams,
|
||||||
HeaderStyle,
|
HeaderStyle,
|
||||||
getUrlParams,
|
getUrlParams,
|
||||||
|
hostedProperties,
|
||||||
|
UserIntent,
|
||||||
} from "../src/UrlParams";
|
} from "../src/UrlParams";
|
||||||
import { mockConfig } from "./utils/test";
|
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
@@ -354,6 +354,135 @@ export const getUrlParams = (
|
|||||||
return params;
|
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.
|
* Gets the app parameters for the current URL.
|
||||||
* @param search The URL search string
|
* @param search The URL search string
|
||||||
@@ -383,78 +512,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
|||||||
const intent = !isWidget
|
const intent = !isWidget
|
||||||
? UserIntent.Unknown
|
? UserIntent.Unknown
|
||||||
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
||||||
// Here we only use constants and `platform` to determine the intent preset.
|
const intentPreset = configurationForIntent(intent);
|
||||||
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 properties: UrlProperties = {
|
const properties: UrlProperties = {
|
||||||
widgetId,
|
widgetId,
|
||||||
|
|||||||
Reference in New Issue
Block a user