Skip to content

Commit 910794c

Browse files
authored
Merge pull request #164 from cephie-studios/voicechat
fix(vc & charts): keep vc open on sidebar close & update LCLK_DEP_1
2 parents 201d925 + 871b316 commit 910794c

7 files changed

Lines changed: 84 additions & 13 deletions

File tree

42.8 KB
Loading

server/websockets/chatWebsocket.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export function setupChatWebsocket(
9696
socket.join(sessionId);
9797

9898
socket.on('typing', ({ username }: { username: string }) => {
99+
if (typeof username !== 'string' || username.length === 0 || username.length > 50) {
100+
return;
101+
}
99102
socket.to(sessionId).emit('userTyping', { userId, username });
100103
});
101104

server/websockets/globalChatWebsocket.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@ export function setupGlobalChatWebsocket(
138138
return;
139139
}
140140

141+
try {
142+
const user = await mainDb
143+
.selectFrom('users')
144+
.select(['username'])
145+
.where('id', '=', userId)
146+
.executeTakeFirst();
147+
socket.data.username = user?.username || 'Unknown';
148+
} catch (error) {
149+
console.error('[Global Chat] Error fetching username:', error);
150+
socket.data.username = 'Unknown';
151+
}
152+
141153
socket.data.userId = userId;
142154
socket.data.station = station;
143155
socket.data.position = position;
@@ -160,7 +172,7 @@ export function setupGlobalChatWebsocket(
160172

161173
connectedGlobalChatUsers.set(userId, {
162174
id: userId,
163-
username: user?.username || 'Unknown',
175+
username: socket.data.username,
164176
avatar: avatarUrl,
165177
station: station,
166178
position: position || null,
@@ -189,11 +201,11 @@ export function setupGlobalChatWebsocket(
189201
}
190202

191203
socket.on('globalTyping', ({ username }: { username: string }) => {
192-
socket.broadcast.emit('globalUserTyping', { userId, username });
204+
socket.broadcast.emit('globalUserTyping', { userId, username: socket.data.username });
193205
});
194206

195207
socket.on('globalChatMessage', async ({ user, message }) => {
196-
if (!message || message.length > 500) return;
208+
if (!message || message.length > 500 || user.userId !== socket.data.userId) return;
197209

198210
const sanitizedMessage = sanitizeMessage(message, 500);
199211
if (!sanitizedMessage) return;
@@ -208,7 +220,7 @@ export function setupGlobalChatWebsocket(
208220

209221
connectedGlobalChatUsers.set(user.userId, {
210222
id: user.userId,
211-
username: user.username || existingUser?.username || 'Unknown',
223+
username: socket.data.username,
212224
avatar: avatarUrl || existingUser?.avatar || null,
213225
station: socket.data.station,
214226
position: socket.data.position || null,

src/components/chat/ChatSidebar.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface ChatSidebarProps {
5757
isPFATC?: boolean;
5858
unreadSessionCount?: number;
5959
unreadGlobalCount?: number;
60+
onVoiceStateChange?: (_inVoice: boolean) => void;
6061
}
6162

6263
export default function ChatSidebar({
@@ -71,6 +72,7 @@ export default function ChatSidebar({
7172
isPFATC = false,
7273
unreadSessionCount = 0,
7374
unreadGlobalCount = 0,
75+
onVoiceStateChange,
7476
}: ChatSidebarProps) {
7577
const { user } = useAuth();
7678
const { airports } = useData();
@@ -141,6 +143,7 @@ export default function ChatSidebar({
141143
const [talkingUsers, setTalkingUsers] = useState<Set<string>>(new Set());
142144
const [audioLevels, setAudioLevels] = useState<Map<string, number>>(new Map());
143145
const [isInVoice, setIsInVoice] = useState(false);
146+
const [voiceDevices, setVoiceDevices] = useState<MediaDeviceInfo[]>([]);
144147

145148
const voiceSocketRef = useRef<ReturnType<
146149
typeof createVoiceChatSocket
@@ -169,6 +172,10 @@ export default function ChatSidebar({
169172
const userVolumesRef = useRef(userVolumes);
170173
useEffect(() => { userVolumesRef.current = userVolumes; }, [userVolumes]);
171174

175+
useEffect(() => {
176+
onVoiceStateChange?.(isInVoice);
177+
}, [isInVoice, onVoiceStateChange]);
178+
172179
const getConnectionIcon = () => {
173180
if (connectionState.connecting)
174181
return <Wifi className="w-4 h-4 animate-pulse text-yellow-400" />;
@@ -737,7 +744,7 @@ export default function ChatSidebar({
737744
}, [activeTab, open]);
738745

739746
useEffect(() => {
740-
if (!sessionId || !accessId || !user || !open) return;
747+
if (!sessionId || !accessId || !user) return;
741748

742749
voiceSocketRef.current = createVoiceChatSocket(
743750
sessionId,
@@ -774,12 +781,14 @@ export default function ChatSidebar({
774781
});
775782
},
776783
() => userVolumesRef.current,
777-
// onDevicesRefreshed — no-op here; VoiceChat manages its own device list
778-
undefined
784+
(devices: MediaDeviceInfo[]) => setVoiceDevices(devices),
779785
);
780786

781787
if (voiceSocketRef.current) {
782788
voiceSocketRef.current.socket.emit('get-voice-users');
789+
voiceSocketRef.current.socket.on('user-left-voice', ({ userId: leftId }: { userId: string }) => {
790+
setVoiceUsers((prev) => prev.filter((u) => u.userId !== leftId));
791+
});
783792
}
784793

785794
return () => {
@@ -788,12 +797,19 @@ export default function ChatSidebar({
788797
voiceSocketRef.current = null;
789798
}
790799
setVoiceUsers([]);
800+
setVoiceDevices([]);
791801
setTalkingUsers(new Set());
792802
setConnectionState({ connected: false, connecting: false, error: null });
793803
setIsInVoice(false);
794804
};
795805
// eslint-disable-next-line react-hooks/exhaustive-deps
796-
}, [sessionId, accessId, user, open]);
806+
}, [sessionId, accessId, user]);
807+
808+
useEffect(() => {
809+
if (open && voiceSocketRef.current) {
810+
voiceSocketRef.current.socket.emit('get-voice-users');
811+
}
812+
}, [open]);
797813

798814
useEffect(() => {
799815
try {
@@ -1073,6 +1089,7 @@ export default function ChatSidebar({
10731089
setUserVolumes={setUserVolumes}
10741090
talkingUsers={talkingUsers}
10751091
audioLevels={audioLevels}
1092+
externalDevices={voiceDevices}
10761093
/>
10771094
<div className="shrink-0 relative mx-5 mb-5 mt-0 rounded-bl-3xl pt-8">
10781095
<div

src/components/chat/VoiceChat.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface VoiceChatProps {
1919
setUserVolumes: React.Dispatch<React.SetStateAction<Map<string, number>>>;
2020
talkingUsers: Set<string>;
2121
audioLevels: Map<string, number>;
22+
externalDevices?: MediaDeviceInfo[];
2223
}
2324

2425
export default function VoiceChat({
@@ -30,6 +31,7 @@ export default function VoiceChat({
3031
setUserVolumes,
3132
talkingUsers,
3233
audioLevels,
34+
externalDevices,
3335
}: VoiceChatProps) {
3436
const { user } = useAuth();
3537

@@ -59,6 +61,10 @@ export default function VoiceChat({
5961
});
6062

6163
const refreshDevices = useCallback(async () => {
64+
if (!navigator.mediaDevices?.enumerateDevices) {
65+
console.warn('Media devices API not available');
66+
return;
67+
}
6268
try {
6369
const devices = await navigator.mediaDevices.enumerateDevices();
6470
const audioInputs = devices.filter((d) => d.kind === 'audioinput');
@@ -70,12 +76,19 @@ export default function VoiceChat({
7076

7177
useEffect(() => {
7278
if (!isInVoice) return;
79+
if (!navigator.mediaDevices) return;
7380
navigator.mediaDevices.addEventListener('devicechange', refreshDevices);
7481
return () => {
7582
navigator.mediaDevices.removeEventListener('devicechange', refreshDevices);
7683
};
7784
}, [isInVoice, refreshDevices]);
7885

86+
useEffect(() => {
87+
if (externalDevices && externalDevices.length > 0) {
88+
setAudioInputDevices(externalDevices);
89+
}
90+
}, [externalDevices]);
91+
7992
useEffect(() => {
8093
try {
8194
localStorage.setItem('voice-chat-audio-input', selectedAudioInput);

src/components/tools/Toolbar.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
22
import {
33
Info,
44
MessageCircle,
5+
Phone,
56
Settings,
67
Wifi,
78
WifiOff,
@@ -120,6 +121,7 @@ export default function Toolbar({
120121
}: ToolbarProps) {
121122
const [runway, setRunway] = useState(activeRunway || '');
122123
const [chatOpen, setChatOpen] = useState(false);
124+
const [isInVoice, setIsInVoice] = useState(false);
123125
const [atisOpen, setAtisOpen] = useState(false);
124126
const [activeUsers, setActiveUsers] = useState<SessionUser[]>([]);
125127
const [unreadMentions, setUnreadMentions] = useState<ChatMention[]>([]);
@@ -578,6 +580,14 @@ export default function Toolbar({
578580
{unreadMentions.length}
579581
</div>
580582
)}
583+
{isInVoice && !chatOpen && unreadMentions.length === 0 && (
584+
<div className="absolute -top-2 -right-2 flex items-center justify-center w-5 h-5 rounded-full bg-green-500 border-2 border-zinc-900">
585+
<Phone className="w-2.5 h-2.5 text-white" />
586+
</div>
587+
)}
588+
{isInVoice && !chatOpen && unreadMentions.length > 0 && (
589+
<Phone className="absolute -bottom-1 -right-1 w-3 h-3 text-green-400" />
590+
)}
581591
</Button>
582592

583593
<Button
@@ -631,6 +641,7 @@ export default function Toolbar({
631641
isPFATC={isPFATC}
632642
unreadSessionCount={unreadSessionMentions.length}
633643
unreadGlobalCount={unreadGlobalMentions.length}
644+
onVoiceStateChange={setIsInVoice}
634645
/>
635646

636647
<ATIS

src/sockets/voiceChatSocket.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,13 +542,15 @@ export function createVoiceChatSocket(
542542
const pc = peerConnections.get(fromUserId);
543543
if (pc) {
544544
try {
545-
localStream?.getTracks().forEach(track => {
545+
const stream = localStream;
546+
if (!stream) return;
547+
stream.getTracks().forEach(track => {
546548
const existing = pc.getTransceivers().find(t => t.receiver.track.kind === track.kind);
547549
if (existing) {
548550
if (existing.sender.track !== track) existing.sender.replaceTrack(track);
549551
existing.direction = 'sendrecv';
550552
} else {
551-
pc.addTransceiver(track, { direction: 'sendrecv', streams: [localStream!] });
553+
pc.addTransceiver(track, { direction: 'sendrecv', streams: [stream] });
552554
}
553555
});
554556
await pc.setLocalDescription();
@@ -559,7 +561,7 @@ export function createVoiceChatSocket(
559561
}
560562
});
561563

562-
const cleanup = () => {
564+
const cleanupRTC = () => {
563565
stopAudioLevelMonitoring();
564566
peerConnections.forEach(pc => pc.close());
565567
peerConnections.clear();
@@ -569,9 +571,22 @@ export function createVoiceChatSocket(
569571
audioElements.clear();
570572
boostGainNodes.forEach(g => { try { g.disconnect(); } catch {/**/} });
571573
boostGainNodes.clear();
574+
remoteAudioMonitorIntervals.forEach(i => clearInterval(i));
575+
remoteAudioMonitorIntervals.clear();
576+
statsIntervals.forEach(i => clearInterval(i));
577+
statsIntervals.clear();
572578
iceHangTimeouts.forEach(t => clearTimeout(t));
573579
iceHangTimeouts.clear();
574-
if (localStream) localStream.getTracks().forEach(t => t.stop());
580+
candidateBufferMap.clear();
581+
makingOfferMap.clear();
582+
ignoreOfferMap.clear();
583+
if (localStream) { localStream.getTracks().forEach(t => t.stop()); localStream = null; }
584+
if (analyser) { try { analyser.disconnect(); } catch {/**/} analyser = null; }
585+
if (microphone) { try { microphone.disconnect(); } catch {/**/} microphone = null; }
586+
};
587+
588+
const cleanup = () => {
589+
cleanupRTC();
575590
if (audioContext && audioContext.state !== 'closed') audioContext.close();
576591
socket.disconnect();
577592
};
@@ -627,7 +642,7 @@ export function createVoiceChatSocket(
627642
},
628643
leaveVoice: () => {
629644
socket.emit('leave-voice-session');
630-
cleanup();
645+
cleanupRTC();
631646
},
632647
cleanup,
633648
};

0 commit comments

Comments
 (0)