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:
@@ -142,6 +142,23 @@ func (c *Client) readPump() {
|
||||
switch event.Type {
|
||||
case EventTypingStart, EventPresenceUpdate:
|
||||
c.Hub.BroadcastEvent(event)
|
||||
case EventVoiceWhisper:
|
||||
// Forward voice whispers only to the target user, not broadcast
|
||||
var whisperData struct {
|
||||
TargetUserID string `json:"target_user_id"`
|
||||
FromUserID string `json:"from_user_id"`
|
||||
FromUsername string `json:"from_username"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
if raw, ok := event.Data.(json.RawMessage); ok {
|
||||
if err := json.Unmarshal(raw, &whisperData); err == nil {
|
||||
whisperData.FromUserID = c.UserID
|
||||
c.Hub.SendToUser(whisperData.TargetUserID, Event{
|
||||
Type: EventVoiceWhisper,
|
||||
Data: whisperData,
|
||||
})
|
||||
}
|
||||
}
|
||||
default:
|
||||
c.Hub.logger.Info("received event from client", "type", event.Type, "user_id", c.UserID)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
EventVoiceLeave = "VOICE_LEAVE"
|
||||
EventVoiceMute = "VOICE_MUTE"
|
||||
EventVoiceDeafen = "VOICE_DEAFEN"
|
||||
EventVoiceWhisper = "VOICE_WHISPER"
|
||||
)
|
||||
|
||||
// Event represents a WebSocket event sent to clients.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user