fix loudspeaker confusion and icons

This commit is contained in:
Timo K
2026-04-15 16:41:19 +02:00
parent 3fc823e049
commit 9fa382ed0c
5 changed files with 39 additions and 30 deletions

View File

@@ -23,6 +23,7 @@ import {
OverflowHorizontalIcon,
OverflowVerticalIcon,
VolumeOnSolidIcon,
VolumeOffSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import styles from "./Button.module.css";
@@ -134,31 +135,25 @@ export const EndCallButton: FC<EndCallButtonProps> = ({
interface LoudspeakerButtonProps extends ComponentPropsWithoutRef<"button"> {
size?: "sm" | "lg";
/** The button will be rendered:
* true: currently in loudspeaker mode, pressing will switch to earpiece (rendered as enabled)
* false: currently in earpiece mode, pressing will switch to loudspeaker (rendered as disabled)
*/
isEarpieceTarget: boolean;
loudspeakerModeEnabled: boolean;
}
export const LoudspeakerButton: FC<LoudspeakerButtonProps> = ({
isEarpieceTarget,
loudspeakerModeEnabled,
...props
}) => {
const { t } = useTranslation();
const label = isEarpieceTarget
? t("settings.devices.handset")
: t("settings.devices.loudspeaker");
// if the target is the earpice, we are currently in loudspeaker mode.
const enabled = isEarpieceTarget;
const label = loudspeakerModeEnabled
? t("settings.devices.loudspeaker")
: t("settings.devices.handset");
return (
<Tooltip label={label}>
<CpdButton
iconOnly
Icon={VolumeOnSolidIcon}
Icon={loudspeakerModeEnabled ? VolumeOnSolidIcon : VolumeOffSolidIcon}
{...props}
kind={enabled ? "primary" : "secondary"}
role="switch"
aria-checked={enabled}
kind={loudspeakerModeEnabled ? "primary" : "secondary"}
aria-checked={loudspeakerModeEnabled}
/>
</Tooltip>
);

View File

@@ -54,6 +54,8 @@ export const Default: Story = {
args: {
hideLogo: true,
layoutMode: "grid",
audioEnabled: true,
videoEnabled: true,
setLayoutMode: fn(),
openSettings: fn(),
toggleAudio: fn(),
@@ -73,8 +75,8 @@ export const Default: Story = {
mapping: {
NoOutputCallback: undefined,
// This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one.
speaker: { targetOutput: "speaker", switch: fn() },
earpiece: { targetOutput: "earpiece", switch: fn() },
speaker: { targetOutput: "earpiece", switch: fn() },
earpiece: { targetOutput: "speaker", switch: fn() },
},
},
toggleScreenSharing: fnArgType,
@@ -102,7 +104,16 @@ export const AudioVideoEnabled: Story = {
videoEnabled: true,
},
};
export const WithAudioOutput: Story = {
export const WithAudioOutputSpeaker: Story = {
...Default,
args: {
...Default.args,
audioOutputSwitcher: { targetOutput: "earpiece", switch: fn() },
},
};
export const WithAudioOutputEarpiece: Story = {
...Default,
args: {
...Default.args,

View File

@@ -35,6 +35,14 @@ export interface FooterProps {
ref?: Ref<HTMLDivElement>;
/** Children will only be visible if the component is wider than 5*/
children?: JSX.Element | JSX.Element[] | false;
audioEnabled: boolean;
/** Also controls if the audioMute button is disabled */
toggleAudio: (() => void) | undefined;
videoEnabled: boolean;
/** Also controls if the videoMute button is disabled */
toggleVideo: (() => void) | undefined;
/* This is needed for WindowMode = "flat" */
hideControls?: boolean;
/** hide the entire footer*/
@@ -49,13 +57,6 @@ export interface FooterProps {
/** Also controls if the layout button is visible */
setLayoutMode?: (mode: GridMode) => void;
audioEnabled?: boolean;
/** Also controls if the audioMute button is disabled */
toggleAudio?: () => void;
videoEnabled?: boolean;
/** Also controls if the videoMute button is disabled */
toggleVideo?: () => void;
sharingScreen?: boolean;
toggleScreenSharing?: () => void;
@@ -175,7 +176,7 @@ export const CallFooter: FC<FooterProps> = ({
<LoudspeakerButton
size={buttonSize}
onClick={() => audioOutputSwitcher.switch()}
isEarpieceTarget={audioOutputSwitcher.targetOutput === "earpiece"}
loudspeakerModeEnabled={audioOutputSwitcher.targetOutput === "earpiece"}
/>
);
}, [audioOutputSwitcher, buttonSize]);

View File

@@ -253,9 +253,10 @@ describe("InCallView", () => {
["earpiece-id", { type: "earpiece" }],
]),
);
const selected$ = new BehaviorSubject<
{ id: string; virtualEarpiece: boolean } | undefined
>({ id: "speaker-id", virtualEarpiece: false });
const selected$ = new BehaviorSubject({
id: "speaker-id",
virtualEarpiece: false,
});
const mediaDevices = mockMediaDevices({
audioOutput: {
@@ -267,8 +268,7 @@ describe("InCallView", () => {
const { getByRole } = createInCallView({ mediaDevices });
// The button should be visible. When current output is "speaker",
// the switcher targets "earpiece", so the tooltip label is "Handset".
const audioOutputBtn = getByRole("switch", { name: "Handset" });
const audioOutputBtn = getByRole("button", { name: "Loudspeaker" });
expect(audioOutputBtn).toBeVisible();
await user.click(audioOutputBtn);

View File

@@ -222,6 +222,8 @@ export const LobbyView: FC<Props> = ({
{!recentsButtonInFooter && recentsButton}
</div>
<CallFooter
audioEnabled={audioEnabled}
videoEnabled={videoEnabled}
toggleAudio={toggleAudio ?? undefined}
toggleVideo={toggleVideo ?? undefined}
openSettings={openSettings}