Show the wait for the first background effect on its tile

- The first effect chosen in a session shows a spinner on its own tile,
  in place of the tick, until a frame carrying it is drawn. Later changes
  show nothing, having nothing to prepare.
- Ended by the frame, not the promises, which resolve while the segmenter
  is still being built. The transformer reports the first frame that went
  through the segmenter; one passed through untouched doesn't count.
- The pipeline publishes the wait with its state, and both footer view
  models take it as an input.
- The lobby tests' provider mock gains the observable hook, built once: a
  fresh observable per render rebuilt the footer on every render.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-10-08 14:35:23 +02:00
co-authored by Claude Opus 5.5
parent 00402b54b5
commit deef868cd0
16 changed files with 231 additions and 29 deletions
+2
View File
@@ -22,6 +22,7 @@ import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import classNames from "classnames";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { useTranslation } from "react-i18next";
import { map } from "rxjs";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { HeaderStyle, useUrlParams } from "../UrlParams";
@@ -184,6 +185,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
vm,
props.muteStates,
mediaDevices,
backgroundEffectsState$.pipe(map(({ settling }) => settling ?? false)),
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
{ showControls: urlParams.showControls, header: urlParams.header },
);
+14 -5
View File
@@ -10,6 +10,7 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TooltipProvider } from "@vector-im/compound-web";
import { type MatrixClient, type RoomSummary } from "matrix-js-sdk";
import { type Observable } from "rxjs";
import { KnockLobbyView } from "./KnockLobbyView";
import { LeaveToHomeProvider } from "../LeaveToHomeContext";
@@ -21,13 +22,21 @@ vi.mock("@livekit/components-react", () => ({
usePreviewTracks: (): unknown[] => [],
}));
vi.mock("../livekit/BackgroundEffectsContext", () => ({
useBackgroundEffects: (): BackgroundEffectsState => ({
vi.mock("../livekit/BackgroundEffectsContext", async () => {
const { of } = await import("rxjs");
const none: BackgroundEffectsState = {
supported: false,
processor: undefined,
}),
useSyncBackgroundEffects: (): void => {},
}));
};
// One observable, as the real hook keeps: a fresh one each render would
// rebuild the footer on every render.
const none$ = of(none);
return {
useBackgroundEffects: (): BackgroundEffectsState => none,
useBackgroundEffectsState$: (): Observable<BackgroundEffectsState> => none$,
useSyncBackgroundEffects: (): void => {},
};
});
vi.mock("react-use-measure", () => ({
default: (): [() => void, object] => [(): void => {}, {}],
+14 -6
View File
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { of } from "rxjs";
import { type Observable, of } from "rxjs";
import { LeaveToHomeProvider } from "../LeaveToHomeContext";
import { TooltipProvider } from "@vector-im/compound-web";
import { type MatrixClient } from "matrix-js-sdk";
@@ -41,13 +41,21 @@ vi.mock("@livekit/components-react", () => ({
usePreviewTracks: (): unknown[] => [],
}));
vi.mock("../livekit/BackgroundEffectsContext", () => ({
useBackgroundEffects: (): BackgroundEffectsState => ({
vi.mock("../livekit/BackgroundEffectsContext", async () => {
const { of } = await import("rxjs");
const none: BackgroundEffectsState = {
supported: false,
processor: undefined,
}),
useSyncBackgroundEffects: (): void => {},
}));
};
// One observable, as the real hook keeps: a fresh one each render would
// rebuild the footer on every render.
const none$ = of(none);
return {
useBackgroundEffects: (): BackgroundEffectsState => none,
useBackgroundEffectsState$: (): Observable<BackgroundEffectsState> => none$,
useSyncBackgroundEffects: (): void => {},
};
});
vi.mock("react-use-measure", () => ({
default: (): [() => void, object] => [(): void => {}, {}],
+12 -1
View File
@@ -24,6 +24,7 @@ import {
type LocalVideoTrack,
Track,
} from "livekit-client";
import { map } from "rxjs";
import inCallStyles from "./InCallView.module.css";
import styles from "./LobbyView.module.css";
@@ -41,6 +42,7 @@ import { ObservableScope } from "../state/ObservableScope";
import { useInitial } from "../useInitial";
import {
useBackgroundEffects,
useBackgroundEffectsState$,
useSyncBackgroundEffects,
} from "../livekit/BackgroundEffectsContext";
import { getValue } from "../utils/observable";
@@ -137,6 +139,7 @@ export const LobbyView: FC<Props> = ({
);
const { processor } = useBackgroundEffects();
const backgroundEffectsState$ = useBackgroundEffectsState$();
const initialProcessor = useInitial(() => processor);
const localTrackOptions = useMemo<CreateLocalTracksOptions>(
@@ -195,6 +198,7 @@ export const LobbyView: FC<Props> = ({
footerScope,
muteStates,
devices,
backgroundEffectsState$.pipe(map(({ settling }) => settling ?? false)),
openSettings,
hangup,
// Logo and header are connected: only show the logo in SPA with header.
@@ -204,7 +208,14 @@ export const LobbyView: FC<Props> = ({
return (): void => {
footerScope.end();
};
}, [devices, hangup, hideHeader, muteStates, openSettings]);
}, [
devices,
backgroundEffectsState$,
hangup,
hideHeader,
muteStates,
openSettings,
]);
// TODO: Unify this component with InCallView, so we can get slick joining
// animations and don't have to feel bad about reusing its CSS