fix: reliable speaking indicator via direct store writes + polling safety net

Eliminate double-state architecture (useState → useEffect bridge → store)
that lost speaking events due to React 18 batching. ActiveSpeakersChanged
now writes speakingParticipantIds directly to voiceStore; 200ms poll
catches missed SDK events. Each VoiceUser subscribes to its own identity
via fine-grained selector for minimal re-renders. Also adds connection
quality indicator and ConnectionInfoPopover.
This commit is contained in:
Jannis Braun
2026-02-23 02:36:39 +01:00
parent 176f4db27e
commit 9091f08ced
10 changed files with 604 additions and 46 deletions
@@ -93,12 +93,10 @@ export function AppLayout() {
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
const setParticipants = useVoiceStore((s) => s.setParticipants);
const {
connect: connectVoice,
connectDm: connectDmVoice,
disconnect: disconnectVoice,
participants: voiceParticipants,
isConnected: isVoiceConnected,
isConnecting: isVoiceConnecting,
connectedChannelId,
@@ -107,11 +105,6 @@ export function AppLayout() {
// Initialize WebSocket
const { isConnected: isWsConnected } = useWebSocket();
// Sync participants to store
useEffect(() => {
setParticipants(voiceParticipants);
}, [voiceParticipants, setParticipants]);
// Track the last channel we attempted to connect to, to prevent effect loops
const lastAttemptedRef = React.useRef<string | null>(null);
@@ -26,6 +26,8 @@ export function MainContent() {
const setVoiceFullscreen = useUIStore((s) => s.setVoiceFullscreen);
const participants = useVoiceStore((s) => s.participants);
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected);
const connectionError = useVoiceStore((s) => s.connectionError);
const showDms = useUIStore((s) => s.showDms);
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
const outgoingCall = useVoiceStore((s) => s.outgoingCall);
@@ -236,8 +238,16 @@ export function MainContent() {
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46Z" />
</svg>
<span className="font-bold text-discord-text-primary">{channel.name}</span>
<span className="text-xs text-discord-green font-medium ml-2">Connected</span>
<span className="text-xs text-discord-text-muted ml-1">{participants.length} connected</span>
{connectionError ? (
<span className="text-xs text-discord-red font-medium ml-2">Connection Failed</span>
) : isLiveKitConnected ? (
<>
<span className="text-xs text-discord-green font-medium ml-2">Connected</span>
<span className="text-xs text-discord-text-muted ml-1">{participants.length} connected</span>
</>
) : (
<span className="text-xs text-discord-yellow font-medium ml-2">Connecting...</span>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<MemberListToggleButton />