mirror of
https://github.com/vector-im/element-call.git
synced 2026-04-28 09:44:37 +00:00
Merge branch 'livekit' into valere/auto_fit_based_on_video_ratio
This commit is contained in:
@@ -84,7 +84,6 @@ Please see LICENSE in the repository root for full details.
|
||||
.expand {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
padding: var(--cpd-space-2x);
|
||||
border: none;
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
@@ -108,6 +107,35 @@ Please see LICENSE in the repository root for full details.
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.volumeSlider {
|
||||
width: 100%;
|
||||
min-width: 172px;
|
||||
}
|
||||
|
||||
/* Disable the hover effect for the screen share volume menu button */
|
||||
.volumeMenuItem:hover {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.volumeMenuItem {
|
||||
gap: var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.menuMuteButton {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Make icons change color with the theme */
|
||||
.menuMuteButton > svg {
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.expand > svg {
|
||||
display: block;
|
||||
color: var(--cpd-color-icon-primary);
|
||||
@@ -119,17 +147,22 @@ Please see LICENSE in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
.expand:active {
|
||||
.expand:active,
|
||||
.expand[data-state="open"] {
|
||||
background: var(--cpd-color-gray-100);
|
||||
}
|
||||
|
||||
@media (hover) {
|
||||
.tile > div > button {
|
||||
opacity: 0;
|
||||
}
|
||||
.tile:hover > div > button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tile:has(:focus-visible) > div > button {
|
||||
.tile:has(:focus-visible) > div > button,
|
||||
.tile > div:has([data-state="open"]) > button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { test, expect, vi } from "vitest";
|
||||
import { isInaccessible, render, screen } from "@testing-library/react";
|
||||
import { axe } from "vitest-axe";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
import { SpotlightTile } from "./SpotlightTile";
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
mockLocalMedia,
|
||||
mockRemoteMedia,
|
||||
mockRemoteParticipant,
|
||||
mockRemoteScreenShare,
|
||||
} from "../utils/test";
|
||||
import { SpotlightTileViewModel } from "../state/TileViewModel";
|
||||
import { constant } from "../state/Behavior";
|
||||
@@ -78,3 +80,63 @@ test("SpotlightTile is accessible", async () => {
|
||||
await user.click(screen.getByRole("button", { name: "Expand" }));
|
||||
expect(toggleExpanded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("Screen share volume UI is shown when screen share has audio", async () => {
|
||||
const vm = mockRemoteScreenShare(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{},
|
||||
mockRemoteParticipant({}),
|
||||
);
|
||||
|
||||
vi.spyOn(vm, "audioEnabled$", "get").mockReturnValue(constant(true));
|
||||
|
||||
const toggleExpanded = vi.fn();
|
||||
const { container } = render(
|
||||
<TooltipProvider>
|
||||
<SpotlightTile
|
||||
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
||||
targetWidth={300}
|
||||
targetHeight={200}
|
||||
expanded={false}
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
focusable
|
||||
/>
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
|
||||
// Volume menu button should exist
|
||||
expect(screen.queryByRole("button", { name: /volume/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Screen share volume UI is hidden when screen share has no audio", async () => {
|
||||
const vm = mockRemoteScreenShare(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{},
|
||||
mockRemoteParticipant({}),
|
||||
);
|
||||
|
||||
vi.spyOn(vm, "audioEnabled$", "get").mockReturnValue(constant(false));
|
||||
|
||||
const toggleExpanded = vi.fn();
|
||||
const { container } = render(
|
||||
<SpotlightTile
|
||||
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
||||
targetWidth={300}
|
||||
targetHeight={200}
|
||||
expanded={false}
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
focusable
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
|
||||
// Volume menu button should not exist
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /volume/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
CollapseIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
VolumeOffIcon,
|
||||
VolumeOnIcon,
|
||||
VolumeOffSolidIcon,
|
||||
VolumeOnSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { animated } from "@react-spring/web";
|
||||
import { type Observable, map } from "rxjs";
|
||||
@@ -27,6 +31,7 @@ import { useObservableRef } from "observable-hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||
import { Menu, MenuItem } from "@vector-im/compound-web";
|
||||
|
||||
import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react";
|
||||
import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react";
|
||||
@@ -45,6 +50,8 @@ import { type UserMediaViewModel } from "../state/media/UserMediaViewModel";
|
||||
import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel";
|
||||
import { type RemoteScreenShareViewModel } from "../state/media/RemoteScreenShareViewModel";
|
||||
import { type MediaViewModel } from "../state/media/MediaViewModel";
|
||||
import { Slider } from "../Slider";
|
||||
import { platform } from "../Platform";
|
||||
|
||||
interface SpotlightItemBaseProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
@@ -240,6 +247,73 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||
|
||||
SpotlightItem.displayName = "SpotlightItem";
|
||||
|
||||
interface ScreenShareVolumeButtonProps {
|
||||
vm: RemoteScreenShareViewModel;
|
||||
}
|
||||
|
||||
const ScreenShareVolumeButton: FC<ScreenShareVolumeButtonProps> = ({ vm }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const audioEnabled = useBehavior(vm.audioEnabled$);
|
||||
const playbackMuted = useBehavior(vm.playbackMuted$);
|
||||
const playbackVolume = useBehavior(vm.playbackVolume$);
|
||||
|
||||
const VolumeIcon = playbackMuted ? VolumeOffIcon : VolumeOnIcon;
|
||||
const VolumeSolidIcon = playbackMuted
|
||||
? VolumeOffSolidIcon
|
||||
: VolumeOnSolidIcon;
|
||||
|
||||
const [volumeMenuOpen, setVolumeMenuOpen] = useState(false);
|
||||
const onMuteButtonClick = useCallback(() => vm.togglePlaybackMuted(), [vm]);
|
||||
const onVolumeChange = useCallback(
|
||||
(v: number) => vm.adjustPlaybackVolume(v),
|
||||
[vm],
|
||||
);
|
||||
const onVolumeCommit = useCallback(() => vm.commitPlaybackVolume(), [vm]);
|
||||
|
||||
return (
|
||||
audioEnabled && (
|
||||
<Menu
|
||||
open={volumeMenuOpen}
|
||||
onOpenChange={setVolumeMenuOpen}
|
||||
title={t("video_tile.screen_share_volume")}
|
||||
side="top"
|
||||
align="end"
|
||||
trigger={
|
||||
<button
|
||||
className={styles.expand}
|
||||
aria-label={t("video_tile.screen_share_volume")}
|
||||
>
|
||||
<VolumeSolidIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<MenuItem
|
||||
as="div"
|
||||
className={styles.volumeMenuItem}
|
||||
onSelect={null}
|
||||
label={null}
|
||||
hideChevron={true}
|
||||
>
|
||||
<button className={styles.menuMuteButton} onClick={onMuteButtonClick}>
|
||||
<VolumeIcon aria-hidden width={24} height={24} />
|
||||
</button>
|
||||
<Slider
|
||||
className={styles.volumeSlider}
|
||||
label={t("video_tile.volume")}
|
||||
value={playbackVolume}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onValueChange={onVolumeChange}
|
||||
onValueCommit={onVolumeCommit}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
vm: SpotlightTileViewModel;
|
||||
@@ -274,6 +348,7 @@ export const SpotlightTile: FC<Props> = ({
|
||||
const latestMedia = useLatest(media);
|
||||
const latestVisibleId = useLatest(visibleId);
|
||||
const visibleIndex = media.findIndex((vm) => vm.id === visibleId);
|
||||
const visibleMedia = media.at(visibleIndex);
|
||||
const canGoBack = visibleIndex > 0;
|
||||
const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1;
|
||||
|
||||
@@ -381,16 +456,21 @@ export const SpotlightTile: FC<Props> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.bottomRightButtons}>
|
||||
<button
|
||||
className={classNames(styles.expand)}
|
||||
aria-label={"maximise"}
|
||||
onClick={onToggleFullscreen}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<FullScreenIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
|
||||
<div className={styles.bottomRightButtons}>
|
||||
{visibleMedia?.type === "screen share" && !visibleMedia.local && (
|
||||
<ScreenShareVolumeButton vm={visibleMedia} />
|
||||
)}
|
||||
{platform === "desktop" && (
|
||||
<button
|
||||
className={classNames(styles.expand)}
|
||||
aria-label={"maximise"}
|
||||
onClick={onToggleFullscreen}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<FullScreenIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
)}
|
||||
{onToggleExpanded && (
|
||||
<button
|
||||
className={classNames(styles.expand)}
|
||||
|
||||
Reference in New Issue
Block a user