Guard what can be added as a background

- Five of one's own at most, so the grid is whole: at the limit the add
  tile goes, and it comes back once one is removed. The store holds the
  limit too.
- An added image is refused if it isn't an image, is animated, or can't
  be decoded; reduced to 1920px on its longest edge; and laid on an opaque
  ground, since a transparent pixel would be a hole in the background.
- Animation is read from the file, as a still and an animated WebP share
  a type. Chromium has no selected track until the decoder's tracks are
  ready; without waiting for them an animated GIF got through.
- A refusal says why, in place of the notice, and is scrolled into view.
  It can be dismissed, is gone once the menu closes, and the next shows
  even if it reads the same. What was in force stays.
- End to end on Chromium and Firefox, from what the device then keeps: an
  animated GIF is refused with its message, a 4000x3000 image is kept at
  1920x1440, and a half transparent one is kept opaque. All three fail
  with the preparation bypassed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-10-08 14:35:25 +02:00
co-authored by Claude Opus 5.5
parent 8637a1d736
commit 24ca0eacc7
13 changed files with 518 additions and 29 deletions
+23
View File
@@ -33,6 +33,11 @@ import { useBehavior } from "../useBehavior";
import { type LayoutSwitchViewModel } from "../state/LayoutSwitchViewModel";
import { LayoutSwitch } from "../room/LayoutSwitch";
import { type BackgroundEffectOption } from "./BackgroundEffectGrid";
import { type UnusableReason } from "../livekit/backgroundImages";
export interface BackgroundImageRefusal {
reason: UnusableReason | "not-kept";
}
/** A background effect on offer, which the view names. */
export interface BackgroundEffectChoice {
@@ -91,6 +96,8 @@ export interface FooterState {
backgroundEffectNotice: "unavailable" | "slow" | undefined;
/** Whether the first effect chosen is still being prepared. */
backgroundEffectSettling: boolean;
/** Why the last file offered couldn't be kept, if it couldn't. */
backgroundImageRefusal: BackgroundImageRefusal | undefined;
showFooter: boolean;
/* This is needed for WindowMode = "flat" */
@@ -179,6 +186,21 @@ export const CallFooter: FC<FooterProps> = ({
const backgroundEffectNotice = useBehavior(vm.backgroundEffectNotice$);
const backgroundEffectSettling = useBehavior(vm.backgroundEffectSettling$);
const addBackgroundImage = useBehavior(vm.addBackgroundImage$);
const refusal = useBehavior(vm.backgroundImageRefusal$);
const refusalMessage = useMemo(() => {
switch (refusal?.reason) {
case undefined:
return undefined;
case "not-an-image":
return { text: t("error.background_not_an_image") };
case "animated":
return { text: t("error.background_animated") };
case "undecodable":
return { text: t("error.background_undecodable") };
case "not-kept":
return { text: t("error.background_not_kept") };
}
}, [refusal, t]);
const backgroundEffects = useBackgroundEffectLabels(
useBehavior(vm.backgroundEffects$),
);
@@ -248,6 +270,7 @@ export const CallFooter: FC<FooterProps> = ({
onSelectBackgroundEffect={selectBackgroundEffect}
backgroundEffectSettling={backgroundEffectSettling}
onAddBackgroundImage={addBackgroundImage}
backgroundImageRefusal={refusalMessage}
backgroundEffectNotice={
backgroundEffectNotice === "unavailable"
? t("background_effects.unavailable")