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
@@ -14,6 +14,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
const { participant } = tile;
const isDeafened = useVoiceStore((s) => s.isDeafened);
const participantVolumes = useVoiceStore((s) => s.participantVolumes);
const isSpeaking = useVoiceStore((s) => s.speakingParticipantIds.has(participant.identity));
const [, forceUpdate] = useState(0);
@@ -70,7 +71,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
return (
<div
className={`relative bg-[#111214] rounded-xl overflow-hidden flex items-center justify-center group transition-all duration-200 ${
participant.isSpeaking
isSpeaking
? 'ring-[3px] ring-discord-green shadow-[0_0_12px_rgba(35,165,90,0.25)]'
: 'ring-1 ring-white/[0.06] hover:ring-white/10'
} ${large ? 'h-full w-full' : 'h-full aspect-video'}`}
@@ -92,7 +93,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
name={participant.username}
size={large ? 100 : 64}
/>
{participant.isSpeaking && (
{isSpeaking && (
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-discord-green animate-pulse" />
)}
</div>