Stop asking whether Element Call is a widget

The remaining reads of the widget global were all asking one of two
different questions, so they get two different answers.

The app shell — the auth hooks, automatic guest registration, the group
call loader's diagnostic and the initial mute state — wants to know
whether Element Call was launched as a widget. That is a property of the
URL it was launched with, so expose the isWidget that computeUrlParams
already computed internally, documented as being for shell use only.

The call interface — whether to offer the profile settings tab — wants to
know something about its host, so it asks the bridge. A host that can
dismiss Element Call owns the user's account, so their profile is not ours
to edit; this reuses the close capability as a proxy, with a TODO
alongside the others.

ClientContext also takes supportsReactions from the bridge rather than
checking four widget capabilities itself, which removes widgetApi from
InitResult — a field that was always null outside widget mode.

Note this changes behaviour for a malformed widget URL: one carrying a
widget ID and parent URL but missing the room, user, device or base URL
would previously have fallen back to registering a guest user, and will
now not.
This commit is contained in:
Valere
2026-09-02 15:04:24 +02:00
parent bc6c232ef0
commit f28d9e9b06
9 changed files with 45 additions and 46 deletions
+7 -4
View File
@@ -28,7 +28,6 @@ import { useTranslation } from "react-i18next";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { HeaderStyle, useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { widget } from "../widget";
import { useHostBridge } from "../HostBridge.ts";
import styles from "./InCallView.module.css";
import { GridTile } from "../tile/GridTile";
@@ -251,6 +250,7 @@ export const InCallView: FC<InCallViewProps> = ({
}) => {
const logger = rootLogger.getChild("[InCallView]");
const { t } = useTranslation();
const hostBridge = useHostBridge();
const { sendReaction, toggleRaisedHand } = useReactionsSender();
useWakeLock();
@@ -334,14 +334,17 @@ export const InCallView: FC<InCallViewProps> = ({
const openProfile = useMemo(
() =>
// Profile settings are unavailable in widget mode
widget === null
// A host that can dismiss us is a host that owns the user's account, so
// their profile is not ours to edit.
// TODO: another use of the close capability as a proxy — see the note in
// GroupCallView.
hostBridge.close === undefined
? (): void => {
setSettingsTab("profile");
setSettingsOpen(true);
}
: null,
[setSettingsTab, setSettingsOpen],
[setSettingsTab, setSettingsOpen, hostBridge],
);
const [headerRef, headerBounds] = useMeasure();
+3 -3
View File
@@ -29,7 +29,6 @@ import { GroupCallView } from "./GroupCallView";
import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { HomePage } from "../home/HomePage";
import { widget } from "../widget";
import { useHostBridge } from "../HostBridge.ts";
import { CallTerminatedMessage, useLoadGroupCall } from "./useLoadGroupCall";
import { LobbyView } from "./LobbyView";
@@ -77,7 +76,7 @@ export const RoomPage: FC = (): ReactNode => {
calculateInitialMuteState(
urlParams.skipLobby,
urlParams.callIntent,
widget !== null,
urlParams.isWidget,
),
hostBridge,
),
@@ -88,7 +87,7 @@ export const RoomPage: FC = (): ReactNode => {
useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as
// a URL param, automatically register a passwordless user
if (!loading && !authenticated && displayName && !widget) {
if (!loading && !authenticated && displayName && !urlParams.isWidget) {
setIsRegistering(true);
registerPasswordlessUser(displayName)
.catch((e) => {
@@ -102,6 +101,7 @@ export const RoomPage: FC = (): ReactNode => {
loading,
authenticated,
displayName,
urlParams.isWidget,
setIsRegistering,
registerPasswordlessUser,
]);
+4 -2
View File
@@ -34,7 +34,7 @@ import {
EndCallIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { widget } from "../widget";
import { useUrlParams } from "../UrlParams";
export type GroupCallLoaded = {
kind: "loaded";
@@ -132,6 +132,7 @@ export const useLoadGroupCall = (
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
const activeRoom = useRef<Room | undefined>(undefined);
const { t } = useTranslation();
const { isWidget } = useUrlParams();
const bannedError = useCallback(
(): CallTerminatedMessage =>
@@ -249,7 +250,7 @@ export const useLoadGroupCall = (
// room already joined so we are done here already.
return room!;
}
if (widget)
if (isWidget)
// in widget mode we never should reach this point. (getRoom should return the room.)
throw new Error(
"Room not found. The widget-api did not pass over the relevant room events/information.",
@@ -373,6 +374,7 @@ export const useLoadGroupCall = (
}, [
bannedError,
client,
isWidget,
knockRejectError,
removeNoticeError,
roomIdOrAlias,