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