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, theme]);
const bridge = useInitial(
(): HostBridge => ({
const bridge = useInitial((): HostBridge => ({
setAlwaysOnScreen: async (alwaysOnScreen) => {
await latest.current.setAlwaysOnScreen?.(alwaysOnScreen);
},
@@ -180,8 +179,7 @@ export function useComponentHostBridge(
// offers to edit the profile from inside a component.
supportsProfileChanges: false,
...requests,
}),
);
}));
useImperativeHandle(
ref,
+18 -3
View File
@@ -40,6 +40,7 @@ import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
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`,
);
// 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 (
<I18nextProvider i18n={i18n}>
<HostBridgeProvider value={hostBridge}>
<UrlParamsProvider value={params}>
<div ref={setContainer} className={styles.root}>
{container !== null &&
rtcSession !== null &&
mediaDevices !== null && (
{ready && (
<RootElementProvider value={container}>
{/* Whatever goes wrong in here is shown in here. Left to
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.
"media.navigator.streams.fake": 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.
*/
// 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. */
function settingsButton(pane: Locator): Locator {
return pane
@@ -206,16 +211,18 @@ test("looks the same in a small container as in a small window", async ({
browser,
}) => {
// 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 };
// 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
// 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({
viewport: size,
ignoreHTTPSErrors: true,
permissions: ["microphone", "camera"],
});
const referencePage = await referenceContext.newPage();
await referencePage.goto("/");