Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified public/assets/app/charts/LCLK/LCLK_DEP_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions server/websockets/chatWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export function setupChatWebsocket(
socket.join(sessionId);

socket.on('typing', ({ username }: { username: string }) => {
if (typeof username !== 'string' || username.length === 0 || username.length > 50) {
return;
}
socket.to(sessionId).emit('userTyping', { userId, username });
});

Expand Down
20 changes: 16 additions & 4 deletions server/websockets/globalChatWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ export function setupGlobalChatWebsocket(
return;
}

try {
const user = await mainDb
.selectFrom('users')
.select(['username'])
.where('id', '=', userId)
.executeTakeFirst();
socket.data.username = user?.username || 'Unknown';
} catch (error) {
console.error('[Global Chat] Error fetching username:', error);
socket.data.username = 'Unknown';
}

socket.data.userId = userId;
socket.data.station = station;
socket.data.position = position;
Expand All @@ -160,7 +172,7 @@ export function setupGlobalChatWebsocket(

connectedGlobalChatUsers.set(userId, {
id: userId,
username: user?.username || 'Unknown',
username: socket.data.username,
avatar: avatarUrl,
station: station,
position: position || null,
Expand Down Expand Up @@ -189,11 +201,11 @@ export function setupGlobalChatWebsocket(
}

socket.on('globalTyping', ({ username }: { username: string }) => {
socket.broadcast.emit('globalUserTyping', { userId, username });
socket.broadcast.emit('globalUserTyping', { userId, username: socket.data.username });
});

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

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

connectedGlobalChatUsers.set(user.userId, {
id: user.userId,
username: user.username || existingUser?.username || 'Unknown',
username: socket.data.username,
avatar: avatarUrl || existingUser?.avatar || null,
station: socket.data.station,
position: socket.data.position || null,
Expand Down
25 changes: 21 additions & 4 deletions src/components/chat/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ interface ChatSidebarProps {
isPFATC?: boolean;
unreadSessionCount?: number;
unreadGlobalCount?: number;
onVoiceStateChange?: (_inVoice: boolean) => void;
}

export default function ChatSidebar({
Expand All @@ -71,6 +72,7 @@ export default function ChatSidebar({
isPFATC = false,
unreadSessionCount = 0,
unreadGlobalCount = 0,
onVoiceStateChange,
}: ChatSidebarProps) {
const { user } = useAuth();
const { airports } = useData();
Expand Down Expand Up @@ -141,6 +143,7 @@ export default function ChatSidebar({
const [talkingUsers, setTalkingUsers] = useState<Set<string>>(new Set());
const [audioLevels, setAudioLevels] = useState<Map<string, number>>(new Map());
const [isInVoice, setIsInVoice] = useState(false);
const [voiceDevices, setVoiceDevices] = useState<MediaDeviceInfo[]>([]);

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

useEffect(() => {
onVoiceStateChange?.(isInVoice);
}, [isInVoice, onVoiceStateChange]);

const getConnectionIcon = () => {
if (connectionState.connecting)
return <Wifi className="w-4 h-4 animate-pulse text-yellow-400" />;
Expand Down Expand Up @@ -737,7 +744,7 @@ export default function ChatSidebar({
}, [activeTab, open]);

useEffect(() => {
if (!sessionId || !accessId || !user || !open) return;
if (!sessionId || !accessId || !user) return;

voiceSocketRef.current = createVoiceChatSocket(
sessionId,
Expand Down Expand Up @@ -774,12 +781,14 @@ export default function ChatSidebar({
});
},
() => userVolumesRef.current,
// onDevicesRefreshed — no-op here; VoiceChat manages its own device list
undefined
(devices: MediaDeviceInfo[]) => setVoiceDevices(devices),
);

if (voiceSocketRef.current) {
voiceSocketRef.current.socket.emit('get-voice-users');
voiceSocketRef.current.socket.on('user-left-voice', ({ userId: leftId }: { userId: string }) => {
setVoiceUsers((prev) => prev.filter((u) => u.userId !== leftId));
});
}

return () => {
Expand All @@ -788,12 +797,19 @@ export default function ChatSidebar({
voiceSocketRef.current = null;
}
setVoiceUsers([]);
setVoiceDevices([]);
setTalkingUsers(new Set());
setConnectionState({ connected: false, connecting: false, error: null });
setIsInVoice(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, accessId, user, open]);
}, [sessionId, accessId, user]);

useEffect(() => {
if (open && voiceSocketRef.current) {
voiceSocketRef.current.socket.emit('get-voice-users');
}
}, [open]);

useEffect(() => {
try {
Expand Down Expand Up @@ -1073,6 +1089,7 @@ export default function ChatSidebar({
setUserVolumes={setUserVolumes}
talkingUsers={talkingUsers}
audioLevels={audioLevels}
externalDevices={voiceDevices}
/>
<div className="shrink-0 relative mx-5 mb-5 mt-0 rounded-bl-3xl pt-8">
<div
Expand Down
13 changes: 13 additions & 0 deletions src/components/chat/VoiceChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface VoiceChatProps {
setUserVolumes: React.Dispatch<React.SetStateAction<Map<string, number>>>;
talkingUsers: Set<string>;
audioLevels: Map<string, number>;
externalDevices?: MediaDeviceInfo[];
}

export default function VoiceChat({
Expand All @@ -30,6 +31,7 @@ export default function VoiceChat({
setUserVolumes,
talkingUsers,
audioLevels,
externalDevices,
}: VoiceChatProps) {
const { user } = useAuth();

Expand Down Expand Up @@ -59,6 +61,10 @@ export default function VoiceChat({
});

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

useEffect(() => {
if (!isInVoice) return;
if (!navigator.mediaDevices) return;
navigator.mediaDevices.addEventListener('devicechange', refreshDevices);
return () => {
navigator.mediaDevices.removeEventListener('devicechange', refreshDevices);
};
}, [isInVoice, refreshDevices]);

useEffect(() => {
if (externalDevices && externalDevices.length > 0) {
setAudioInputDevices(externalDevices);
}
}, [externalDevices]);

useEffect(() => {
try {
localStorage.setItem('voice-chat-audio-input', selectedAudioInput);
Expand Down
11 changes: 11 additions & 0 deletions src/components/tools/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import {
Info,
MessageCircle,
Phone,
Settings,
Wifi,
WifiOff,
Expand Down Expand Up @@ -120,6 +121,7 @@ export default function Toolbar({
}: ToolbarProps) {
const [runway, setRunway] = useState(activeRunway || '');
const [chatOpen, setChatOpen] = useState(false);
const [isInVoice, setIsInVoice] = useState(false);
const [atisOpen, setAtisOpen] = useState(false);
const [activeUsers, setActiveUsers] = useState<SessionUser[]>([]);
const [unreadMentions, setUnreadMentions] = useState<ChatMention[]>([]);
Expand Down Expand Up @@ -578,6 +580,14 @@ export default function Toolbar({
{unreadMentions.length}
</div>
)}
{isInVoice && !chatOpen && unreadMentions.length === 0 && (
<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">
<Phone className="w-2.5 h-2.5 text-white" />
</div>
)}
{isInVoice && !chatOpen && unreadMentions.length > 0 && (
<Phone className="absolute -bottom-1 -right-1 w-3 h-3 text-green-400" />
)}
</Button>

<Button
Expand Down Expand Up @@ -631,6 +641,7 @@ export default function Toolbar({
isPFATC={isPFATC}
unreadSessionCount={unreadSessionMentions.length}
unreadGlobalCount={unreadGlobalMentions.length}
onVoiceStateChange={setIsInVoice}
/>

<ATIS
Expand Down
25 changes: 20 additions & 5 deletions src/sockets/voiceChatSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,15 @@ export function createVoiceChatSocket(
const pc = peerConnections.get(fromUserId);
if (pc) {
try {
localStream?.getTracks().forEach(track => {
const stream = localStream;
if (!stream) return;
stream.getTracks().forEach(track => {
const existing = pc.getTransceivers().find(t => t.receiver.track.kind === track.kind);
if (existing) {
if (existing.sender.track !== track) existing.sender.replaceTrack(track);
existing.direction = 'sendrecv';
} else {
pc.addTransceiver(track, { direction: 'sendrecv', streams: [localStream!] });
pc.addTransceiver(track, { direction: 'sendrecv', streams: [stream] });
}
});
await pc.setLocalDescription();
Expand All @@ -559,7 +561,7 @@ export function createVoiceChatSocket(
}
});

const cleanup = () => {
const cleanupRTC = () => {
stopAudioLevelMonitoring();
peerConnections.forEach(pc => pc.close());
peerConnections.clear();
Expand All @@ -569,9 +571,22 @@ export function createVoiceChatSocket(
audioElements.clear();
boostGainNodes.forEach(g => { try { g.disconnect(); } catch {/**/} });
boostGainNodes.clear();
remoteAudioMonitorIntervals.forEach(i => clearInterval(i));
remoteAudioMonitorIntervals.clear();
statsIntervals.forEach(i => clearInterval(i));
statsIntervals.clear();
iceHangTimeouts.forEach(t => clearTimeout(t));
iceHangTimeouts.clear();
if (localStream) localStream.getTracks().forEach(t => t.stop());
candidateBufferMap.clear();
makingOfferMap.clear();
ignoreOfferMap.clear();
if (localStream) { localStream.getTracks().forEach(t => t.stop()); localStream = null; }
if (analyser) { try { analyser.disconnect(); } catch {/**/} analyser = null; }
if (microphone) { try { microphone.disconnect(); } catch {/**/} microphone = null; }
};

const cleanup = () => {
cleanupRTC();
if (audioContext && audioContext.state !== 'closed') audioContext.close();
socket.disconnect();
};
Expand Down Expand Up @@ -627,7 +642,7 @@ export function createVoiceChatSocket(
},
leaveVoice: () => {
socket.emit('leave-voice-session');
cleanup();
cleanupRTC();
},
cleanup,
};
Expand Down