mirror of
https://github.com/element-hq/element-call.git
synced 2026-05-20 07:15:21 +00:00
refactor video blur toggle
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
"user_menu": "User menu"
|
||||
},
|
||||
"action": {
|
||||
"blur_background": "Blur background",
|
||||
"close": "Close",
|
||||
"copy_link": "Copy link",
|
||||
"edit": "Edit",
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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<FooterProps> = ({ 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<FooterProps> = ({ ref, children, vm }) => {
|
||||
enabled={videoEnabled ?? false}
|
||||
onMuteClick={toggleVideo}
|
||||
options={videoOptions}
|
||||
toggles={videoToggles}
|
||||
selectedOption={selectedVideo}
|
||||
onSelect={selectVideoButtonOption}
|
||||
backgroundBlurToggleClick={toggleBlur}
|
||||
videoBlurEnabled={videoBlurEnabled}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
test("renders", () => {
|
||||
const { container } = render(
|
||||
<TooltipProvider>
|
||||
<MediaMuteAndSwitchButton title={"Switcher"} />
|
||||
<MediaMuteAndSwitchButton title={"Switcher"} iconsAndLabels={"audio"} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
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(
|
||||
<TooltipProvider>
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
toggles={[
|
||||
{ label: "Background blur", id: "bg_blur", enabled: false },
|
||||
]}
|
||||
backgroundBlurToggleClick={onVideoBlurToggle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</TooltipProvider>,
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<MediaMuteAndSwitchButtonProps> = ({
|
||||
title,
|
||||
enabled,
|
||||
@@ -68,13 +61,24 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
iconsAndLabels,
|
||||
options,
|
||||
selectedOption,
|
||||
toggles,
|
||||
videoBlurEnabled,
|
||||
backgroundBlurToggleClick,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(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<MediaMuteAndSwitchButtonProps> = ({
|
||||
data-testid="incall_videomute"
|
||||
/>
|
||||
);
|
||||
toggles = [];
|
||||
break;
|
||||
case "audio":
|
||||
button = (
|
||||
@@ -103,7 +108,6 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
data-testid="incall_mute"
|
||||
/>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -182,10 +186,10 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
<ToggleMenuItem
|
||||
label={toggle.label}
|
||||
onSelect={(e) => {
|
||||
onSelect?.(toggle.id);
|
||||
backgroundBlurToggleClick?.();
|
||||
e.preventDefault();
|
||||
}}
|
||||
checked={toggle.enabled}
|
||||
checked={toggle.enabled ?? false}
|
||||
key={toggle.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -5,15 +5,40 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-disabled="true"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-testid="incall_mute"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 8v-.006l6.831 6.832-.002.002 1.414 1.415.003-.003 1.414 1.414-.003.003L20.5 20.5a1 1 0 0 1-1.414 1.414l-3.022-3.022A7.95 7.95 0 0 1 13 19.938V21a1 1 0 0 1-2 0v-1.062A8 8 0 0 1 4 12a1 1 0 1 1 2 0 6 6 0 0 0 8.587 5.415l-1.55-1.55A4.005 4.005 0 0 1 8 12v-1.172L2.086 4.914A1 1 0 0 1 3.5 3.5zm9.417 6.583 1.478 1.477A7.96 7.96 0 0 0 20 12a1 1 0 0 0-2 0c0 .925-.21 1.8-.583 2.583M8.073 5.238l7.793 7.793q.132-.495.134-1.031V6a4 4 0 0 0-7.927-.762"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Microphone"
|
||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
data-state="closed"
|
||||
id="radix-_r_0_"
|
||||
id="radix-_r_5_"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user