/*
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,
useState,
} from "react";
import { type MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { ElementCall } 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 last credentials used, so that a reload does not mean typing them again. */
function loadCredentials(): Credentials {
try {
const stored = localStorage.getItem(CREDENTIALS_KEY);
if (stored !== null)
return { ...DEFAULT_CREDENTIALS, ...(JSON.parse(stored) as Credentials) };
} catch (e) {
logger.warn("Could not read the stored harness credentials", e);
}
return DEFAULT_CREDENTIALS;
}
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 embedded Element Call, 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;
log: (pane: string, message: string) => void;
}> = ({ session, roomId, log }): ReactNode => {
const [mounted, setMounted] = useState(true);
const bridge = useMemo(
() =>
createDevHostBridge(
(message) => log(session.label, message),
() => setMounted(false),
),
[log, session.label],
);
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 embedded
* in a host has to sit underneath this — being unable to is one of the reasons
* for embedding it rather than putting it in 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 embedding Element Call: 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);
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 (
);
return (