This commit is contained in:
Timo K.
2026-09-09 16:05:18 +02:00
parent 2ac95df75f
commit 59ef5f5400
4 changed files with 100 additions and 75 deletions
+2 -4
View File
@@ -141,8 +141,7 @@ export function useComponentHostBridge(
requests.themeChange$.next({ data: { name: theme }, reply: () => {} }); requests.themeChange$.next({ data: { name: theme }, reply: () => {} });
}, [requests, theme]); }, [requests, theme]);
const bridge = useInitial( const bridge = useInitial((): HostBridge => ({
(): HostBridge => ({
setAlwaysOnScreen: async (alwaysOnScreen) => { setAlwaysOnScreen: async (alwaysOnScreen) => {
await latest.current.setAlwaysOnScreen?.(alwaysOnScreen); await latest.current.setAlwaysOnScreen?.(alwaysOnScreen);
}, },
@@ -180,8 +179,7 @@ export function useComponentHostBridge(
// offers to edit the profile from inside a component. // offers to edit the profile from inside a component.
supportsProfileChanges: false, supportsProfileChanges: false,
...requests, ...requests,
}), }));
);
useImperativeHandle( useImperativeHandle(
ref, ref,
+18 -3
View File
@@ -40,6 +40,7 @@ import {
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import { type MatrixClient } from "matrix-js-sdk"; import { type MatrixClient } from "matrix-js-sdk";
@@ -298,14 +299,28 @@ export const ElementCall: FC<ElementCallProps> = ({
`Element Call was asked to call in ${roomId}, which its host's client does not know about`, `Element Call was asked to call in ${roomId}, which its host's client does not know about`,
); );
// Everything the call needs is in hand once these exist, and the first
// render with them is where the call itself appears: the moment the host
// is told that Element Call has loaded, as the widget tells its client once
// its own initialisation is over. Once per mount, however often the pieces
// are later swapped out.
const ready =
container !== null && rtcSession !== null && mediaDevices !== null;
const announcedLoaded = useRef(false);
useEffect(() => {
if (!ready || announcedLoaded.current) return;
announcedLoaded.current = true;
hostBridge
.contentLoaded()
.catch((e) => logger.error("Could not tell the host we had loaded", e));
}, [ready, hostBridge]);
return ( return (
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<HostBridgeProvider value={hostBridge}> <HostBridgeProvider value={hostBridge}>
<UrlParamsProvider value={params}> <UrlParamsProvider value={params}>
<div ref={setContainer} className={styles.root}> <div ref={setContainer} className={styles.root}>
{container !== null && {ready && (
rtcSession !== null &&
mediaDevices !== null && (
<RootElementProvider value={container}> <RootElementProvider value={container}>
{/* Whatever goes wrong in here is shown in here. Left to {/* Whatever goes wrong in here is shown in here. Left to
propagate, an error would unmount the host's own tree. */} propagate, an error would unmount the host's own tree. */}
+5
View File
@@ -86,6 +86,11 @@ export default defineConfig({
// enumerateDevices work on CI runners without real hardware. // enumerateDevices work on CI runners without real hardware.
"media.navigator.streams.fake": true, "media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true, "media.navigator.permission.disabled": true,
// Vite serves HTTPS over HTTP/2, and Firefox intermittently stalls
// on Node's HTTP/2 server with a page that never finishes loading
// (one run in five or so, locally). Every server in the suite
// still speaks HTTP/1.1, so nothing is lost by insisting on it.
"network.http.http2.enabled": false,
}, },
}, },
}, },
+9 -2
View File
@@ -25,6 +25,11 @@ import { SpaHelpers } from "../spa-helpers.ts";
* guaranteed both. * guaranteed both.
*/ */
// Each test signs in twice, sets up crypto twice and syncs twice before
// anything is on screen, and then waits for media to connect; the waits below
// are sized for that, so the tests have to be too
test.describe.configure({ timeout: 180_000 });
/** The settings button, whichever of the two the footer is currently showing. */ /** The settings button, whichever of the two the footer is currently showing. */
function settingsButton(pane: Locator): Locator { function settingsButton(pane: Locator): Locator {
return pane return pane
@@ -206,16 +211,18 @@ test("looks the same in a small container as in a small window", async ({
browser, browser,
}) => { }) => {
// Two calls to set up, one of them through the harness's two logins // Two calls to set up, one of them through the harness's two logins
test.setTimeout(240_000); test.setTimeout(300_000);
const size = { width: 300, height: 300 }; const size = { width: 300, height: 300 };
// The reference is Element Call owning a window of that size, which is what // The reference is Element Call owning a window of that size, which is what
// a mobile app's webview or a browser's picture-in-picture gives it, and // a mobile app's webview or a browser's picture-in-picture gives it, and
// what its small-window styling was written for. // what its small-window styling was written for.
// No permissions to grant: each browser is launched with fake media that is
// handed out without asking (see playwright.config.ts), and Firefox rejects
// a request for `camera` or `microphone` outright
const referenceContext = await browser.newContext({ const referenceContext = await browser.newContext({
viewport: size, viewport: size,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
permissions: ["microphone", "camera"],
}); });
const referencePage = await referenceContext.newPage(); const referencePage = await referenceContext.newPage();
await referencePage.goto("/"); await referencePage.goto("/");