mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Wire the background effects grid to the pipeline
- Footer view model exposes the chosen effect and a setter for it - The footer names the catalogue: a view model holds no translated text - The camera menu shows the grid in place of the blur toggle - Shipped images are numbered, not named, since the art is a stand-in - Withholding the setter is what renders the section disabled, as with the speaker section where no output can be chosen Exploration for FEATURES_SPEC/2026-09_Background_Effects.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"add_background_image": "Add image",
|
"add_background_image": "Add image",
|
||||||
|
"background_effect_blur": "Blur",
|
||||||
|
"background_effect_none": "None",
|
||||||
|
"background_effect_numbered": "Background {{n}}",
|
||||||
"blur_background": "Blur background",
|
"blur_background": "Blur background",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export const Default: Story = {
|
|||||||
toggleScreenSharing: fn(),
|
toggleScreenSharing: fn(),
|
||||||
toggleBlur: fn(),
|
toggleBlur: fn(),
|
||||||
videoBlurEnabled: true,
|
videoBlurEnabled: true,
|
||||||
|
backgroundEffect: "none",
|
||||||
|
selectBackgroundEffect: fn(),
|
||||||
hangup: fn(),
|
hangup: fn(),
|
||||||
buttonSize: "lg",
|
buttonSize: "lg",
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import LogoMark from "../icons/LogoMark.svg?react";
|
import LogoMark from "../icons/LogoMark.svg?react";
|
||||||
import LogoType from "../icons/LogoType.svg?react";
|
import LogoType from "../icons/LogoType.svg?react";
|
||||||
@@ -23,9 +24,11 @@ import {
|
|||||||
} from "../button";
|
} from "../button";
|
||||||
import styles from "./CallFooter.module.css";
|
import styles from "./CallFooter.module.css";
|
||||||
import {
|
import {
|
||||||
|
type BackgroundEffectOption,
|
||||||
MediaMuteAndSwitchButton,
|
MediaMuteAndSwitchButton,
|
||||||
type MenuOptions,
|
type MenuOptions,
|
||||||
} from "./MediaMuteAndSwitchButton";
|
} from "./MediaMuteAndSwitchButton";
|
||||||
|
import { shippedBackgrounds } from "../livekit/backgroundEffects";
|
||||||
import { type Behavior } from "../state/Behavior";
|
import { type Behavior } from "../state/Behavior";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
import { type ViewModel } from "../state/ViewModel";
|
||||||
import { useBehavior } from "../useBehavior";
|
import { useBehavior } from "../useBehavior";
|
||||||
@@ -57,6 +60,8 @@ export interface FooterActions {
|
|||||||
/** Also controls if the videoMute button is disabled */
|
/** Also controls if the videoMute button is disabled */
|
||||||
toggleVideo: (() => void) | undefined;
|
toggleVideo: (() => void) | undefined;
|
||||||
toggleBlur: (() => void) | undefined;
|
toggleBlur: (() => void) | undefined;
|
||||||
|
/** Undefined where background processing is unavailable. */
|
||||||
|
selectBackgroundEffect: ((id: string) => void) | undefined;
|
||||||
toggleScreenSharing: (() => void) | undefined;
|
toggleScreenSharing: (() => void) | undefined;
|
||||||
/** Also controls if the settings button is visible */
|
/** Also controls if the settings button is visible */
|
||||||
openSettings: (() => void) | undefined;
|
openSettings: (() => void) | undefined;
|
||||||
@@ -70,6 +75,8 @@ export interface FooterState {
|
|||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
videoBusy: boolean;
|
videoBusy: boolean;
|
||||||
videoBlurEnabled: boolean;
|
videoBlurEnabled: boolean;
|
||||||
|
/** The chosen background effect, in its stored form. */
|
||||||
|
backgroundEffect: string;
|
||||||
showFooter: boolean;
|
showFooter: boolean;
|
||||||
|
|
||||||
/* This is needed for WindowMode = "flat" */
|
/* This is needed for WindowMode = "flat" */
|
||||||
@@ -123,6 +130,7 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
children,
|
children,
|
||||||
vm,
|
vm,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const asOverlay = useBehavior(vm.asOverlay$);
|
const asOverlay = useBehavior(vm.asOverlay$);
|
||||||
const showFooter = useBehavior(vm.showFooter$);
|
const showFooter = useBehavior(vm.showFooter$);
|
||||||
const hideControls = useBehavior(vm.hideControls$);
|
const hideControls = useBehavior(vm.hideControls$);
|
||||||
@@ -151,8 +159,26 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
const selectedAudioOutput = useBehavior(vm.selectedAudioOutput$);
|
const selectedAudioOutput = useBehavior(vm.selectedAudioOutput$);
|
||||||
const selectAudioOutputOption = useBehavior(vm.selectAudioOutputOption$);
|
const selectAudioOutputOption = useBehavior(vm.selectAudioOutputOption$);
|
||||||
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
|
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
|
||||||
const toggleBlur = useBehavior(vm.toggleBlur$);
|
const backgroundEffect = useBehavior(vm.backgroundEffect$);
|
||||||
const videoBlurEnabled = useBehavior(vm.videoBlurEnabled$);
|
const selectBackgroundEffect = useBehavior(vm.selectBackgroundEffect$);
|
||||||
|
|
||||||
|
// The catalogue is named here rather than in the view model: the names are
|
||||||
|
// for reading, and a view model has no business holding translated text.
|
||||||
|
const backgroundEffects = useMemo(
|
||||||
|
(): BackgroundEffectOption[] => [
|
||||||
|
{ id: "none", kind: "none", label: t("action.background_effect_none") },
|
||||||
|
{ id: "blur", kind: "blur", label: t("action.background_effect_blur") },
|
||||||
|
...shippedBackgrounds.map((background, i) => ({
|
||||||
|
id: `image:${background.id}`,
|
||||||
|
kind: "image" as const,
|
||||||
|
// Numbered rather than named: the images are stand-ins, and naming
|
||||||
|
// them here would invent names the design has not given them.
|
||||||
|
label: t("action.background_effect_numbered", { n: i + 1 }),
|
||||||
|
imageUrl: background.imagePath,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
const buttonSize = useBehavior(vm.buttonSize$);
|
const buttonSize = useBehavior(vm.buttonSize$);
|
||||||
const showLogo = useBehavior(vm.showLogo$);
|
const showLogo = useBehavior(vm.showLogo$);
|
||||||
|
|
||||||
@@ -214,8 +240,9 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
options={videoOptions}
|
options={videoOptions}
|
||||||
selectedOption={selectedVideo}
|
selectedOption={selectedVideo}
|
||||||
onSelect={selectVideoButtonOption}
|
onSelect={selectVideoButtonOption}
|
||||||
videoBlurToggleClick={toggleBlur}
|
backgroundEffects={backgroundEffects}
|
||||||
videoBlurEnabled={videoBlurEnabled}
|
selectedBackgroundEffect={backgroundEffect}
|
||||||
|
onSelectBackgroundEffect={selectBackgroundEffect}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { type MenuOptions } from "./MediaMuteAndSwitchButton";
|
|||||||
import { type MediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices } from "../state/MediaDevices";
|
||||||
import {
|
import {
|
||||||
backgroundBlur as backgroundBlurSettings,
|
backgroundBlur as backgroundBlurSettings,
|
||||||
|
backgroundEffect as backgroundEffectSetting,
|
||||||
debugTileLayout as debugTileLayoutSetting,
|
debugTileLayout as debugTileLayoutSetting,
|
||||||
} from "../settings/settings";
|
} from "../settings/settings";
|
||||||
import { type Behavior, constant } from "../state/Behavior";
|
import { type Behavior, constant } from "../state/Behavior";
|
||||||
@@ -76,6 +77,8 @@ function buildDeviceBehaviors(
|
|||||||
| "selectVideoButtonOption$"
|
| "selectVideoButtonOption$"
|
||||||
| "toggleBlur$"
|
| "toggleBlur$"
|
||||||
| "videoBlurEnabled$"
|
| "videoBlurEnabled$"
|
||||||
|
| "backgroundEffect$"
|
||||||
|
| "selectBackgroundEffect$"
|
||||||
> {
|
> {
|
||||||
const options$ = (
|
const options$ = (
|
||||||
available$: Behavior<Map<string, MenuOptions["label"]>>,
|
available$: Behavior<Map<string, MenuOptions["label"]>>,
|
||||||
@@ -128,6 +131,20 @@ function buildDeviceBehaviors(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
videoBlurEnabled$: backgroundBlurSettings.value$,
|
videoBlurEnabled$: backgroundBlurSettings.value$,
|
||||||
|
backgroundEffect$: backgroundEffectSetting.value$,
|
||||||
|
// Withholding the callback is what renders the section disabled, the same
|
||||||
|
// way the speaker section is disabled where no output can be chosen.
|
||||||
|
selectBackgroundEffect$: scope.behavior(
|
||||||
|
disableSwitcher$.pipe(
|
||||||
|
map((switcherDisabled) =>
|
||||||
|
!switcherDisabled && supportsBackgroundProcessors()
|
||||||
|
? (id: string): void => {
|
||||||
|
backgroundEffectSetting.setValue(id);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user