diff --git a/packages/client/components/app/menus/UserContextMenu.tsx b/packages/client/components/app/menus/UserContextMenu.tsx index d67cdee22..e1f29cfbd 100644 --- a/packages/client/components/app/menus/UserContextMenu.tsx +++ b/packages/client/components/app/menus/UserContextMenu.tsx @@ -29,6 +29,7 @@ import MdReport from "@material-design-icons/svg/outlined/report.svg?component-s import MdChecked from "@material-symbols/svg-400/outlined/check_box.svg?component-solid"; import MdUnchecked from "@material-symbols/svg-400/outlined/check_box_outline_blank.svg?component-solid"; +import { useVoice } from "@revolt/rtc"; import { ContextMenu, ContextMenuButton, @@ -45,6 +46,7 @@ export function UserContextMenu(props: { member?: ServerMember; contextMessage?: Message; inVoice?: boolean; + isScreenshare?: boolean; }) { // TODO: if we take serverId instead, we could dynamically fetch server member here // same for the floating menu I guess? @@ -52,6 +54,7 @@ export function UserContextMenu(props: { const client = useClient(); const navigate = useNavigate(); const { openModal } = useModals(); + const voice = useVoice(); // server context const params = useSmartParams(); @@ -191,7 +194,7 @@ export function UserContextMenu(props: { return ( - + e.stopImmediatePropagation()} onClick={(e) => e.stopImmediatePropagation()} @@ -230,6 +233,45 @@ export function UserContextMenu(props: { + + e.stopImmediatePropagation()} + onClick={(e) => e.stopImmediatePropagation()} + > + + Screen Share Volume + + + voice.setScreenshareVolume( + props.user.id, + event.currentTarget.value, + ) + } + labelFormatter={(label) => (label * 100).toFixed(0) + "%"} + /> + + + voice.setScreenshareMuted( + props.user.id, + !voice.getScreenshareMuted(props.user.id), + ) + } + actionSymbol={ + voice.getScreenshareMuted(props.user.id) ? MdChecked : MdUnchecked + } + > + Mute Screen Share + + + + diff --git a/packages/client/components/rtc/components/RoomAudioManager.tsx b/packages/client/components/rtc/components/RoomAudioManager.tsx index 28acc633e..24fbd9a83 100644 --- a/packages/client/components/rtc/components/RoomAudioManager.tsx +++ b/packages/client/components/rtc/components/RoomAudioManager.tsx @@ -50,10 +50,14 @@ export function RoomAudioManager() { trackRef={track()} volume={ state.voice.outputVolume * - state.voice.getUserVolume(track().participant.identity) + (track().source === Track.Source.ScreenShareAudio + ? voice.getScreenshareVolume(track().participant.identity) + : state.voice.getUserVolume(track().participant.identity)) } muted={ - state.voice.getUserMuted(track().participant.identity) || + (track().source === Track.Source.ScreenShareAudio + ? voice.getScreenshareMuted(track().participant.identity) + : state.voice.getUserMuted(track().participant.identity)) || voice.deafen() } enableBoosting diff --git a/packages/client/components/rtc/state.tsx b/packages/client/components/rtc/state.tsx index a2a2e0d1a..eda6cc8a1 100644 --- a/packages/client/components/rtc/state.tsx +++ b/packages/client/components/rtc/state.tsx @@ -1,10 +1,10 @@ import { Accessor, - JSX, - Setter, batch, createContext, createSignal, + JSX, + Setter, useContext, } from "solid-js"; import { RoomContext } from "solid-livekit-components"; @@ -18,6 +18,7 @@ import { Voice as VoiceSettings } from "@revolt/state/stores/Voice"; import { VoiceCallCardContext } from "@revolt/ui/components/features/voice/callCard/VoiceCallCard"; import { CONFIGURATION } from "@revolt/common"; +import { createStore, SetStoreFunction } from "solid-js/store"; import { InRoom } from "./components/InRoom"; import { RoomAudioManager } from "./components/RoomAudioManager"; @@ -28,6 +29,11 @@ type State = | "CONNECTED" | "RECONNECTING"; +type ScreenShareSettings = { + volumes: Record; + mutes: Record; +}; + class Voice { #settings: VoiceSettings; @@ -52,6 +58,9 @@ class Voice { screenshare: Accessor; #setScreenshare: Setter; + screenshareSettingStore: ScreenShareSettings; + #setScreenshareSettingStore: SetStoreFunction; + constructor(voiceSettings: VoiceSettings) { this.#settings = voiceSettings; @@ -82,6 +91,15 @@ class Voice { const [screenshare, setScreenshare] = createSignal(false); this.screenshare = screenshare; this.#setScreenshare = setScreenshare; + + const [screenshareSettingsStore, setScreenshareSettingsStore] = createStore( + { + mutes: {}, + volumes: {}, + } as ScreenShareSettings, + ); + this.screenshareSettingStore = screenshareSettingsStore as never; + this.#setScreenshareSettingStore = setScreenshareSettingsStore; } async connect(channel: Channel, auth?: { url: string; token: string }) { @@ -179,6 +197,7 @@ class Voice { if (!room) throw "invalid state"; await room.localParticipant.setScreenShareEnabled( !room.localParticipant.isScreenShareEnabled, + { audio: true }, ); this.#setScreenshare(room.localParticipant.isScreenShareEnabled); @@ -195,6 +214,22 @@ class Voice { get speakingPermission() { return !!this.channel()?.havePermission("Speak"); } + + setScreenshareVolume(userId: string, volume: number) { + this.#setScreenshareSettingStore("volumes", userId, volume); + } + + getScreenshareVolume(userId: string): number { + return this.screenshareSettingStore.volumes[userId] || 1.0; + } + + setScreenshareMuted(userId: string, muted: boolean) { + this.#setScreenshareSettingStore("mutes", userId, muted); + } + + getScreenshareMuted(userId: string): boolean { + return this.screenshareSettingStore.mutes[userId] || false; + } } const voiceContext = createContext(null as unknown as Voice); diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx index cc62c68e1..701c0a1ab 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx @@ -18,7 +18,7 @@ import { styled } from "styled-system/jsx"; import { UserContextMenu } from "@revolt/app"; import { useUser } from "@revolt/markdown/users"; -import { InRoom } from "@revolt/rtc"; +import { InRoom, useVoice } from "@revolt/rtc"; import { Avatar } from "@revolt/ui/components/design"; import { OverflowingText } from "@revolt/ui/components/utils"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; @@ -229,6 +229,7 @@ function ScreenshareTile() { const participant = useEnsureParticipant(); const track = useMaybeTrackRefContext(); const user = useUser(participant.identity); + const voice = useVoice(); const isMuted = useIsMuted({ participant, @@ -237,7 +238,7 @@ function ScreenshareTile() { let videoRef: HTMLDivElement | undefined; - const toggleFullscreen = () => { + function toggleFullscreen() { if (!videoRef) return; if (!isTrackReference(track)) return; if (!document.fullscreenElement) { @@ -245,7 +246,10 @@ function ScreenshareTile() { } else { document.exitFullscreen(); } - }; + } + + const isScreenshareMuted = () => + voice.getScreenshareMuted(user().user!.id) ? "by-user" : isMuted() || false; return (
( + + ), + }} > {user().username} - - no_sound + + + no_sound + fullscreen