/* 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 FormEvent, type ReactNode, useCallback, useMemo, useRef, useState, } from "react"; import { type MatrixClient } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; import { ElementCall, type ElementCallHandle, supportedLanguages, } from "../index"; import { createDevHostBridge } from "./DevHostBridge"; import { createSession, joinRoom } from "./session"; import styles from "./Harness.module.css"; interface Credentials { homeserver: string; username: string; password: string; room: string; } const CREDENTIALS_KEY = "element-call-component-harness"; const DEFAULT_CREDENTIALS: Credentials = { homeserver: "https://synapse.m.localhost", username: "", password: "", room: "", }; /** * The credentials to start with: the last ones used, so that a reload does not * mean typing them again, overridden by anything in the query string. * * A host reading its own URL is entirely proper — it was Element Call doing so * that was the mistake. It lets the end-to-end tests, or a shared link, say * which account and room to use. */ function loadCredentials(): Credentials { let stored: Partial = {}; try { const json = localStorage.getItem(CREDENTIALS_KEY); if (json !== null) stored = JSON.parse(json) as Credentials; } catch (e) { logger.warn("Could not read the stored harness credentials", e); } const query = new URLSearchParams(location.search); const fromUrl = Object.fromEntries( (["homeserver", "username", "password", "room"] as const) .map((name) => [name, query.get(name)]) .filter(([, value]) => value !== null), ) as Partial; return { ...DEFAULT_CREDENTIALS, ...stored, ...fromUrl }; } interface Session { label: string; client: MatrixClient; } type State = | { phase: "credentials" } | { phase: "starting"; progress: string } | { phase: "started"; roomId: string; sessions: Session[] } | { phase: "failed"; error: string }; interface LogEntry { pane: string; message: string; at: string; } /** * One Element Call component, with the controls a host would have over it: the * requests it can make of Element Call, and the ability to take it off screen * altogether. */ const Pane: FC<{ session: Session; roomId: string; language: string | undefined; log: (pane: string, message: string) => void; }> = ({ session, roomId, language, log }): ReactNode => { const [mounted, setMounted] = useState(true); const bridge = useMemo( () => createDevHostBridge( (message) => log(session.label, message), () => setMounted(false), ), [log, session.label], ); // What the host asks of Element Call goes through the component's handle. // Worth saying out loud when a request is refused — asking to hang up when // there is no call, say — since that is the sort of thing the harness is for. const handle = useRef(null); const ask = ( name: string, make: (handle: ElementCallHandle) => Promise, ): void => { if (handle.current === null) { log(session.label, `← ${name}: not mounted`); return; } log(session.label, `← ${name}`); make(handle.current).then( (reply) => log( session.label, `→ ${name} acknowledged${reply === undefined ? "" : `: ${JSON.stringify(reply)}`}`, ), (e: unknown) => log(session.label, `→ ${name} refused: ${e}`), ); }; return (
{session.label} {session.client.getDeviceId()}
{/* Resizable, because how Element Call copes with the size it is given is one of the things we cannot find out from the standalone app */}
{mounted && ( )}
); }; /** Host furniture, to make it visible if Element Call styles anything but itself. */ const HostChrome: FC = (): ReactNode => ( ); /** * A dialog of the host's own, over the top of the calls. Element Call as a * component has to sit underneath this — being unable to is one of the reasons * for a component rather than an iframe. */ const HostDialog: FC<{ onClose: () => void }> = ({ onClose }): ReactNode => (

A dialog belonging to the host

This should cover the calls completely.

); /** * Stands in for a host application using the Element Call component: it owns the Matrix * clients, the page and the space each call is given, and reaches Element Call * only through the component's public interface. * * Two calls at once, from two devices of the same account, so that a real call * happens between them and anything Element Call keeps once per process rather * than once per call shows itself. */ export const Harness: FC = (): ReactNode => { const [credentials, setCredentials] = useState(loadCredentials); const [state, setState] = useState({ phase: "credentials" }); const [entries, setEntries] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); // The host's language setting, which Element Call follows. Undefined means // the host has none and Element Call uses the browser's. const [language, setLanguage] = useState(undefined); const log = useCallback((pane: string, message: string): void => { setEntries((entries) => [ ...entries, { pane, message, at: new Date().toLocaleTimeString() }, ].slice(-100), ); }, []); const start = useCallback( (event: FormEvent): void => { event.preventDefault(); localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(credentials)); const { homeserver, username, password, room } = credentials; const progress = (message: string): void => setState({ phase: "starting", progress: message }); progress("Starting"); void (async (): Promise => { try { // One at a time: two logins at once from the same account is the // shape of request homeservers rate limit const sessions: Session[] = []; for (const label of ["Call A", "Call B"]) sessions.push({ label, client: await createSession( homeserver, username, password, (message) => progress(`${label}: ${message}`), ), }); progress("Joining the room"); let roomId = room; for (const { client } of sessions) roomId = await joinRoom(client, roomId); setState({ phase: "started", roomId, sessions }); } catch (e) { logger.error("The harness could not start", e); setState({ phase: "failed", error: `${e}` }); } })(); }, [credentials], ); const field = ( name: keyof Credentials, label: string, type = "text", ): ReactNode => ( ); if (state.phase !== "started") return (

Element Call component harness

Signs in twice and shows the Element Call component twice, in a page that is not Element Call's own.

{field("homeserver", "Homeserver")} {field("username", "Username")} {field("password", "Password", "password")} {field("room", "Room ID or alias")} {state.phase === "starting" &&

{state.progress}

} {state.phase === "failed" && (

{state.error}

)}
); return (

Element Call component harness

{state.roomId}
{state.sessions.map((session) => ( ))}

Host bridge

    {entries.map((entry, i) => (
  1. {entry.at} {entry.pane}{" "} {entry.message}
  2. ))}
{dialogOpen && setDialogOpen(false)} />}
); };