Fix voice status persistence and global visibility across channels
This commit is contained in:
@@ -408,6 +408,18 @@ function handleVoiceJoin(event: Record<string, unknown>, userId: string): void {
|
|||||||
userId,
|
userId,
|
||||||
action: 'join',
|
action: 'join',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also broadcast current voice status if it exists (persisted during moves)
|
||||||
|
const status = connectionManager.getVoiceUserStatus(userId);
|
||||||
|
if (status) {
|
||||||
|
connectionManager.sendToServer(serverId, {
|
||||||
|
type: 'voice_status_update',
|
||||||
|
userId,
|
||||||
|
channelId,
|
||||||
|
isMuted: status.isMuted,
|
||||||
|
isDeafened: status.isDeafened,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVoiceLeave(userId: string): void {
|
function handleVoiceLeave(userId: string): void {
|
||||||
|
|||||||
@@ -112,11 +112,9 @@ class ConnectionManager {
|
|||||||
this.voiceStates.delete(channelId);
|
this.voiceStates.delete(channelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.voiceUserStates.delete(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveAllVoice(userId: string): string | null {
|
leaveAllVoice(userId: string): string | null {
|
||||||
this.voiceUserStates.delete(userId);
|
|
||||||
for (const [channelId, users] of this.voiceStates) {
|
for (const [channelId, users] of this.voiceStates) {
|
||||||
if (users.has(userId)) {
|
if (users.has(userId)) {
|
||||||
users.delete(userId);
|
users.delete(userId);
|
||||||
@@ -560,6 +558,7 @@ export async function registerWebSocket(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
// Leave voice if in one
|
// Leave voice if in one
|
||||||
const leftChannel = connectionManager.leaveAllVoice(removedUserId);
|
const leftChannel = connectionManager.leaveAllVoice(removedUserId);
|
||||||
|
connectionManager.clearVoiceUserStatus(removedUserId);
|
||||||
if (leftChannel) {
|
if (leftChannel) {
|
||||||
// Get channel's server to broadcast
|
// Get channel's server to broadcast
|
||||||
const channel = db.select().from(schema.channels).where(eq(schema.channels.id, leftChannel)).get();
|
const channel = db.select().from(schema.channels).where(eq(schema.channels.id, leftChannel)).get();
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export function ChannelSidebar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleMic();
|
toggleMic();
|
||||||
|
// Broadcast via WebSocket
|
||||||
|
wsSend({ type: 'voice_status', isMuted: !isMuted, isDeafened });
|
||||||
};
|
};
|
||||||
const handleDeafenToggle = async () => {
|
const handleDeafenToggle = async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
@@ -52,8 +54,6 @@ export function ChannelSidebar() {
|
|||||||
room.remoteParticipants.forEach((p) => p.setVolume(0));
|
room.remoteParticipants.forEach((p) => p.setVolume(0));
|
||||||
if (!isMuted)
|
if (!isMuted)
|
||||||
toggleMic();
|
toggleMic();
|
||||||
// Broadcast deafened state via metadata
|
|
||||||
await room.localParticipant.setMetadata(JSON.stringify({ deafened: true }));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Undeafen: restore mic + restore remote audio
|
// Undeafen: restore mic + restore remote audio
|
||||||
@@ -63,7 +63,6 @@ export function ChannelSidebar() {
|
|||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
if (isMuted)
|
if (isMuted)
|
||||||
toggleMic();
|
toggleMic();
|
||||||
await room.localParticipant.setMetadata(JSON.stringify({ deafened: false }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -71,6 +70,8 @@ export function ChannelSidebar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleDeafen();
|
toggleDeafen();
|
||||||
|
// Broadcast via WebSocket
|
||||||
|
wsSend({ type: 'voice_status', isMuted: willDeafen, isDeafened: willDeafen });
|
||||||
};
|
};
|
||||||
const server = servers.find(s => s.id === currentServerId);
|
const server = servers.find(s => s.id === currentServerId);
|
||||||
const currentMember = members.find(m => m.userId === user?.id);
|
const currentMember = members.find(m => m.userId === user?.id);
|
||||||
@@ -93,6 +94,8 @@ export function ChannelSidebar() {
|
|||||||
}
|
}
|
||||||
setCurrentVoiceChannel(channelId);
|
setCurrentVoiceChannel(channelId);
|
||||||
wsSend({ type: 'voice_join', channelId });
|
wsSend({ type: 'voice_join', channelId });
|
||||||
|
// Also broadcast current status immediately after joining
|
||||||
|
wsSend({ type: 'voice_status', isMuted: useVoiceStore.getState().isMuted, isDeafened: useVoiceStore.getState().isDeafened });
|
||||||
navigate(`/channels/${currentServerId}/${channelId}`);
|
navigate(`/channels/${currentServerId}/${channelId}`);
|
||||||
};
|
};
|
||||||
// Floating bottom panel — shared between DM view and server view
|
// Floating bottom panel — shared between DM view and server view
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function VoiceChannel({ channelId, channelName, onClick }) {
|
|||||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS;
|
const voiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS;
|
||||||
const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const participants = useVoiceStore((s) => s.participants);
|
const participants = useVoiceStore((s) => s.participants);
|
||||||
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
const members = useServerStore((s) => s.members);
|
const members = useServerStore((s) => s.members);
|
||||||
const isActive = currentVoiceChannel === channelId;
|
const isActive = currentVoiceChannel === channelId;
|
||||||
return (_jsxs("div", { children: [_jsxs("button", { onClick: onClick, className: `w-full flex items-center gap-1.5 px-2 h-8 rounded-[4px] group transition-colors ${isActive
|
return (_jsxs("div", { children: [_jsxs("button", { onClick: onClick, className: `w-full flex items-center gap-1.5 px-2 h-8 rounded-[4px] group transition-colors ${isActive
|
||||||
@@ -14,11 +15,12 @@ export function VoiceChannel({ channelId, channelName, onClick }) {
|
|||||||
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-modifier-hover'}`, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", className: "flex-shrink-0 opacity-60", children: _jsx("path", { d: "M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" }) }), _jsx("span", { className: "truncate text-[15px] font-medium", children: channelName })] }), voiceUsers.length > 0 && (_jsx("div", { className: "ml-6 mt-0.5 space-y-0.5", children: voiceUsers.map((userId) => {
|
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-modifier-hover'}`, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", className: "flex-shrink-0 opacity-60", children: _jsx("path", { d: "M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" }) }), _jsx("span", { className: "truncate text-[15px] font-medium", children: channelName })] }), voiceUsers.length > 0 && (_jsx("div", { className: "ml-6 mt-0.5 space-y-0.5", children: voiceUsers.map((userId) => {
|
||||||
const member = members.find(m => m.userId === userId);
|
const member = members.find(m => m.userId === userId);
|
||||||
const participant = participants.find(p => p.userId === userId);
|
const participant = participants.find(p => p.userId === userId);
|
||||||
|
const voiceState = voiceUserStates.get(userId);
|
||||||
const displayName = member?.user.displayName ?? member?.user.username ?? participant?.username ?? userId;
|
const displayName = member?.user.displayName ?? member?.user.username ?? participant?.username ?? userId;
|
||||||
const avatar = member?.user.avatar ?? null;
|
const avatar = member?.user.avatar ?? null;
|
||||||
const status = member?.user.status;
|
const status = member?.user.status;
|
||||||
const isParticipantDeafened = participant?.isDeafened ?? false;
|
const isParticipantDeafened = voiceState?.isDeafened ?? participant?.isDeafened ?? false;
|
||||||
const isMuted = participant?.isMuted ?? false;
|
const isMuted = voiceState?.isMuted ?? participant?.isMuted ?? false;
|
||||||
const hasCamera = participant?.isCameraOn ?? false;
|
const hasCamera = participant?.isCameraOn ?? false;
|
||||||
const isScreenSharing = participant?.isScreenSharing ?? false;
|
const isScreenSharing = participant?.isScreenSharing ?? false;
|
||||||
return (_jsxs("div", { className: "flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-modifier-hover transition-colors", children: [_jsx(Avatar, { src: avatar, name: displayName, size: 20, status: status }), _jsx("span", { className: "text-[13px] text-discord-text-secondary truncate flex-1 min-w-0", children: displayName }), _jsxs("div", { className: "flex items-center gap-1 flex-shrink-0", children: [isParticipantDeafened ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-red", children: [_jsx("path", { d: "M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" }), _jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] })) : isMuted ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-red", children: [_jsx("path", { d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" }), _jsx("path", { d: "M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" }), _jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] })) : null, hasCamera && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("path", { d: "M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" }) })), isScreenSharing && (_jsx("span", { className: "bg-discord-green text-white text-[9px] font-bold px-1 rounded leading-[14px]", children: "LIVE" }))] })] }, userId));
|
return (_jsxs("div", { className: "flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-modifier-hover transition-colors", children: [_jsx(Avatar, { src: avatar, name: displayName, size: 20, status: status }), _jsx("span", { className: "text-[13px] text-discord-text-secondary truncate flex-1 min-w-0", children: displayName }), _jsxs("div", { className: "flex items-center gap-1 flex-shrink-0", children: [isParticipantDeafened ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-red", children: [_jsx("path", { d: "M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" }), _jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] })) : isMuted ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-red", children: [_jsx("path", { d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" }), _jsx("path", { d: "M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" }), _jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] })) : null, hasCamera && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("path", { d: "M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" }) })), isScreenSharing && (_jsx("span", { className: "bg-discord-green text-white text-[9px] font-bold px-1 rounded leading-[14px]", children: "LIVE" }))] })] }, userId));
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export function useLiveKit() {
|
|||||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||||
const videoQuality = useVoiceStore((s) => s.videoQuality);
|
const videoQuality = useVoiceStore((s) => s.videoQuality);
|
||||||
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
|
|
||||||
const updateParticipants = useCallback(() => {
|
const updateParticipants = useCallback(() => {
|
||||||
const r = roomRef.current;
|
const r = roomRef.current;
|
||||||
@@ -122,13 +123,21 @@ export function useLiveKit() {
|
|||||||
else if (pub.source === Track.Source.Camera && p.isCameraEnabled) videoTrack = mt;
|
else if (pub.source === Track.Source.Camera && p.isCameraEnabled) videoTrack = mt;
|
||||||
else if (pub.source === Track.Source.ScreenShare && p.isScreenShareEnabled) screenTrack = mt;
|
else if (pub.source === Track.Source.ScreenShare && p.isScreenShareEnabled) screenTrack = mt;
|
||||||
});
|
});
|
||||||
|
const userState = useVoiceStore.getState().voiceUserStates.get(userId);
|
||||||
let isDeafened = false;
|
let isDeafened = false;
|
||||||
|
let isMuted = !p.isMicrophoneEnabled;
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
isDeafened = useVoiceStore.getState().isDeafened;
|
isDeafened = useVoiceStore.getState().isDeafened;
|
||||||
|
isMuted = useVoiceStore.getState().isMuted;
|
||||||
} else {
|
} else {
|
||||||
isDeafened = useVoiceStore.getState().deafenedUserIds.has(userId);
|
isDeafened = userState?.isDeafened ?? useVoiceStore.getState().deafenedUserIds.has(userId);
|
||||||
|
if (userState) {
|
||||||
|
isMuted = userState.isMuted;
|
||||||
}
|
}
|
||||||
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted: !p.isMicrophoneEnabled, isDeafened, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
|
}
|
||||||
|
|
||||||
|
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted, isDeafened, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
|
||||||
};
|
};
|
||||||
processParticipant(r.localParticipant, true);
|
processParticipant(r.localParticipant, true);
|
||||||
r.remoteParticipants.forEach((p) => processParticipant(p, false));
|
r.remoteParticipants.forEach((p) => processParticipant(p, false));
|
||||||
@@ -318,6 +327,10 @@ export function useLiveKit() {
|
|||||||
}
|
}
|
||||||
}, [isScreenSharing, videoQuality, updateParticipants]);
|
}, [isScreenSharing, videoQuality, updateParticipants]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateParticipants();
|
||||||
|
}, [voiceUserStates, isMuted, isDeafened, updateParticipants]);
|
||||||
|
|
||||||
// Sync quality changes
|
// Sync quality changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ function handleEvent(event: ServerEvent): void {
|
|||||||
addVoiceUser(event.channelId, event.userId);
|
addVoiceUser(event.channelId, event.userId);
|
||||||
} else {
|
} else {
|
||||||
removeVoiceUser(event.channelId, event.userId);
|
removeVoiceUser(event.channelId, event.userId);
|
||||||
clearVoiceUserStatus(event.userId);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user