From 72520dbb3f8f9a08701c207d79047e38160f4e4a Mon Sep 17 00:00:00 2001 From: Jake Janicke Date: Wed, 4 Mar 2026 01:41:16 -0600 Subject: [PATCH] Add screen share volume slider UI Signed-off-by: Jake Janicke --- locales/en/app.json | 1 + src/tile/SpotlightTile.module.css | 26 +++++++++ src/tile/SpotlightTile.tsx | 94 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/locales/en/app.json b/locales/en/app.json index 0b0ac7b4..6ec4859e 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -256,6 +256,7 @@ "mute_for_me": "Mute for me", "muted_for_me": "Muted for me", "volume": "Volume", + "screen_share_volume": "Screen share volume", "waiting_for_media": "Waiting for media..." } } diff --git a/src/tile/SpotlightTile.module.css b/src/tile/SpotlightTile.module.css index 622496d2..43c3d53c 100644 --- a/src/tile/SpotlightTile.module.css +++ b/src/tile/SpotlightTile.module.css @@ -108,6 +108,32 @@ Please see LICENSE in the repository root for full details. z-index: 1; } +.volumeSlider { + width: 100%; +} + +/* Disable the hover effect for the screen share volume menu button */ +.volumeMenuItem:hover { + background: transparent; + cursor: default; +} + +.volumeMenuItem { + gap: var(--cpd-space-3x); +} + +.menuMuteButton { + background: none; + border: none; + 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); diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 75c69479..93a1fbd9 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -20,6 +20,8 @@ import { CollapseIcon, ChevronLeftIcon, ChevronRightIcon, + VolumeOffIcon, + VolumeOnIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { animated } from "@react-spring/web"; import { type Observable, map } from "rxjs"; @@ -27,6 +29,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 +48,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 { constant } from "../state/Behavior"; interface SpotlightItemBaseProps { ref?: Ref; @@ -260,6 +265,33 @@ export const SpotlightTile: FC = ({ const visibleIndex = media.findIndex((vm) => vm.id === visibleId); const canGoBack = visibleIndex > 0; const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1; + const currentMedia = media[visibleIndex]; + // isScreenShare only needs to check "audioEnabled$" but I wanted to be more specific + // just in case more models are added in the future, since screen shares always have video + const isScreenShare = + currentMedia && + "audioEnabled$" in currentMedia && + "videoEnabled$" in currentMedia; + + const hasAudio$ = useBehavior( + isScreenShare && currentMedia?.audioEnabled$ + ? currentMedia.audioEnabled$ + : constant(false), + ); + const isLocalScreenShare = isScreenShare && currentMedia.local; + const screenShareLocallyMuted = useBehavior( + isScreenShare + ? (currentMedia as RemoteScreenShareViewModel).playbackMuted$ + : constant(false), + ); + const ScreenShareVolumeIcon = screenShareLocallyMuted + ? VolumeOffIcon + : VolumeOnIcon; + const screenShareVolume = useBehavior( + isScreenShare + ? (currentMedia as RemoteScreenShareViewModel).playbackVolume$ + : constant(0), + ); const isFullscreen = useCallback((): boolean => { const rootElement = document.body; @@ -328,6 +360,7 @@ export const SpotlightTile: FC = ({ }, [latestVisibleId, latestMedia, setScrollToId]); const ToggleExpandIcon = expanded ? CollapseIcon : ExpandIcon; + const [openVolumeMenu, setOpenVolumeMenu] = useState(false); return ( = ({ ))}
+ {/* + Show volume slider only when the tile is a screenshare, has audio, + is in spotlight mode, and isn't your own screen share. + */} + {isScreenShare && + hasAudio$ && + onToggleExpanded && + !isLocalScreenShare && ( + + + + } + > + + + + ( + currentMedia as RemoteScreenShareViewModel + ).adjustPlaybackVolume(v) + } + onValueCommit={() => + ( + currentMedia as RemoteScreenShareViewModel + ).commitPlaybackVolume() + } + /> + + + )}