mirror of
https://github.com/vector-im/element-call.git
synced 2026-06-30 18:02:56 +00:00
feat(mute): add syncing state and disable toggle during async mute
This commit is contained in:
@@ -80,7 +80,9 @@ export const Default: Story = {
|
||||
showLogo: false,
|
||||
layoutMode: "grid",
|
||||
audioEnabled: true,
|
||||
audioBusy: false,
|
||||
videoEnabled: true,
|
||||
videoBusy: false,
|
||||
setLayoutMode: fn(),
|
||||
openSettings: fn(),
|
||||
toggleAudio: fn(),
|
||||
|
||||
@@ -72,7 +72,9 @@ export interface FooterActions {
|
||||
// we do not use any ? optional properties so that the vm type is including all fields.
|
||||
export interface FooterState {
|
||||
audioEnabled: boolean;
|
||||
audioBusy: boolean;
|
||||
videoEnabled: boolean;
|
||||
videoBusy: boolean;
|
||||
videoBlurEnabled: boolean;
|
||||
showFooter: boolean;
|
||||
|
||||
@@ -122,7 +124,9 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
const setLayoutMode = useBehavior(vm.setLayoutMode$);
|
||||
const openSettings = useBehavior(vm.openSettings$);
|
||||
const audioEnabled = useBehavior(vm.audioEnabled$);
|
||||
const audioBusy = useBehavior(vm.audioBusy$);
|
||||
const videoEnabled = useBehavior(vm.videoEnabled$);
|
||||
const videoBusy = useBehavior(vm.videoBusy$);
|
||||
const toggleAudio = useBehavior(vm.toggleAudio$);
|
||||
const toggleVideo = useBehavior(vm.toggleVideo$);
|
||||
const sharingScreen = useBehavior(vm.sharingScreen$);
|
||||
@@ -167,6 +171,7 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
key="audio"
|
||||
iconsAndLabels="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
busy={audioBusy ?? false}
|
||||
onMuteClick={toggleAudio}
|
||||
data-testid="incall_mute"
|
||||
options={audioOptions}
|
||||
@@ -180,8 +185,9 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
busy={audioBusy ?? false}
|
||||
onClick={toggleAudio}
|
||||
disabled={toggleAudio === undefined}
|
||||
disabled={(audioBusy ?? false) || toggleAudio === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
);
|
||||
@@ -194,6 +200,7 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
key="video"
|
||||
iconsAndLabels="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
busy={videoBusy ?? false}
|
||||
onMuteClick={toggleVideo}
|
||||
options={videoOptions}
|
||||
selectedOption={selectedVideo}
|
||||
@@ -208,8 +215,9 @@ export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
busy={videoBusy ?? false}
|
||||
onClick={toggleVideo}
|
||||
disabled={toggleVideo === undefined}
|
||||
disabled={(videoBusy ?? false) || toggleVideo === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -32,14 +32,21 @@ function buildMuteBehaviors(
|
||||
muteStates: MuteStates,
|
||||
): Pick<
|
||||
ViewModel<FooterSnapshot>,
|
||||
"audioEnabled$" | "toggleAudio$" | "videoEnabled$" | "toggleVideo$"
|
||||
| "audioEnabled$"
|
||||
| "audioBusy$"
|
||||
| "toggleAudio$"
|
||||
| "videoEnabled$"
|
||||
| "videoBusy$"
|
||||
| "toggleVideo$"
|
||||
> {
|
||||
return {
|
||||
audioEnabled$: muteStates.audio.enabled$,
|
||||
audioBusy$: muteStates.audio.syncing$,
|
||||
toggleAudio$: scope.behavior(
|
||||
muteStates.audio.toggle$.pipe(map((t) => t ?? undefined)),
|
||||
),
|
||||
videoEnabled$: muteStates.video.enabled$,
|
||||
videoBusy$: muteStates.video.syncing$,
|
||||
toggleVideo$: scope.behavior(
|
||||
muteStates.video.toggle$.pipe(map((t) => t ?? undefined)),
|
||||
),
|
||||
@@ -252,7 +259,9 @@ export function createLobbyFooterViewModel(
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
audioEnabled: undefined,
|
||||
audioBusy: false,
|
||||
videoEnabled: undefined,
|
||||
videoBusy: false,
|
||||
layoutMode: undefined,
|
||||
sharingScreen: false,
|
||||
audioOutputSwitcher: undefined,
|
||||
|
||||
@@ -93,6 +93,27 @@ describe("MediaMuteAndSwitchButton", () => {
|
||||
expect(onMute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("disables mute button while busy", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onMute = vi.fn();
|
||||
const { getByRole } = renderComponent(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Switcher"}
|
||||
onMuteClick={onMute}
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
busy={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const muteButton = getByRole("switch", { name: "Mute microphone" });
|
||||
expect(muteButton).toHaveAttribute("aria-disabled", "true");
|
||||
expect(muteButton).toHaveAttribute("aria-busy", "true");
|
||||
|
||||
await user.click(muteButton);
|
||||
expect(onMute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("requests device names when opened", async () => {
|
||||
const user = userEvent.setup();
|
||||
const requestDeviceNames = vi.fn();
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface MediaMuteAndSwitchButtonProps {
|
||||
enabled?: boolean;
|
||||
/** Callback if the mute button is clicked */
|
||||
onMuteClick?: () => void;
|
||||
/** True while mute/unmute operation is syncing. */
|
||||
busy?: boolean;
|
||||
iconsAndLabels: "video" | "audio";
|
||||
/** The options available for the media device selector modal */
|
||||
options?: MenuOptions[];
|
||||
@@ -59,6 +61,7 @@ const BLUR_ID = "blur";
|
||||
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
title,
|
||||
enabled,
|
||||
busy,
|
||||
onMuteClick,
|
||||
iconsAndLabels,
|
||||
options,
|
||||
@@ -69,6 +72,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
}) => {
|
||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const isBusy = busy ?? false;
|
||||
const { t } = useTranslation();
|
||||
const devices = useMediaDevices();
|
||||
|
||||
@@ -83,12 +87,14 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
button = (
|
||||
<VideoButton
|
||||
enabled={enabled ?? false}
|
||||
busy={isBusy}
|
||||
onClick={(e) => {
|
||||
if (isBusy) return;
|
||||
onMuteClick?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
disabled={onMuteClick === undefined}
|
||||
disabled={isBusy || onMuteClick === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>
|
||||
);
|
||||
@@ -106,12 +112,14 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
button = (
|
||||
<MicButton
|
||||
enabled={enabled ?? false}
|
||||
busy={isBusy}
|
||||
onClick={(e) => {
|
||||
if (isBusy) return;
|
||||
onMuteClick?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
disabled={onMuteClick === undefined}
|
||||
disabled={isBusy || onMuteClick === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
class="container"
|
||||
>
|
||||
<button
|
||||
aria-busy="false"
|
||||
aria-checked="false"
|
||||
aria-disabled="true"
|
||||
aria-labelledby="_r_0_"
|
||||
|
||||
Reference in New Issue
Block a user