Skip to content

fix(voice): fix race conditions, broken icons, and null deref in call UI#8

Merged
bryanchriswhite merged 4 commits into
mainfrom
voice-ux-polish
Feb 20, 2026
Merged

fix(voice): fix race conditions, broken icons, and null deref in call UI#8
bryanchriswhite merged 4 commits into
mainfrom
voice-ux-polish

Conversation

@bryanchriswhite
Copy link
Copy Markdown

Summary

  • Fix mic enable race condition: setMicrophoneEnabled was called inside batch() before room.connect(), and the .then() callback didn't verify the room was still current — could set stale mic state after quick disconnect/reconnect
  • Add reconnecting/reconnected listeners: Voice state never transitioned to RECONNECTING because no LiveKit event listeners were registered, so users always saw "Connected" during network interruptions
  • Fix camera icon: camera_video is not a valid Material Symbol name — changed to videocam in VoiceCallCardActions and VoiceStatefulUserIcons
  • Fix PiP snap: Used window.outerWidth/outerHeight instead of innerWidth/innerHeight, causing PiP card to snap to wrong corner when browser has toolbars/devtools
  • Guard null deref: user().user! in voice participant tile crashes when User object hasn't been fetched yet — added conditional spread

Test plan

  • Join voice channel, verify mic auto-enables without error
  • Disconnect and quickly reconnect — verify mic state is correct
  • Cause network interruption — verify status shows "Reconnecting" then "Connected"
  • Toggle camera on — verify videocam icon renders correctly
  • Drag PiP card with devtools open — verify it snaps to correct corner
  • Join call where a new participant joins — verify no crash from unknown user

🔗 Related beads: chat-d25, chat-aad, chat-3f6, chat-8mh, chat-92p

…n call UI

- Fix mic enable race: move setMicrophoneEnabled outside batch and guard
  the .then() callback against stale room reference
- Add reconnecting/reconnected event listeners so status indicator shows
  correct state during network interruptions
- Fix camera icon: camera_video is not a valid Material Symbol, use videocam
- Fix PiP snap: use innerWidth/innerHeight instead of outerWidth/outerHeight
  so corner detection matches clientX/clientY coordinate space
- Guard user().user null dereference in voice participant tile to prevent
  crash when participant User object hasn't been fetched yet
@github-actions
Copy link
Copy Markdown

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 20, 2026

Greptile Summary

This PR fixes multiple critical voice call issues: race conditions in microphone state management, missing network reconnection status, broken camera icons, incorrect PiP positioning, and null pointer crashes.

  • Race condition fix: Added this.room() === room guards to prevent stale callbacks from setting microphone state for disconnected rooms during quick reconnect scenarios
  • Reconnection status: Registered reconnecting and reconnected LiveKit event listeners so users now see proper connection status during network interruptions
  • Icon fixes: Corrected invalid Material Symbol camera_video to valid videocam in two components
  • PiP positioning: Changed from window.outerWidth/outerHeight (includes browser chrome) to innerWidth/innerHeight (viewport only) for accurate corner snapping
  • Null safety: Wrapped floating directive props in conditional spread to prevent crashes when User object hasn't loaded yet

All changes are defensive improvements with no breaking changes to the API surface.

Confidence Score: 5/5

  • This PR is safe to merge - all changes are defensive bug fixes with no architectural changes
  • All fixes address real bugs with clear solutions: guards prevent race conditions, event listeners fix missing state transitions, icon names are corrected to valid Material Symbols, window measurements use correct API, and conditional spread prevents null derefs. No logic changes to happy paths.
  • No files require special attention - all changes are straightforward defensive fixes

Important Files Changed

Filename Overview
packages/client/components/rtc/state.tsx Adds room validation guards to prevent stale mic state, adds reconnecting/reconnected listeners
packages/client/components/ui/components/features/voice/VoiceStatefulUserIcons.tsx Fixes invalid Material Symbol name from camera_video to videocam
packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx Corrects PiP snap positioning from outerWidth/outerHeight to innerWidth/innerHeight
packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx Fixes invalid Material Symbol name from camera_video to videocam
packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx Guards against null dereference when User object hasn't been fetched using conditional spread

Sequence Diagram

sequenceDiagram
    participant User
    participant Voice
    participant Room1 as Room (old)
    participant Room2 as Room (new)
    
    Note over User,Room2: Race Condition Scenario
    
    User->>Voice: joinCall()
    Voice->>Room1: room.connect()
    Voice->>Room1: setMicrophoneEnabled(true)
    
    Note over Voice,Room1: User quickly disconnects
    User->>Voice: disconnect()
    
    Note over Voice,Room2: User reconnects immediately
    User->>Voice: joinCall() again
    Voice->>Room2: room.connect()
    Voice->>Room2: setMicrophoneEnabled(true)
    
    Note over Room1,Voice: ❌ OLD: Stale callback fires
    Room1-->>Voice: .then() callback (no guard)
    Room1->>Voice: setMicrophone(true) for wrong room
    
    Note over Room2,Voice: ✅ NEW: Guard prevents stale state
    Room1-->>Voice: .then() callback
    Voice->>Voice: if (this.room() === room)
    Note over Voice: Guard rejects stale room
    
    Room2-->>Voice: .then() callback
    Voice->>Voice: if (this.room() === room)
    Room2->>Voice: setMicrophone(true) for correct room
Loading

Last reviewed commit: 4ebad53

* gh/main:
  feat(e2e): implement LiveKit mocking layer (#5)
  fix(voice): fix spotlight and hide-members features
  fix(voice): remove incorrect prevTracks parameter from autoSpotlightId memo
  fix(voice): make track dependencies explicit in spotlight memo
  fix(voice): remove unused spotlightControls to fix linting
  feat(voice): implement auto-spotlight and manual pinning
  chore: generate i18n catalog
  feat(voice): add hide members toggle in call actions

# Conflicts:
#	packages/client/components/rtc/state.tsx
@bryanchriswhite
Copy link
Copy Markdown
Author

@greptile

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread packages/client/components/rtc/state.tsx
Address review feedback: the mock path's setTimeout callback could set
mic state on a stale room if disconnect/reconnect happens within the
10ms window. Add the same room() === room check used in the non-mock path.
@bryanchriswhite
Copy link
Copy Markdown
Author

Addressed @greptile feedback: added stale room guards to the mock connection path (both the setTimeout callback and the inner mic enable .then()). See 4ebad53.

@greptile

@bryanchriswhite bryanchriswhite merged commit ece71a2 into main Feb 20, 2026
8 checks passed
@bryanchriswhite bryanchriswhite deleted the voice-ux-polish branch February 20, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant