diff --git a/src/state/media/RemoteScreenShareViewModel.ts b/src/state/media/RemoteScreenShareViewModel.ts index 78fc0494..8c46aeb3 100644 --- a/src/state/media/RemoteScreenShareViewModel.ts +++ b/src/state/media/RemoteScreenShareViewModel.ts @@ -26,6 +26,9 @@ export interface RemoteScreenShareViewModel * Whether this screen share's video should be displayed. */ videoEnabled$: Behavior; + /** + * Whether this screen share should be considered to have an audio track. + */ audioEnabled$: Behavior; } diff --git a/src/tile/SpotlightTile.test.tsx b/src/tile/SpotlightTile.test.tsx index 8b0b29a7..aac81b9c 100644 --- a/src/tile/SpotlightTile.test.tsx +++ b/src/tile/SpotlightTile.test.tsx @@ -140,34 +140,3 @@ test("Screen share volume UI is hidden when screen share has no audio", async () screen.queryByRole("button", { name: /volume/i }), ).not.toBeInTheDocument(); }); - -test("Screen share volume UI is hidden in grid mode", async () => { - const vm = mockRemoteScreenShare( - mockRtcMembership("@alice:example.org", "AAAA"), - {}, - mockRemoteParticipant({}), - ); - - vi.spyOn(vm, "audioEnabled$", "get").mockReturnValue(constant(true)); - - const { container } = render( - - - , - ); - - expect(await axe(container)).toHaveNoViolations(); - - // Volume menu button should not exist in grid mode - expect( - screen.queryByRole("button", { name: /volume/i }), - ).not.toBeInTheDocument(); -}); diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 00eeda9e..c209e7be 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -22,6 +22,8 @@ import { 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"; @@ -49,7 +51,7 @@ 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"; +import { platform } from "../Platform"; interface SpotlightItemBaseProps { ref?: Ref; @@ -229,6 +231,73 @@ const SpotlightItem: FC = ({ SpotlightItem.displayName = "SpotlightItem"; +interface ScreenShareVolumeButtonProps { + vm: RemoteScreenShareViewModel; +} + +const ScreenShareVolumeButton: FC = ({ 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 && ( + + + + } + > + + + + + + ) + ); +}; + interface Props { ref?: Ref; vm: SpotlightTileViewModel; @@ -263,37 +332,9 @@ export const SpotlightTile: FC = ({ 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; - const currentMedia = media[visibleIndex]; - // only "audioEnabled$" needs to be checked but I wanted to be more specific just in - // case more models are added in the future, since screen shares always have video - const currentScreenShare = - currentMedia && - "audioEnabled$" in currentMedia && - "videoEnabled$" in currentMedia - ? (currentMedia as RemoteScreenShareViewModel) - : null; - - const isScreenShare = currentScreenShare != null; - - const hasAudio$ = useBehavior( - currentScreenShare?.audioEnabled$ ?? constant(false), - ); - - const isLocalScreenShare = currentScreenShare?.local ?? false; - - const screenShareLocallyMuted = useBehavior( - currentScreenShare?.playbackMuted$ ?? constant(false), - ); - - const ScreenShareVolumeIcon = screenShareLocallyMuted - ? VolumeOffIcon - : VolumeOnIcon; - - const screenShareVolume = useBehavior( - currentScreenShare?.playbackVolume$ ?? constant(0), - ); const isFullscreen = useCallback((): boolean => { const rootElement = document.body; @@ -362,7 +403,6 @@ 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() - } - /> - - - )} - +
+ {visibleMedia?.type === "screen share" && !visibleMedia.local && ( + + )} + {platform === "desktop" && ( + + )} {onToggleExpanded && (