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
+12
View File
@@ -11,8 +11,11 @@ interface VoiceState {
isCameraOn: boolean;
isScreenSharing: boolean;
participants: ParticipantInfo[];
speakingParticipantIds: Set<string>;
connectionError: string | null;
isLiveKitConnected: boolean;
connectionQuality: 'excellent' | 'good' | 'poor' | 'lost' | 'unknown';
setConnectionQuality: (q: 'excellent' | 'good' | 'poor' | 'lost' | 'unknown') => void;
inputVolume: number; // 0-200 (100 = default)
outputVolume: number; // 0-200 (100 = default)
inputDeviceId: string;
@@ -49,6 +52,7 @@ interface VoiceState {
removeVoiceUser: (channelId: string, userId: string) => void;
setCurrentVoiceChannel: (channelId: string | null) => void;
setParticipants: (participants: ParticipantInfo[]) => void;
setSpeakingParticipants: (ids: Set<string>) => void;
setConnectionError: (error: string | null) => void;
setIsLiveKitConnected: (connected: boolean) => void;
setInputVolume: (volume: number) => void;
@@ -90,8 +94,10 @@ export const useVoiceStore = create<VoiceState>()(
isCameraOn: false,
isScreenSharing: false,
participants: [],
speakingParticipantIds: new Set(),
connectionError: null,
isLiveKitConnected: false,
connectionQuality: 'unknown',
inputVolume: 100,
outputVolume: 100,
inputDeviceId: 'default',
@@ -202,8 +208,10 @@ export const useVoiceStore = create<VoiceState>()(
}),
setParticipants: (participants) => set({ participants }),
setSpeakingParticipants: (ids) => set({ speakingParticipantIds: ids }),
setConnectionError: (error) => set({ connectionError: error }),
setIsLiveKitConnected: (connected) => set({ isLiveKitConnected: connected }),
setConnectionQuality: (quality) => set({ connectionQuality: quality }),
setInputVolume: (volume) => {
set({ inputVolume: volume });
@@ -264,8 +272,10 @@ export const useVoiceStore = create<VoiceState>()(
isCameraOn: false,
isScreenSharing: false,
participants: [],
speakingParticipantIds: new Set(),
connectionError: null,
isLiveKitConnected: false,
connectionQuality: 'unknown',
focusedParticipantId: null,
activeDmCall: null,
outgoingCall: null,
@@ -283,8 +293,10 @@ export const useVoiceStore = create<VoiceState>()(
isCameraOn: false,
isScreenSharing: false,
participants: [],
speakingParticipantIds: new Set(),
connectionError: null,
isLiveKitConnected: false,
connectionQuality: 'unknown',
inputVolume: 100,
outputVolume: 100,
inputDeviceId: 'default',