Introduce ElementCallView

Being in a call and deciding to show one are different jobs, but RoomPage
did both: routing, authentication, resolving room aliases and knocking, and
then the call itself. Only the first set belongs to whatever is hosting
Element Call.

Add ElementCallView as the seam between them. It takes a client and a call
to join, and owns whether the user has joined — which is the call's own
business rather than its host's. RoomPage keeps everything about arriving
at a call and renders this for the call itself.

Nothing else moves yet. muteStates is still passed in, because the
standalone shell shares one with the lobby it shows while waiting to be let
into a room, and two instances would both report the user's mute state to
the host.
This commit is contained in:
Valere
2026-09-03 12:10:04 +02:00
parent ebb7fe07b7
commit 1bfb018d90
2 changed files with 75 additions and 5 deletions
+73
View File
@@ -0,0 +1,73 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type FC, type ReactNode, useState } from "react";
import { type MatrixClient } from "matrix-js-sdk";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { GroupCallView } from "./room/GroupCallView";
import { type MuteStates } from "./state/MuteStates";
import { type UrlParams } from "./UrlParams";
interface Props {
/** The client to place the call with. */
client: MatrixClient;
/** The call to join. */
rtcSession: MatrixRTCSession;
/** The audio and video mute state to start from, and keep in step with. */
muteStates: MuteStates;
/**
* Whether the user is signed in as a guest, and so should be offered the
* chance to create an account when the call ends.
*/
isPasswordlessUser: boolean;
/** Whether to keep the user in this call rather than letting them navigate. */
confineToRoom: boolean;
/** Whether to wait for the host to ask us to join. */
preload: UrlParams["preload"];
/** Whether to enter the call directly, without showing the lobby first. */
skipLobby: UrlParams["skipLobby"];
}
/**
* A call, as a component.
*
* This owns being in a call, and nothing about how Element Call came to be
* showing one: no routing, no authentication, no resolving of room aliases.
* Those belong to whatever is hosting it — the standalone app's own shell, or
* an application embedding Element Call directly.
*
* TODO: `muteStates` is still passed in, because the standalone shell shares
* one with the lobby it shows while waiting to be let into a room. Ownership
* moves here once that lobby has its own.
*/
export const ElementCallView: FC<Props> = ({
client,
rtcSession,
muteStates,
isPasswordlessUser,
confineToRoom,
preload,
skipLobby,
}): ReactNode => {
// Whether the user is in the call is the call's own business, not its host's.
const [joined, setJoined] = useState(false);
return (
<GroupCallView
client={client}
rtcSession={rtcSession}
joined={joined}
setJoined={setJoined}
isPasswordlessUser={isPasswordlessUser}
confineToRoom={confineToRoom}
preload={preload}
skipLobby={skipLobby}
muteStates={muteStates}
/>
);
};
+2 -5
View File
@@ -25,7 +25,7 @@ import {
import { useClientLegacy } from "../ClientContext"; import { useClientLegacy } from "../ClientContext";
import { ErrorPage, FullScreenView, LoadingPage } from "../FullScreenView"; import { ErrorPage, FullScreenView, LoadingPage } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallView } from "./GroupCallView"; import { ElementCallView } from "../ElementCallView";
import { useRoomIdentifier, useUrlParams } from "../UrlParams"; import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { HomePage } from "../home/HomePage"; import { HomePage } from "../home/HomePage";
@@ -62,7 +62,6 @@ export const RoomPage: FC = (): ReactNode => {
const { avatarUrl, displayName: userDisplayName } = useProfile(client); const { avatarUrl, displayName: userDisplayName } = useProfile(client);
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers); const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
const [joined, setJoined] = useState(false);
const devices = useMediaDevices(); const devices = useMediaDevices();
const [muteStates, setMuteStates] = useState<MuteStates | null>(null); const [muteStates, setMuteStates] = useState<MuteStates | null>(null);
@@ -125,11 +124,9 @@ export const RoomPage: FC = (): ReactNode => {
case "loaded": case "loaded":
return ( return (
muteStates && ( muteStates && (
<GroupCallView <ElementCallView
client={client!} client={client!}
rtcSession={groupCallState.rtcSession} rtcSession={groupCallState.rtcSession}
joined={joined}
setJoined={setJoined}
isPasswordlessUser={passwordlessUser} isPasswordlessUser={passwordlessUser}
confineToRoom={confineToRoom} confineToRoom={confineToRoom}
preload={preload} preload={preload}