From d5bebcc3a538d4db5e8caf584ab024951e41be79 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 18 May 2026 18:11:27 +0200 Subject: [PATCH] refactor video blur toggle --- locales/en/app.json | 1 + src/components/CallFooter.stories.tsx | 3 +- src/components/CallFooter.tsx | 10 ++-- src/components/CallFooterViewModel.tsx | 50 +++++-------------- .../MediaMuteAndSwitchButton.test.tsx | 11 ++-- src/components/MediaMuteAndSwitchButton.tsx | 34 +++++++------ .../MediaMuteAndSwitchButton.test.tsx.snap | 27 +++++++++- 7 files changed, 71 insertions(+), 65 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index b51c6ed9..a14663e9 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -3,6 +3,7 @@ "user_menu": "User menu" }, "action": { + "blur_background": "Blur background", "close": "Close", "copy_link": "Copy link", "edit": "Edit", diff --git a/src/components/CallFooter.stories.tsx b/src/components/CallFooter.stories.tsx index f46f656f..2c78c823 100644 --- a/src/components/CallFooter.stories.tsx +++ b/src/components/CallFooter.stories.tsx @@ -79,6 +79,8 @@ export const Default: Story = { toggleAudio: fn(), toggleVideo: fn(), toggleScreenSharing: fn(), + toggleBlur: fn(), + videoBlurEnabled: true, hangup: fn(), buttonSize: "lg", showFooter: true, @@ -234,7 +236,6 @@ export const Pip: Story = { args: { ...Default.args, buttonSize: "md", - showSettingsButton: false, layoutMode: undefined, }, play: async ({ args, canvasElement }) => { diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index dfec25a9..051da6be 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -32,7 +32,6 @@ import { type GridMode } from "../state/CallViewModel/CallViewModel"; import { MediaMuteAndSwitchButton, type MenuOptions, - type ToggleOption, } from "./MediaMuteAndSwitchButton"; import { type ViewModel } from "../state/ViewModel"; import { useBehavior } from "../useBehavior"; @@ -61,6 +60,7 @@ export interface FooterActions { toggleAudio: (() => void) | undefined; /** Also controls if the videoMute button is disabled */ toggleVideo: (() => void) | undefined; + toggleBlur: (() => void) | undefined; /** Also controls if the layout button is visible */ setLayoutMode: ((mode: GridMode) => void) | undefined; toggleScreenSharing: (() => void) | undefined; @@ -73,6 +73,7 @@ export interface FooterActions { export interface FooterState { audioEnabled: boolean; videoEnabled: boolean; + videoBlurEnabled: boolean; showFooter: boolean; /* This is needed for WindowMode = "flat" */ @@ -106,7 +107,6 @@ export interface FooterState { selectedVideo: string | undefined; selectAudioButtonOption: ((deviceId: string) => void) | undefined; selectVideoButtonOption: ((option: string) => void) | undefined; - videoToggles: ToggleOption[]; } export interface FooterProps { @@ -139,7 +139,8 @@ export const CallFooter: FC = ({ ref, children, vm }) => { const selectedAudio = useBehavior(vm.selectedAudio$); const selectAudioButtonOption = useBehavior(vm.selectAudioButtonOption$); const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$); - const videoToggles = useBehavior(vm.videoToggles$); + const toggleBlur = useBehavior(vm.toggleBlur$); + const videoBlurEnabled = useBehavior(vm.videoBlurEnabled$); const buttonSize = useBehavior(vm.buttonSize$); const showLogo = useBehavior(vm.showLogo$); @@ -195,9 +196,10 @@ export const CallFooter: FC = ({ ref, children, vm }) => { enabled={videoEnabled ?? false} onMuteClick={toggleVideo} options={videoOptions} - toggles={videoToggles} selectedOption={selectedVideo} onSelect={selectVideoButtonOption} + backgroundBlurToggleClick={toggleBlur} + videoBlurEnabled={videoBlurEnabled} />, ); } else { diff --git a/src/components/CallFooterViewModel.tsx b/src/components/CallFooterViewModel.tsx index 4be52f0d..a7aec8d5 100644 --- a/src/components/CallFooterViewModel.tsx +++ b/src/components/CallFooterViewModel.tsx @@ -9,10 +9,7 @@ import { combineLatest, map, switchMap } from "rxjs"; import { supportsBackgroundProcessors } from "@livekit/track-processors"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; -import { - type MenuOptions, - type ToggleOption, -} from "./MediaMuteAndSwitchButton"; +import { type MenuOptions } from "./MediaMuteAndSwitchButton"; import { type MediaDevices } from "../state/MediaDevices"; import { mediaDeviceLabelToString } from "../settings/DeviceSelection"; import { @@ -67,7 +64,8 @@ function buildDeviceBehaviors( | "videoOptions$" | "selectedVideo$" | "selectVideoButtonOption$" - | "videoToggles$" + | "toggleBlur$" + | "videoBlurEnabled$" > { return { audioOptions$: scope.behavior( @@ -115,40 +113,18 @@ function buildDeviceBehaviors( selectedVideo$: scope.behavior( mediaDevices.videoInput.selected$.pipe(map((s) => s?.id)), ), - selectVideoButtonOption$: scope.behavior( - backgroundBlurSettings.value$.pipe( - map((current) => { - return (option: string) => { - if (option === "blur") { - backgroundBlurSettings.setValue(!current); - } else { - mediaDevices.videoInput.select(option); - } - }; + selectVideoButtonOption$: constant(mediaDevices.videoInput.select), + toggleBlur$: scope.behavior( + combineLatest([backgroundBlurSettings.value$, disableSwitcher$]).pipe( + map(([current, switcherDisabled]) => { + return () => + !switcherDisabled && supportsBackgroundProcessors() + ? (): void => backgroundBlurSettings.setValue(!current) + : constant(undefined); }), ), ), - videoToggles$: scope.behavior( - disableSwitcher$.pipe( - switchMap((disable) => - disable - ? constant([] as ToggleOption[]) - : backgroundBlurSettings.value$.pipe( - map((blurActive) => - supportsBackgroundProcessors() - ? [ - { - id: "blur", - enabled: blurActive, - label: "Blur Background", - }, - ] - : [], - ), - ), - ), - ), - ), + videoBlurEnabled$: backgroundBlurSettings.value$, }; } @@ -278,7 +254,6 @@ export function createLobbyFooterViewModel( openSettings, hangup, debugTileLayout: false, - showSettingsButton: openSettings !== undefined, showFooter: true, toggleAudio: undefined, toggleVideo: undefined, @@ -298,7 +273,6 @@ export function createLobbyFooterViewModel( selectedVideo: undefined, selectAudioButtonOption: undefined, selectVideoButtonOption: undefined, - videoToggles: undefined, }), ...buildMuteBehaviors(scope, muteStates), ...buildDeviceBehaviors(scope, mediaDevices, constant(false)), diff --git a/src/components/MediaMuteAndSwitchButton.test.tsx b/src/components/MediaMuteAndSwitchButton.test.tsx index 44bc29a3..5dd3c2d4 100644 --- a/src/components/MediaMuteAndSwitchButton.test.tsx +++ b/src/components/MediaMuteAndSwitchButton.test.tsx @@ -17,7 +17,7 @@ describe("MediaMuteAndSwitchButton", () => { test("renders", () => { const { container } = render( - + , ); expect(container).toMatchSnapshot(); @@ -187,15 +187,14 @@ describe("MediaMuteAndSwitchButton", () => { test("renders menu with toggle control and calls toggle callback", async () => { const user = userEvent.setup(); const onSelect = vi.fn(); + const onVideoBlurToggle = vi.fn(); const { getByRole } = render( , @@ -204,14 +203,14 @@ describe("MediaMuteAndSwitchButton", () => { await user.click(getByRole("button", { name: "Microphone" })); const toggle = screen.getByRole("menuitemcheckbox", { - name: "Background blur", + name: "Blur background", }); expect(toggle).toBeInTheDocument(); expect(toggle).toHaveAttribute("aria-checked", "false"); await user.click(toggle); - expect(onSelect).toHaveBeenCalledWith("bg_blur"); + expect(onVideoBlurToggle).toHaveBeenCalled(); }); test("renders check icon to mark the selected menu item", async () => { diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx index 4a6737f7..c9c8a50d 100644 --- a/src/components/MediaMuteAndSwitchButton.tsx +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -30,11 +30,6 @@ export interface MenuOptions { label: string; id: string; } -export interface ToggleOption { - label: string; - enabled: boolean; - id: string; -} export interface MediaMuteAndSwitchButtonProps { /** The title used in the Switcher modal. */ @@ -48,12 +43,8 @@ export interface MediaMuteAndSwitchButtonProps { options?: MenuOptions[]; /** The option that will currently be rendered as the selected option */ selectedOption?: string; - /** - * The available toggles (including there current state) - * The toggle state is not stored by this component. - * It is handled externally and needs to be set by listening to the `onSelect` callback and setting the right toggle item to `enabled` - */ - toggles?: ToggleOption[]; + backgroundBlurToggleClick?: () => void; + videoBlurEnabled?: boolean; /** * For any toggle and option this method will be called. * So toggles need to be implemented by listening here and setting the right toggle item to `enabled` @@ -61,6 +52,8 @@ export interface MediaMuteAndSwitchButtonProps { onSelect?: (id: string) => void; } +const BLUR_ID = "blur"; + export const MediaMuteAndSwitchButton: FC = ({ title, enabled, @@ -68,13 +61,24 @@ export const MediaMuteAndSwitchButton: FC = ({ iconsAndLabels, options, selectedOption, - toggles, + videoBlurEnabled, + backgroundBlurToggleClick, onSelect, }) => { const [plannedSelection, setPlannedSelection] = useState(null); const [menuOpen, setMenuOpen] = useState(false); let button; + let toggles = + backgroundBlurToggleClick === undefined + ? [] + : [ + { + label: t("action.blur_background"), + enabled: videoBlurEnabled, + id: BLUR_ID, + }, + ]; switch (iconsAndLabels) { case "video": button = ( @@ -89,6 +93,7 @@ export const MediaMuteAndSwitchButton: FC = ({ data-testid="incall_videomute" /> ); + toggles = []; break; case "audio": button = ( @@ -103,7 +108,6 @@ export const MediaMuteAndSwitchButton: FC = ({ data-testid="incall_mute" /> ); - break; } @@ -182,10 +186,10 @@ export const MediaMuteAndSwitchButton: FC = ({ { - onSelect?.(toggle.id); + backgroundBlurToggleClick?.(); e.preventDefault(); }} - checked={toggle.enabled} + checked={toggle.enabled ?? false} key={toggle.id} /> ))} diff --git a/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap index 6558f254..ed8d2931 100644 --- a/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap +++ b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap @@ -5,15 +5,40 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
+