mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-27 19:29:19 +00:00
refactor video blur toggle
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"user_menu": "User menu"
|
"user_menu": "User menu"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
|
"blur_background": "Blur background",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export const Default: Story = {
|
|||||||
toggleAudio: fn(),
|
toggleAudio: fn(),
|
||||||
toggleVideo: fn(),
|
toggleVideo: fn(),
|
||||||
toggleScreenSharing: fn(),
|
toggleScreenSharing: fn(),
|
||||||
|
toggleBlur: fn(),
|
||||||
|
videoBlurEnabled: true,
|
||||||
hangup: fn(),
|
hangup: fn(),
|
||||||
buttonSize: "lg",
|
buttonSize: "lg",
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
@@ -234,7 +236,6 @@ export const Pip: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Default.args,
|
...Default.args,
|
||||||
buttonSize: "md",
|
buttonSize: "md",
|
||||||
showSettingsButton: false,
|
|
||||||
layoutMode: undefined,
|
layoutMode: undefined,
|
||||||
},
|
},
|
||||||
play: async ({ args, canvasElement }) => {
|
play: async ({ args, canvasElement }) => {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
|||||||
import {
|
import {
|
||||||
MediaMuteAndSwitchButton,
|
MediaMuteAndSwitchButton,
|
||||||
type MenuOptions,
|
type MenuOptions,
|
||||||
type ToggleOption,
|
|
||||||
} from "./MediaMuteAndSwitchButton";
|
} from "./MediaMuteAndSwitchButton";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
import { type ViewModel } from "../state/ViewModel";
|
||||||
import { useBehavior } from "../useBehavior";
|
import { useBehavior } from "../useBehavior";
|
||||||
@@ -61,6 +60,7 @@ export interface FooterActions {
|
|||||||
toggleAudio: (() => void) | undefined;
|
toggleAudio: (() => void) | undefined;
|
||||||
/** 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;
|
||||||
/** Also controls if the layout button is visible */
|
/** Also controls if the layout button is visible */
|
||||||
setLayoutMode: ((mode: GridMode) => void) | undefined;
|
setLayoutMode: ((mode: GridMode) => void) | undefined;
|
||||||
toggleScreenSharing: (() => void) | undefined;
|
toggleScreenSharing: (() => void) | undefined;
|
||||||
@@ -73,6 +73,7 @@ export interface FooterActions {
|
|||||||
export interface FooterState {
|
export interface FooterState {
|
||||||
audioEnabled: boolean;
|
audioEnabled: boolean;
|
||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
|
videoBlurEnabled: boolean;
|
||||||
showFooter: boolean;
|
showFooter: boolean;
|
||||||
|
|
||||||
/* This is needed for WindowMode = "flat" */
|
/* This is needed for WindowMode = "flat" */
|
||||||
@@ -106,7 +107,6 @@ export interface FooterState {
|
|||||||
selectedVideo: string | undefined;
|
selectedVideo: string | undefined;
|
||||||
selectAudioButtonOption: ((deviceId: string) => void) | undefined;
|
selectAudioButtonOption: ((deviceId: string) => void) | undefined;
|
||||||
selectVideoButtonOption: ((option: string) => void) | undefined;
|
selectVideoButtonOption: ((option: string) => void) | undefined;
|
||||||
videoToggles: ToggleOption[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FooterProps {
|
export interface FooterProps {
|
||||||
@@ -139,7 +139,8 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
|||||||
const selectedAudio = useBehavior(vm.selectedAudio$);
|
const selectedAudio = useBehavior(vm.selectedAudio$);
|
||||||
const selectAudioButtonOption = useBehavior(vm.selectAudioButtonOption$);
|
const selectAudioButtonOption = useBehavior(vm.selectAudioButtonOption$);
|
||||||
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
|
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 buttonSize = useBehavior(vm.buttonSize$);
|
||||||
const showLogo = useBehavior(vm.showLogo$);
|
const showLogo = useBehavior(vm.showLogo$);
|
||||||
|
|
||||||
@@ -195,9 +196,10 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
|||||||
enabled={videoEnabled ?? false}
|
enabled={videoEnabled ?? false}
|
||||||
onMuteClick={toggleVideo}
|
onMuteClick={toggleVideo}
|
||||||
options={videoOptions}
|
options={videoOptions}
|
||||||
toggles={videoToggles}
|
|
||||||
selectedOption={selectedVideo}
|
selectedOption={selectedVideo}
|
||||||
onSelect={selectVideoButtonOption}
|
onSelect={selectVideoButtonOption}
|
||||||
|
backgroundBlurToggleClick={toggleBlur}
|
||||||
|
videoBlurEnabled={videoBlurEnabled}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import { combineLatest, map, switchMap } from "rxjs";
|
|||||||
import { supportsBackgroundProcessors } from "@livekit/track-processors";
|
import { supportsBackgroundProcessors } from "@livekit/track-processors";
|
||||||
|
|
||||||
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||||
import {
|
import { type MenuOptions } from "./MediaMuteAndSwitchButton";
|
||||||
type MenuOptions,
|
|
||||||
type ToggleOption,
|
|
||||||
} from "./MediaMuteAndSwitchButton";
|
|
||||||
import { type MediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices } from "../state/MediaDevices";
|
||||||
import { mediaDeviceLabelToString } from "../settings/DeviceSelection";
|
import { mediaDeviceLabelToString } from "../settings/DeviceSelection";
|
||||||
import {
|
import {
|
||||||
@@ -67,7 +64,8 @@ function buildDeviceBehaviors(
|
|||||||
| "videoOptions$"
|
| "videoOptions$"
|
||||||
| "selectedVideo$"
|
| "selectedVideo$"
|
||||||
| "selectVideoButtonOption$"
|
| "selectVideoButtonOption$"
|
||||||
| "videoToggles$"
|
| "toggleBlur$"
|
||||||
|
| "videoBlurEnabled$"
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
audioOptions$: scope.behavior(
|
audioOptions$: scope.behavior(
|
||||||
@@ -115,40 +113,18 @@ function buildDeviceBehaviors(
|
|||||||
selectedVideo$: scope.behavior(
|
selectedVideo$: scope.behavior(
|
||||||
mediaDevices.videoInput.selected$.pipe(map((s) => s?.id)),
|
mediaDevices.videoInput.selected$.pipe(map((s) => s?.id)),
|
||||||
),
|
),
|
||||||
selectVideoButtonOption$: scope.behavior(
|
selectVideoButtonOption$: constant(mediaDevices.videoInput.select),
|
||||||
backgroundBlurSettings.value$.pipe(
|
toggleBlur$: scope.behavior(
|
||||||
map((current) => {
|
combineLatest([backgroundBlurSettings.value$, disableSwitcher$]).pipe(
|
||||||
return (option: string) => {
|
map(([current, switcherDisabled]) => {
|
||||||
if (option === "blur") {
|
return () =>
|
||||||
backgroundBlurSettings.setValue(!current);
|
!switcherDisabled && supportsBackgroundProcessors()
|
||||||
} else {
|
? (): void => backgroundBlurSettings.setValue(!current)
|
||||||
mediaDevices.videoInput.select(option);
|
: constant(undefined);
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
videoToggles$: scope.behavior(
|
videoBlurEnabled$: backgroundBlurSettings.value$,
|
||||||
disableSwitcher$.pipe(
|
|
||||||
switchMap((disable) =>
|
|
||||||
disable
|
|
||||||
? constant([] as ToggleOption[])
|
|
||||||
: backgroundBlurSettings.value$.pipe(
|
|
||||||
map((blurActive) =>
|
|
||||||
supportsBackgroundProcessors()
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: "blur",
|
|
||||||
enabled: blurActive,
|
|
||||||
label: "Blur Background",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +254,6 @@ export function createLobbyFooterViewModel(
|
|||||||
openSettings,
|
openSettings,
|
||||||
hangup,
|
hangup,
|
||||||
debugTileLayout: false,
|
debugTileLayout: false,
|
||||||
showSettingsButton: openSettings !== undefined,
|
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
toggleAudio: undefined,
|
toggleAudio: undefined,
|
||||||
toggleVideo: undefined,
|
toggleVideo: undefined,
|
||||||
@@ -298,7 +273,6 @@ export function createLobbyFooterViewModel(
|
|||||||
selectedVideo: undefined,
|
selectedVideo: undefined,
|
||||||
selectAudioButtonOption: undefined,
|
selectAudioButtonOption: undefined,
|
||||||
selectVideoButtonOption: undefined,
|
selectVideoButtonOption: undefined,
|
||||||
videoToggles: undefined,
|
|
||||||
}),
|
}),
|
||||||
...buildMuteBehaviors(scope, muteStates),
|
...buildMuteBehaviors(scope, muteStates),
|
||||||
...buildDeviceBehaviors(scope, mediaDevices, constant(false)),
|
...buildDeviceBehaviors(scope, mediaDevices, constant(false)),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
test("renders", () => {
|
test("renders", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<MediaMuteAndSwitchButton title={"Switcher"} />
|
<MediaMuteAndSwitchButton title={"Switcher"} iconsAndLabels={"audio"} />
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -187,15 +187,14 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
test("renders menu with toggle control and calls toggle callback", async () => {
|
test("renders menu with toggle control and calls toggle callback", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
const onVideoBlurToggle = vi.fn();
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<MediaMuteAndSwitchButton
|
<MediaMuteAndSwitchButton
|
||||||
title="Switcher"
|
title="Switcher"
|
||||||
iconsAndLabels="audio"
|
iconsAndLabels="audio"
|
||||||
enabled={true}
|
enabled={true}
|
||||||
toggles={[
|
backgroundBlurToggleClick={onVideoBlurToggle}
|
||||||
{ label: "Background blur", id: "bg_blur", enabled: false },
|
|
||||||
]}
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
@@ -204,14 +203,14 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
await user.click(getByRole("button", { name: "Microphone" }));
|
||||||
|
|
||||||
const toggle = screen.getByRole("menuitemcheckbox", {
|
const toggle = screen.getByRole("menuitemcheckbox", {
|
||||||
name: "Background blur",
|
name: "Blur background",
|
||||||
});
|
});
|
||||||
expect(toggle).toBeInTheDocument();
|
expect(toggle).toBeInTheDocument();
|
||||||
expect(toggle).toHaveAttribute("aria-checked", "false");
|
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||||
|
|
||||||
await user.click(toggle);
|
await user.click(toggle);
|
||||||
|
|
||||||
expect(onSelect).toHaveBeenCalledWith("bg_blur");
|
expect(onVideoBlurToggle).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders check icon to mark the selected menu item", async () => {
|
test("renders check icon to mark the selected menu item", async () => {
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ export interface MenuOptions {
|
|||||||
label: string;
|
label: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface ToggleOption {
|
|
||||||
label: string;
|
|
||||||
enabled: boolean;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MediaMuteAndSwitchButtonProps {
|
export interface MediaMuteAndSwitchButtonProps {
|
||||||
/** The title used in the Switcher modal. */
|
/** The title used in the Switcher modal. */
|
||||||
@@ -48,12 +43,8 @@ export interface MediaMuteAndSwitchButtonProps {
|
|||||||
options?: MenuOptions[];
|
options?: MenuOptions[];
|
||||||
/** The option that will currently be rendered as the selected option */
|
/** The option that will currently be rendered as the selected option */
|
||||||
selectedOption?: string;
|
selectedOption?: string;
|
||||||
/**
|
backgroundBlurToggleClick?: () => void;
|
||||||
* The available toggles (including there current state)
|
videoBlurEnabled?: boolean;
|
||||||
* 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[];
|
|
||||||
/**
|
/**
|
||||||
* For any toggle and option this method will be called.
|
* 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`
|
* 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;
|
onSelect?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BLUR_ID = "blur";
|
||||||
|
|
||||||
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||||
title,
|
title,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -68,13 +61,24 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
iconsAndLabels,
|
iconsAndLabels,
|
||||||
options,
|
options,
|
||||||
selectedOption,
|
selectedOption,
|
||||||
toggles,
|
videoBlurEnabled,
|
||||||
|
backgroundBlurToggleClick,
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
|
let toggles =
|
||||||
|
backgroundBlurToggleClick === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: t("action.blur_background"),
|
||||||
|
enabled: videoBlurEnabled,
|
||||||
|
id: BLUR_ID,
|
||||||
|
},
|
||||||
|
];
|
||||||
switch (iconsAndLabels) {
|
switch (iconsAndLabels) {
|
||||||
case "video":
|
case "video":
|
||||||
button = (
|
button = (
|
||||||
@@ -89,6 +93,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
data-testid="incall_videomute"
|
data-testid="incall_videomute"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
toggles = [];
|
||||||
break;
|
break;
|
||||||
case "audio":
|
case "audio":
|
||||||
button = (
|
button = (
|
||||||
@@ -103,7 +108,6 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
data-testid="incall_mute"
|
data-testid="incall_mute"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +186,10 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
<ToggleMenuItem
|
<ToggleMenuItem
|
||||||
label={toggle.label}
|
label={toggle.label}
|
||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
onSelect?.(toggle.id);
|
backgroundBlurToggleClick?.();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
checked={toggle.enabled}
|
checked={toggle.enabled ?? false}
|
||||||
key={toggle.id}
|
key={toggle.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,15 +5,40 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="container"
|
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
|
<button
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
|
aria-label="Microphone"
|
||||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||||
data-kind="tertiary"
|
data-kind="tertiary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_0_"
|
id="radix-_r_5_"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user