feat(phase6): video grid, noise suppression, whisper system

- New VideoGrid component: live camera feeds (local + remote) in VoicePanel
- Camera toggle in VoiceControls: enable/disable video per participant
- Noise suppression toggle in VoiceControls + voice store
- VOICE_WHISPER WS event: backend routes whisper to target user only
- Whisper UI: per-participant whisper button, notification toasts with dismiss
- Fix circular import between voice.ts and ws.ts (use lazy accessor)
This commit is contained in:
2026-06-30 12:21:53 -04:00
parent 3c063d4f56
commit 775bd953b0
8 changed files with 307 additions and 28 deletions
+22
View File
@@ -227,6 +227,28 @@ func (h *Hub) ClientCount() int {
return len(h.clients)
}
// SendToUser sends an event only to the specified user's connected client(s).
func (h *Hub) SendToUser(userID string, event Event) {
data, err := json.Marshal(event)
if err != nil {
h.logger.Error("failed to marshal event", "type", event.Type, "error", err)
return
}
h.mu.RLock()
defer h.mu.RUnlock()
for client := range h.clients {
if client.UserID == userID {
select {
case client.send <- data:
default:
go func(c *Client) { h.unregister <- c }(client)
}
}
}
}
// RefreshUserServers loads the server membership for a user from the database
// and caches it. Call when a client connects or when server membership changes.
func (h *Hub) RefreshUserServers(userID string) {