fix(voice): broadcast VOICE_JOIN/LEAVE via ws, show participants in sidebar for all users

This commit is contained in:
2026-07-09 12:47:42 -04:00
parent 62e8354d03
commit e6dfe43926
9 changed files with 92 additions and 12 deletions
+17 -10
View File
@@ -2,6 +2,7 @@ import { useVoiceStore } from '../stores/voice.ts';
import { useAuthStore } from '../stores/auth.ts';
import { usePermissions } from '../lib/usePermissions.ts';
import { useChannelStore } from '../stores/channel.ts';
import { useVoicePresenceStore } from '../stores/voicePresence.ts';
import { api } from '../lib/api.ts';
interface VoiceChannelProps {
@@ -13,7 +14,8 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
const currentRoom = useVoiceStore((state) => state.currentRoom);
const isConnected = useVoiceStore((state) => state.isConnected);
const joinVoice = useVoiceStore((state) => state.joinVoice);
const participants = useVoiceStore((state) => state.participants);
const liveParticipants = useVoiceStore((state) => state.participants);
const presenceParticipants = useVoicePresenceStore((s) => s.getParticipants(channelId));
const currentUserId = useAuthStore((state) => state.user?.id);
const activeChannelId = useChannelStore((s) => s.activeChannelId);
@@ -40,6 +42,11 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
const isActive = currentRoom === channelId;
// Use live participants when in the room, otherwise fall back to presence
const displayParticipants = isActive && isConnected
? liveParticipants.map(p => ({ userId: p.identity, username: p.username, isMuted: p.isMuted, isSpeaking: p.isSpeaking, hasVideo: p.hasVideo, isScreenSharing: p.isScreenSharing }))
: presenceParticipants.map(p => ({ userId: p.userId, username: p.username, isMuted: true, isSpeaking: false, hasVideo: false, isScreenSharing: false }));
const handleClick = () => {
if (!isActive) {
console.log("[VoiceChannel] joining voice:", channelId, channelName);
@@ -69,19 +76,19 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
🔊
</span>
<span className="truncate">{channelName}</span>
{isActive && participants.length > 0 && (
{displayParticipants.length > 0 && (
<span className="ml-auto text-xxs text-gb-fg-f">
[{participants.length}]
[{displayParticipants.length}]
</span>
)}
</button>
{isActive && (
{displayParticipants.length > 0 && (
<div className="pl-7 py-0.5">
{participants.map((p) => {
const isMe = p.identity === currentUserId;
{displayParticipants.map((p) => {
const isMe = p.userId === currentUserId;
return (
<div
key={p.identity}
key={p.userId}
className="text-xxs text-gb-fg-f flex items-center gap-1 group"
>
<span className={p.isSpeaking ? 'text-gb-green' : 'text-gb-fg-f'}>
@@ -93,12 +100,12 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
{p.isScreenSharing && <span className="text-gb-aqua" title="Sharing screen">🖥</span>}
{p.hasVideo && <span className="text-gb-orange" title="Camera on">🎥</span>}
{!isMe && (
{!isMe && isActive && (
<div className="ml-auto opacity-0 group-hover:opacity-100 flex items-center gap-1">
{canMute && !p.isMuted && (
<button
className="text-gb-fg-f hover:text-gb-red"
onClick={(e) => { e.stopPropagation(); handleMute(p.identity); }}
onClick={(e) => { e.stopPropagation(); handleMute(p.userId); }}
title={`Mute ${p.username}`}
>
[mute]
@@ -106,7 +113,7 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
)}
<button
className="text-gb-fg-f hover:text-gb-orange"
onClick={(e) => { e.stopPropagation(); whisperTo(p.identity, p.username); }}
onClick={(e) => { e.stopPropagation(); whisperTo(p.userId, p.username); }}
title={`Whisper to ${p.username}`}
>
[whisper]