secutorpro commited on
Commit
2b8468d
·
verified ·
1 Parent(s): 3cd3caa

app shell: auth, realtime wiring, layout

Browse files
Files changed (1) hide show
  1. web/src/App.jsx +145 -0
web/src/App.jsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { api, getToken, setToken } from './api.js';
3
+ import { DispatchSocket } from './ws.js';
4
+ import { PttPlayer, startCapture, sosBeep } from './audio.js';
5
+ import MapView from './components/MapView.jsx';
6
+ import SosPanel from './components/SosPanel.jsx';
7
+ import MembersPanel from './components/MembersPanel.jsx';
8
+ import PttPanel from './components/PttPanel.jsx';
9
+ import Login from './components/Login.jsx';
10
+
11
+ export default function App() {
12
+ const [user, setUser] = useState(null);
13
+ const [members, setMembers] = useState([]);
14
+ const [groups, setGroups] = useState([]);
15
+ const [sos, setSos] = useState([]);
16
+ const [floors, setFloors] = useState({});
17
+ const [connected, setConnected] = useState(false);
18
+ const [tab, setTab] = useState('members');
19
+ const [selectedGroup, setSelectedGroup] = useState(null);
20
+ const [talking, setTalking] = useState(false);
21
+ const [muted, setMuted] = useState(false);
22
+
23
+ const sockRef = useRef(null);
24
+ const playerRef = useRef(null);
25
+ const captureRef = useRef(null);
26
+ const mutedRef = useRef(false);
27
+ mutedRef.current = muted;
28
+
29
+ // restore session on reload
30
+ useEffect(() => {
31
+ if (!getToken()) return;
32
+ api.get('/me').then(setUser).catch(() => setToken(''));
33
+ }, []);
34
+
35
+ const startSocket = useCallback(() => {
36
+ sockRef.current?.close();
37
+ sockRef.current = new DispatchSocket({
38
+ onStatus: setConnected,
39
+ onWelcome: (d) => {
40
+ setGroups(d.groups);
41
+ setMembers(d.members);
42
+ setSos(d.sos);
43
+ setFloors(Object.fromEntries(d.floors.map((f) => [f.groupId, f])));
44
+ },
45
+ onMembers: setMembers,
46
+ onGroups: setGroups,
47
+ onSosNew: (alert) => {
48
+ setSos((prev) => [alert, ...prev.filter((a) => a.id !== alert.id)]);
49
+ if (!mutedRef.current) sosBeep(4);
50
+ },
51
+ onSosUpdate: (alert) => setSos((prev) => prev.map((a) => (a.id === alert.id ? alert : a))),
52
+ onFloor: (f) =>
53
+ setFloors((prev) => {
54
+ if (f.memberId == null) {
55
+ const { [f.groupId]: _drop, ...rest } = prev;
56
+ return rest;
57
+ }
58
+ return { ...prev, [f.groupId]: f };
59
+ }),
60
+ onAudio: ({ memberId, pcm }) => {
61
+ if (mutedRef.current || memberId === 0) return;
62
+ if (!playerRef.current) playerRef.current = new PttPlayer();
63
+ playerRef.current.play(pcm);
64
+ },
65
+ });
66
+ }, []);
67
+
68
+ useEffect(() => {
69
+ if (user && !sockRef.current) startSocket();
70
+ }, [user, startSocket]);
71
+
72
+ const handleLogin = async (username, password) => {
73
+ const { token, user: u } = await api.post('/login', { username, password });
74
+ setToken(token);
75
+ setUser(u);
76
+ };
77
+
78
+ const logout = () => {
79
+ sockRef.current?.close();
80
+ sockRef.current = null;
81
+ captureRef.current?.stop();
82
+ captureRef.current = null;
83
+ setToken('');
84
+ setUser(null);
85
+ };
86
+
87
+ const beginTalk = async () => {
88
+ if (!selectedGroup || talking) return;
89
+ try {
90
+ captureRef.current = await startCapture((pcm) => sockRef.current?.sendAudio(selectedGroup, 0, pcm));
91
+ sockRef.current?.send({ type: 'ptt_start', groupId: selectedGroup });
92
+ setTalking(true);
93
+ } catch (err) {
94
+ console.error('could not start microphone:', err);
95
+ }
96
+ };
97
+
98
+ const endTalk = () => {
99
+ sockRef.current?.send({ type: 'ptt_stop' });
100
+ captureRef.current?.stop();
101
+ captureRef.current = null;
102
+ setTalking(false);
103
+ };
104
+
105
+ const ackSos = (id) => api.post(`/sos/${id}/ack`).catch((e) => console.error(e));
106
+ const resolveSos = (id) => api.post(`/sos/${id}/resolve`).catch((e) => console.error(e));
107
+
108
+ if (!user) return <Login onLogin={handleLogin} />;
109
+
110
+ const openCount = sos.filter((a) => a.state === 'open').length;
111
+
112
+ return (
113
+ <div className="app">
114
+ <header>
115
+ <div className="brand">Dispatch Console</div>
116
+ <div className={`conn ${connected ? 'on' : 'off'}`}>{connected ? 'live' : 'offline'}</div>
117
+ <span className="muted">{user.username}</span>
118
+ <button className="secondary" onClick={logout}>Log out</button>
119
+ </header>
120
+ <aside>
121
+ <SosPanel sos={sos} openCount={openCount} onAck={ackSos} onResolve={resolveSos} />
122
+ <div className="tabs">
123
+ <button className={tab === 'members' ? 'active' : ''} onClick={() => setTab('members')}>Members</button>
124
+ <button className={tab === 'ptt' ? 'active' : ''} onClick={() => setTab('ptt')}>Intercom</button>
125
+ </div>
126
+ {tab === 'members' ? (
127
+ <MembersPanel members={members} groups={groups} />
128
+ ) : (
129
+ <PttPanel
130
+ groups={groups}
131
+ floors={floors}
132
+ selected={selectedGroup}
133
+ setSelected={setSelectedGroup}
134
+ onTalkStart={beginTalk}
135
+ onTalkStop={endTalk}
136
+ talking={talking}
137
+ muted={muted}
138
+ setMuted={setMuted}
139
+ />
140
+ )}
141
+ </aside>
142
+ <MapView members={members} groups={groups} sos={sos} />
143
+ </div>
144
+ );
145
+ }