import { useCallback, useEffect, useRef, useState } from 'react'; import { api, getToken, setToken } from './api.js'; import { DispatchSocket } from './ws.js'; import { PttPlayer, startCapture, sosBeep } from './audio.js'; import MapView from './components/MapView.jsx'; import SosPanel from './components/SosPanel.jsx'; import MembersPanel from './components/MembersPanel.jsx'; import PttPanel from './components/PttPanel.jsx'; import Login from './components/Login.jsx'; export default function App() { const [user, setUser] = useState(null); const [members, setMembers] = useState([]); const [groups, setGroups] = useState([]); const [sos, setSos] = useState([]); const [floors, setFloors] = useState({}); const [connected, setConnected] = useState(false); const [tab, setTab] = useState('members'); const [selectedGroup, setSelectedGroup] = useState(null); const [talking, setTalking] = useState(false); const [muted, setMuted] = useState(false); const sockRef = useRef(null); const playerRef = useRef(null); const captureRef = useRef(null); const mutedRef = useRef(false); mutedRef.current = muted; // restore session on reload useEffect(() => { if (!getToken()) return; api.get('/me').then(setUser).catch(() => setToken('')); }, []); const startSocket = useCallback(() => { sockRef.current?.close(); sockRef.current = new DispatchSocket({ onStatus: setConnected, onWelcome: (d) => { setGroups(d.groups); setMembers(d.members); setSos(d.sos); setFloors(Object.fromEntries(d.floors.map((f) => [f.groupId, f]))); }, onMembers: setMembers, onGroups: setGroups, onSosNew: (alert) => { setSos((prev) => [alert, ...prev.filter((a) => a.id !== alert.id)]); if (!mutedRef.current) sosBeep(4); }, onSosUpdate: (alert) => setSos((prev) => prev.map((a) => (a.id === alert.id ? alert : a))), onFloor: (f) => setFloors((prev) => { if (f.memberId == null) { const { [f.groupId]: _drop, ...rest } = prev; return rest; } return { ...prev, [f.groupId]: f }; }), onAudio: ({ memberId, pcm }) => { if (mutedRef.current || memberId === 0) return; if (!playerRef.current) playerRef.current = new PttPlayer(); playerRef.current.play(pcm); }, }); }, []); useEffect(() => { if (user && !sockRef.current) startSocket(); }, [user, startSocket]); const handleLogin = async (username, password) => { const { token, user: u } = await api.post('/login', { username, password }); setToken(token); setUser(u); }; const logout = () => { sockRef.current?.close(); sockRef.current = null; captureRef.current?.stop(); captureRef.current = null; setToken(''); setUser(null); }; const beginTalk = async () => { if (!selectedGroup || talking) return; try { captureRef.current = await startCapture((pcm) => sockRef.current?.sendAudio(selectedGroup, 0, pcm)); sockRef.current?.send({ type: 'ptt_start', groupId: selectedGroup }); setTalking(true); } catch (err) { console.error('could not start microphone:', err); } }; const endTalk = () => { sockRef.current?.send({ type: 'ptt_stop' }); captureRef.current?.stop(); captureRef.current = null; setTalking(false); }; const ackSos = (id) => api.post(`/sos/${id}/ack`).catch((e) => console.error(e)); const resolveSos = (id) => api.post(`/sos/${id}/resolve`).catch((e) => console.error(e)); if (!user) return ; const openCount = sos.filter((a) => a.state === 'open').length; return (
Dispatch Console
{connected ? 'live' : 'offline'}
{user.username}
); }