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 */}
-