fix(voice): broadcast VOICE_JOIN/LEAVE via ws, show participants in sidebar for all users
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -155,6 +155,18 @@ func (c *Client) readPump() {
|
|||||||
c.Hub.BroadcastEvent(Event{Type: event.Type, Data: data})
|
c.Hub.BroadcastEvent(Event{Type: event.Type, Data: data})
|
||||||
case EventPresenceUpdate:
|
case EventPresenceUpdate:
|
||||||
c.Hub.BroadcastEvent(event)
|
c.Hub.BroadcastEvent(event)
|
||||||
|
case EventVoiceJoin, EventVoiceLeave, EventVoiceMute, EventVoiceDeafen:
|
||||||
|
// Broadcast voice state changes to all clients with sender info
|
||||||
|
var vData map[string]interface{}
|
||||||
|
if raw, ok := event.Data.(json.RawMessage); ok {
|
||||||
|
json.Unmarshal(raw, &vData)
|
||||||
|
}
|
||||||
|
if vData == nil {
|
||||||
|
vData = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
vData["user_id"] = c.UserID
|
||||||
|
vData["username"] = c.Username
|
||||||
|
c.Hub.BroadcastEvent(Event{Type: event.Type, Data: vData})
|
||||||
case EventVoiceWhisper:
|
case EventVoiceWhisper:
|
||||||
// Forward voice whispers only to the target user, not broadcast
|
// Forward voice whispers only to the target user, not broadcast
|
||||||
var whisperData struct {
|
var whisperData struct {
|
||||||
|
|||||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useVoiceStore } from '../stores/voice.ts';
|
|||||||
import { useAuthStore } from '../stores/auth.ts';
|
import { useAuthStore } from '../stores/auth.ts';
|
||||||
import { usePermissions } from '../lib/usePermissions.ts';
|
import { usePermissions } from '../lib/usePermissions.ts';
|
||||||
import { useChannelStore } from '../stores/channel.ts';
|
import { useChannelStore } from '../stores/channel.ts';
|
||||||
|
import { useVoicePresenceStore } from '../stores/voicePresence.ts';
|
||||||
import { api } from '../lib/api.ts';
|
import { api } from '../lib/api.ts';
|
||||||
|
|
||||||
interface VoiceChannelProps {
|
interface VoiceChannelProps {
|
||||||
@@ -13,7 +14,8 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
|
|||||||
const currentRoom = useVoiceStore((state) => state.currentRoom);
|
const currentRoom = useVoiceStore((state) => state.currentRoom);
|
||||||
const isConnected = useVoiceStore((state) => state.isConnected);
|
const isConnected = useVoiceStore((state) => state.isConnected);
|
||||||
const joinVoice = useVoiceStore((state) => state.joinVoice);
|
const joinVoice = useVoiceStore((state) => state.joinVoice);
|
||||||
const participants = useVoiceStore((state) => state.participants);
|
const liveParticipants = useVoiceStore((state) => state.participants);
|
||||||
|
const presenceParticipants = useVoicePresenceStore((s) => s.getParticipants(channelId));
|
||||||
|
|
||||||
const currentUserId = useAuthStore((state) => state.user?.id);
|
const currentUserId = useAuthStore((state) => state.user?.id);
|
||||||
const activeChannelId = useChannelStore((s) => s.activeChannelId);
|
const activeChannelId = useChannelStore((s) => s.activeChannelId);
|
||||||
@@ -40,6 +42,11 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
|
|||||||
|
|
||||||
const isActive = currentRoom === channelId;
|
const isActive = currentRoom === channelId;
|
||||||
|
|
||||||
|
// Use live participants when in the room, otherwise fall back to presence
|
||||||
|
const displayParticipants = isActive && isConnected
|
||||||
|
? liveParticipants.map(p => ({ userId: p.identity, username: p.username, isMuted: p.isMuted, isSpeaking: p.isSpeaking, hasVideo: p.hasVideo, isScreenSharing: p.isScreenSharing }))
|
||||||
|
: presenceParticipants.map(p => ({ userId: p.userId, username: p.username, isMuted: true, isSpeaking: false, hasVideo: false, isScreenSharing: false }));
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
console.log("[VoiceChannel] joining voice:", channelId, channelName);
|
console.log("[VoiceChannel] joining voice:", channelId, channelName);
|
||||||
@@ -69,19 +76,19 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
|
|||||||
🔊
|
🔊
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">{channelName}</span>
|
<span className="truncate">{channelName}</span>
|
||||||
{isActive && participants.length > 0 && (
|
{displayParticipants.length > 0 && (
|
||||||
<span className="ml-auto text-xxs text-gb-fg-f">
|
<span className="ml-auto text-xxs text-gb-fg-f">
|
||||||
[{participants.length}]
|
[{displayParticipants.length}]
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{isActive && (
|
{displayParticipants.length > 0 && (
|
||||||
<div className="pl-7 py-0.5">
|
<div className="pl-7 py-0.5">
|
||||||
{participants.map((p) => {
|
{displayParticipants.map((p) => {
|
||||||
const isMe = p.identity === currentUserId;
|
const isMe = p.userId === currentUserId;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.identity}
|
key={p.userId}
|
||||||
className="text-xxs text-gb-fg-f flex items-center gap-1 group"
|
className="text-xxs text-gb-fg-f flex items-center gap-1 group"
|
||||||
>
|
>
|
||||||
<span className={p.isSpeaking ? 'text-gb-green' : 'text-gb-fg-f'}>
|
<span className={p.isSpeaking ? 'text-gb-green' : 'text-gb-fg-f'}>
|
||||||
@@ -93,12 +100,12 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
|
|||||||
{p.isScreenSharing && <span className="text-gb-aqua" title="Sharing screen">🖥</span>}
|
{p.isScreenSharing && <span className="text-gb-aqua" title="Sharing screen">🖥</span>}
|
||||||
{p.hasVideo && <span className="text-gb-orange" title="Camera on">🎥</span>}
|
{p.hasVideo && <span className="text-gb-orange" title="Camera on">🎥</span>}
|
||||||
|
|
||||||
{!isMe && (
|
{!isMe && isActive && (
|
||||||
<div className="ml-auto opacity-0 group-hover:opacity-100 flex items-center gap-1">
|
<div className="ml-auto opacity-0 group-hover:opacity-100 flex items-center gap-1">
|
||||||
{canMute && !p.isMuted && (
|
{canMute && !p.isMuted && (
|
||||||
<button
|
<button
|
||||||
className="text-gb-fg-f hover:text-gb-red"
|
className="text-gb-fg-f hover:text-gb-red"
|
||||||
onClick={(e) => { e.stopPropagation(); handleMute(p.identity); }}
|
onClick={(e) => { e.stopPropagation(); handleMute(p.userId); }}
|
||||||
title={`Mute ${p.username}`}
|
title={`Mute ${p.username}`}
|
||||||
>
|
>
|
||||||
[mute]
|
[mute]
|
||||||
@@ -106,7 +113,7 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="text-gb-fg-f hover:text-gb-orange"
|
className="text-gb-fg-f hover:text-gb-orange"
|
||||||
onClick={(e) => { e.stopPropagation(); whisperTo(p.identity, p.username); }}
|
onClick={(e) => { e.stopPropagation(); whisperTo(p.userId, p.username); }}
|
||||||
title={`Whisper to ${p.username}`}
|
title={`Whisper to ${p.username}`}
|
||||||
>
|
>
|
||||||
[whisper]
|
[whisper]
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface VoicePresenceEntry {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoicePresenceState {
|
||||||
|
// channelId -> userId -> entry
|
||||||
|
presence: Record<string, Record<string, VoicePresenceEntry>>;
|
||||||
|
_handleJoin: (entry: VoicePresenceEntry) => void;
|
||||||
|
_handleLeave: (userId: string, channelId: string) => void;
|
||||||
|
getParticipants: (channelId: string) => VoicePresenceEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVoicePresenceStore = create<VoicePresenceState>((set, get) => ({
|
||||||
|
presence: {},
|
||||||
|
|
||||||
|
_handleJoin: (entry) => {
|
||||||
|
set((state) => {
|
||||||
|
const room = { ...(state.presence[entry.channelId] || {}) };
|
||||||
|
room[entry.userId] = entry;
|
||||||
|
return { presence: { ...state.presence, [entry.channelId]: room } };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleLeave: (userId, channelId) => {
|
||||||
|
set((state) => {
|
||||||
|
const room = { ...(state.presence[channelId] || {}) };
|
||||||
|
delete room[userId];
|
||||||
|
const next = { ...state.presence, [channelId]: room };
|
||||||
|
if (Object.keys(room).length === 0) delete next[channelId];
|
||||||
|
return { presence: next };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getParticipants: (channelId) => {
|
||||||
|
const room = get().presence[channelId];
|
||||||
|
return room ? Object.values(room) : [];
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -5,6 +5,7 @@ import { useServerStore } from './server.ts';
|
|||||||
import { usePresenceStore } from './presence.ts';
|
import { usePresenceStore } from './presence.ts';
|
||||||
import { useTypingStore } from './typing.ts';
|
import { useTypingStore } from './typing.ts';
|
||||||
import { useVoiceStore } from './voice.ts';
|
import { useVoiceStore } from './voice.ts';
|
||||||
|
import { useVoicePresenceStore } from './voicePresence.ts';
|
||||||
import { useAuthStore } from './auth.ts';
|
import { useAuthStore } from './auth.ts';
|
||||||
import { useConversationStore } from './conversation.ts';
|
import { useConversationStore } from './conversation.ts';
|
||||||
import { useReadStatesStore } from './readStates.ts';
|
import { useReadStatesStore } from './readStates.ts';
|
||||||
@@ -274,6 +275,24 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'VOICE_JOIN': {
|
||||||
|
const { room_name, user_id, username } = payload as Record<string, string>;
|
||||||
|
if (room_name && user_id && username) {
|
||||||
|
useVoicePresenceStore.getState()._handleJoin({
|
||||||
|
userId: user_id,
|
||||||
|
username,
|
||||||
|
channelId: room_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'VOICE_LEAVE': {
|
||||||
|
const { room_name, user_id } = payload as Record<string, string>;
|
||||||
|
if (room_name && user_id) {
|
||||||
|
useVoicePresenceStore.getState()._handleLeave(user_id, room_name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AudioRenderers.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandDropdown.tsx","./src/components/CommandManager.tsx","./src/components/ConnectionStatus.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DeviceSettingsModal.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/ExpandableImage.tsx","./src/components/FeatureRequestsPanel.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/FormatToolbar.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallBanner.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageInput.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/NotificationPrompt.tsx","./src/components/PinnedMessages.tsx","./src/components/Poll.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/slashCommands.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/featureRequest.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AudioRenderers.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandDropdown.tsx","./src/components/CommandManager.tsx","./src/components/ConnectionStatus.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DeviceSettingsModal.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/ExpandableImage.tsx","./src/components/FeatureRequestsPanel.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/FormatToolbar.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallBanner.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageInput.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/NotificationPrompt.tsx","./src/components/PinnedMessages.tsx","./src/components/Poll.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/slashCommands.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/featureRequest.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/voicePresence.ts","./src/stores/ws.ts"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user