Make the settings blur control agree with the camera menu

- Both now read and write one setting, so they cannot disagree
- An image background reads as blur off there; turning blur on replaces
  the image, which is what FR-022 and EC-011 ask for
- The new setting defaults to whatever blur the user already had, so
  nobody loses the blur they turned on before this existed
- Covers the derivation with tests: what the checkbox shows, what it
  writes, and the fallback when a stored background is one we no longer
  ship

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-17 23:01:54 +02:00
co-authored by Claude Opus 5
parent 6a6605c233
commit 8a7eb5c7fd
3 changed files with 82 additions and 4 deletions
+65
View File
@@ -0,0 +1,65 @@
/*
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 { describe, expect, test } from "vitest";
import {
parseEffect,
serializeEffect,
shippedBackgrounds,
imagePathFor,
} from "./backgroundEffects";
describe("the chosen background effect", () => {
test("round-trips through its stored form", () => {
for (const effect of [
{ kind: "none" } as const,
{ kind: "blur" } as const,
{ kind: "image", id: shippedBackgrounds[0].id } as const,
])
expect(parseEffect(serializeEffect(effect))).toEqual(effect);
});
test("falls back to no effect when the stored form names nothing we ship", () => {
// A background removed between releases, or a value from a future one.
expect(parseEffect("image:a-background-we-no-longer-ship")).toEqual({
kind: "none",
});
expect(parseEffect("")).toEqual({ kind: "none" });
});
test("gives every shipped background an image to draw", () => {
for (const background of shippedBackgrounds)
expect(imagePathFor(background.id)).toBeTruthy();
});
});
describe("the blur control in settings", () => {
// The control is a checkbox over a setting that holds more than two states,
// so what it shows and what it writes are both derived. This is that
// derivation, which is what keeps it from disagreeing with the camera menu.
const shows = (stored: string): boolean =>
parseEffect(stored).kind === "blur";
const writes = (on: boolean): string =>
serializeEffect(on ? { kind: "blur" } : { kind: "none" });
test("reads as off while an image background is in force", () => {
expect(shows(serializeEffect({ kind: "image", id: "indoor" }))).toBe(false);
});
test("reads as on while blur is in force", () => {
expect(shows("blur")).toBe(true);
});
test("replaces an image background when it is turned on", () => {
expect(writes(true)).toBe("blur");
});
test("leaves no effect behind when it is turned off", () => {
expect(writes(false)).toBe("none");
});
});
+13 -3
View File
@@ -22,13 +22,17 @@ import { useHostBridge } from "../HostBridge";
import {
useSetting,
soundEffectVolume as soundEffectVolumeSetting,
backgroundBlur as backgroundBlurSetting,
backgroundEffect as backgroundEffectSetting,
developerMode,
} from "./settings";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider";
import { DeviceSelection } from "./DeviceSelection";
import { useTrackProcessor } from "../livekit/TrackProcessorContext";
import {
parseEffect,
serializeEffect,
} from "../livekit/backgroundEffects";
import {
DeveloperSettingsTab,
type DeveloperSettingsSnapshot,
@@ -83,7 +87,13 @@ export const SettingsModal: FC<Props> = ({
const BlurCheckbox: React.FC = (): ReactNode => {
const { supported } = useTrackProcessor();
const [blurActive, setBlurActive] = useSetting(backgroundBlurSetting);
// The same setting the camera menu's Background effects grid writes, so
// the two controls cannot disagree. An image background reads here as blur
// off, and turning blur on replaces it.
const [effect, setEffect] = useSetting(backgroundEffectSetting);
const blurActive = parseEffect(effect).kind === "blur";
const setBlurActive = (on: boolean): void =>
setEffect(serializeEffect(on ? { kind: "blur" } : { kind: "none" }));
return (
<>
@@ -97,7 +107,7 @@ export const SettingsModal: FC<Props> = ({
supported ? "" : t("settings.blur_not_supported_by_browser")
}
type="checkbox"
checked={!!blurActive}
checked={blurActive}
onChange={(b): void => setBlurActive(b.target.checked)}
disabled={!supported}
/>
+4 -1
View File
@@ -123,7 +123,10 @@ export const backgroundBlur = new Setting<boolean>("background-blur", false);
*/
export const backgroundEffect = new Setting<string>(
"background-effect",
"none",
// Defaults to whatever blur the user had already chosen, so someone who
// turned blur on before this existed still has it afterwards. Their own
// choice replaces this the moment they make one.
backgroundBlur.getValue() ? "blur" : "none",
);
export const showHandRaisedTimer = new Setting<boolean>(