diff --git a/packages/web/src/components/layout/ChannelSidebar.tsx b/packages/web/src/components/layout/ChannelSidebar.tsx index 1d8ba612..172bbc21 100644 --- a/packages/web/src/components/layout/ChannelSidebar.tsx +++ b/packages/web/src/components/layout/ChannelSidebar.tsx @@ -74,15 +74,37 @@ export function ChannelSidebar() { navigate(`/channels/${currentServerId}/${channelId}`); }; + // Floating bottom panel — shared between DM view and server view + const floatingPanel = user ? ( +
+
+ {/* Voice controls (expands when connected) */} + {currentVoiceChannelId && } + {/* Separator between voice and user area */} + {currentVoiceChannelId &&
} + {/* User area (always visible) */} + openModal('userSettings')} + /> +
+
+ ) : null; + if (!server) { return ( + <>
-
+
- {/* Voice controls — visible when in a call, even in DM view */} - {currentVoiceChannelId && } - - {/* User area at bottom */} - {user && ( - openModal('userSettings')} - /> - )}
+ {floatingPanel} + ); } return ( + <>
{/* Server header */}
@@ -230,7 +241,7 @@ export function ChannelSidebar() {
{/* Channels */} -
+
{/* Text Channels */}
@@ -321,21 +332,9 @@ export function ChannelSidebar() {
- {/* Voice controls — VoiceControls reads state and calls LiveKit SDK directly */} - {currentVoiceChannelId && } - - {/* User area */} - {user && ( - openModal('userSettings')} - /> - )}
+ {floatingPanel} + ); } @@ -650,7 +649,7 @@ function UserAreaPanel({ )} {/* User area bar */} -
+
{/* Avatar + name */}
diff --git a/packages/web/src/components/voice/VoiceControls.tsx b/packages/web/src/components/voice/VoiceControls.tsx index cfeb1f03..64052b7b 100644 --- a/packages/web/src/components/voice/VoiceControls.tsx +++ b/packages/web/src/components/voice/VoiceControls.tsx @@ -4,6 +4,10 @@ import { useServerStore } from '../../stores/serverStore'; import { getActiveRoom } from '../../hooks/useLiveKit'; import { wsSend } from '../../hooks/useWebSocket'; +/** + * VoiceControls renders the voice status + button rows. + * It has NO wrapper/card styling — the parent provides the container. + */ export function VoiceControls() { const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); const isMuted = useVoiceStore((s) => s.isMuted); @@ -41,21 +45,19 @@ export function VoiceControls() { try { const willDeafen = !isDeafened; if (willDeafen) { - // Deafen: mute mic + mute all remote audio await room.localParticipant.setMicrophoneEnabled(false); room.remoteParticipants.forEach((p) => { p.setVolume(0); }); - if (!isMuted) toggleMic(); // Also mute mic when deafening + if (!isMuted) toggleMic(); } else { - // Undeafen: restore remote audio, unmute mic const outputVolume = useVoiceStore.getState().outputVolume; const scaled = outputVolume / 100; room.remoteParticipants.forEach((p) => { p.setVolume(scaled); }); await room.localParticipant.setMicrophoneEnabled(true); - if (isMuted) toggleMic(); // Unmute mic when undeafening + if (isMuted) toggleMic(); } } catch (err) { console.error('[VoiceControls] Failed to toggle deafen:', err); @@ -103,10 +105,13 @@ export function VoiceControls() { ? 'bg-discord-green/20' : 'bg-discord-yellow/20'; + const btnBase = 'flex-1 h-[34px] flex items-center justify-center rounded-[4px] transition-colors'; + const btnDefaultStyle = 'bg-[#111214] text-discord-text-muted hover:bg-[#1a1b1e] hover:text-discord-text-secondary'; + return ( -
+ <> {/* Row 1: Signal icon + status text + disconnect */} -
+
@@ -141,14 +146,13 @@ export function VoiceControls() {
{/* Row 2: Mute, Deafen, Camera, Screen Share */} -
- {/* Mute */} +
- {/* Deafen */} - {/* Camera */} - {/* Screen Share */}
-
+ ); }