sameernotes commited on
Commit
47a00d3
·
verified ·
1 Parent(s): d512275

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +25 -0
  2. app.js +442 -0
  3. data/users.json +5 -0
  4. package-lock.json +1032 -0
  5. package.json +16 -0
  6. public/index.html +1382 -0
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+ # Use an official Node.js runtime as the base image
3
+ FROM node:22.1.0
4
+
5
+ WORKDIR /usr/src/app
6
+
7
+ # Copy only package files first for better layer caching
8
+ # Use 1000:1000 for non-root user/group typically used in these images
9
+ COPY --chown=1000:1000 package.json package-lock.json ./
10
+
11
+ # Install production dependencies only
12
+ RUN npm install --production --ignore-scripts
13
+
14
+ # Copy the rest of the application files
15
+ # This includes app.js, public/index.html, data/users.json (initially empty if not existing)
16
+ COPY --chown=1000:1000 . .
17
+
18
+ # Expose the port Hugging Face expects (usually 7860)
19
+ EXPOSE 7860
20
+
21
+ # Ensure the app runs as non-root user (good practice)
22
+ USER 1000
23
+
24
+ # Start the application using the start script from package.json
25
+ CMD ["npm", "start"]
app.js ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // --- START: Additions/Modifications in app.js ---
2
+
3
+ const express = require('express');
4
+ const http = require('http');
5
+ const socketIo = require('socket.io');
6
+ const path = require('path');
7
+ const fs = require('fs').promises; // Use promise-based fs
8
+ // const bcrypt = require('bcrypt'); // Still commented out, needed for secure apps
9
+
10
+ // Create express app and server
11
+ const app = express();
12
+ const server = http.createServer(app);
13
+ const io = socketIo(server);
14
+
15
+ // --- Configuration ---
16
+ const PUBLIC_DIR = path.join(__dirname, 'public');
17
+ const DATA_DIR = path.join(__dirname, 'data'); // Directory for data files
18
+ const USERS_FILE_PATH = path.join(DATA_DIR, 'users.json'); // Path to users file
19
+
20
+ // Serve static files from the 'public' directory
21
+ app.use(express.static(PUBLIC_DIR));
22
+
23
+ // --- State ---
24
+ let registeredUsers = {}; // Will be loaded from file
25
+ // onlineUsers structure: { socketId: { username, isTyping, inCallWith: null | string (socketId) } }
26
+ const onlineUsers = {};
27
+ const usernameToSocketId = {}; // Helper map: { username: socketId }
28
+
29
+ // --- File Helper Functions (loadUsers, saveUsers - remain the same) ---
30
+ // ... (Keep existing loadUsers and saveUsers functions) ...
31
+
32
+ async function loadUsers() {
33
+ try {
34
+ await fs.mkdir(DATA_DIR, { recursive: true });
35
+ const data = await fs.readFile(USERS_FILE_PATH, 'utf8');
36
+ return JSON.parse(data);
37
+ } catch (error) {
38
+ if (error.code === 'ENOENT') {
39
+ console.log('users.json not found, starting with empty user list.');
40
+ return {};
41
+ } else if (error instanceof SyntaxError) {
42
+ console.error('Error parsing users.json:', error);
43
+ return {};
44
+ } else {
45
+ console.error('Error loading users.json:', error);
46
+ return {};
47
+ }
48
+ }
49
+ }
50
+
51
+ async function saveUsers(usersData) {
52
+ try {
53
+ await fs.mkdir(DATA_DIR, { recursive: true });
54
+ const data = JSON.stringify(usersData, null, 2);
55
+ await fs.writeFile(USERS_FILE_PATH, data, 'utf8');
56
+ console.log('User data saved to users.json');
57
+ } catch (error) {
58
+ console.error('Error saving users.json:', error);
59
+ }
60
+ }
61
+
62
+
63
+ // --- Socket.IO Logic ---
64
+ io.on('connection', (socket) => {
65
+ console.log('A user connected:', socket.id);
66
+ // Note: currentUsername is only valid *after* successful login within this closure.
67
+ // Use onlineUsers[socket.id]?.username safely elsewhere.
68
+
69
+ // --- Handle Login/Registration Attempt (Modified to track usernameToSocketId) ---
70
+ socket.on('attempt_login', async (credentials) => {
71
+ if (!credentials || !credentials.username || !credentials.password) {
72
+ socket.emit('login_fail', 'Username and password are required.');
73
+ return;
74
+ }
75
+
76
+ const { username, password } = credentials;
77
+
78
+ const isAlreadyOnline = usernameToSocketId[username]; // Check map directly
79
+ if (isAlreadyOnline) {
80
+ socket.emit('login_fail', `User "${username}" is already logged in.`);
81
+ return;
82
+ }
83
+
84
+ if (registeredUsers.hasOwnProperty(username)) {
85
+ if (registeredUsers[username] === password) { // INSECURE
86
+ console.log(`Login success: ${username} (${socket.id})`);
87
+ loginUser(socket, username);
88
+ } else {
89
+ console.log(`Login fail: Invalid password for ${username} (${socket.id})`);
90
+ socket.emit('login_fail', 'Invalid username or password.');
91
+ }
92
+ } else {
93
+ console.log(`Registering new user: ${username} (${socket.id})`);
94
+ registeredUsers[username] = password; // INSECURE
95
+ await saveUsers(registeredUsers);
96
+ loginUser(socket, username);
97
+ }
98
+ });
99
+
100
+ // Modified loginUser to update maps and state
101
+ function loginUser(socketInstance, username) {
102
+ // Store user info
103
+ onlineUsers[socketInstance.id] = {
104
+ username: username,
105
+ isTyping: false,
106
+ inCallWith: null // Initially not in a call
107
+ };
108
+ usernameToSocketId[username] = socketInstance.id; // Add to lookup map
109
+
110
+ socketInstance.emit('login_success', username);
111
+ io.emit('user_join', username);
112
+ io.emit('user_list', getUsersPublicInfo());
113
+ }
114
+
115
+ // --- Message Handling (remains the same) ---
116
+ socket.on('message', (data) => {
117
+ const senderInfo = onlineUsers[socket.id];
118
+ if (!senderInfo || !data || !data.text) return;
119
+
120
+ if (senderInfo.isTyping) {
121
+ senderInfo.isTyping = false;
122
+ // Don't need to exclude sender ID here as getUsersTyping doesn't include self by default logic
123
+ socket.broadcast.emit('user_typing_status', getUsersTyping());
124
+ }
125
+
126
+ io.emit('message', {
127
+ username: senderInfo.username,
128
+ text: data.text,
129
+ timestamp: new Date().toISOString() // Use ISO string for consistency
130
+ });
131
+ });
132
+
133
+ // --- Typing Indicator Handling (remains the same) ---
134
+ socket.on('typing', () => {
135
+ const userInfo = onlineUsers[socket.id];
136
+ if (userInfo && !userInfo.isTyping) {
137
+ userInfo.isTyping = true;
138
+ socket.broadcast.emit('user_typing_status', getUsersTyping(socket.id)); // Exclude self
139
+ }
140
+ });
141
+
142
+ socket.on('stop_typing', () => {
143
+ const userInfo = onlineUsers[socket.id];
144
+ if (userInfo && userInfo.isTyping) {
145
+ userInfo.isTyping = false;
146
+ socket.broadcast.emit('user_typing_status', getUsersTyping(socket.id)); // Exclude self
147
+ }
148
+ });
149
+
150
+ // --- Disconnect Handling (Modified to handle calls and maps) ---
151
+ socket.on('disconnect', () => {
152
+ const userInfo = onlineUsers[socket.id];
153
+ if (userInfo) {
154
+ const username = userInfo.username;
155
+ const userInCallWithSocketId = userInfo.inCallWith;
156
+
157
+ console.log(`${username} (${socket.id}) disconnected`);
158
+
159
+ // --- Hang up any active call ---
160
+ if (userInCallWithSocketId && onlineUsers[userInCallWithSocketId]) {
161
+ console.log(`Disconnect: Hanging up call between ${username} and ${onlineUsers[userInCallWithSocketId].username}`);
162
+ io.to(userInCallWithSocketId).emit('call-ended'); // Notify the other user
163
+ onlineUsers[userInCallWithSocketId].inCallWith = null; // Clear call state for the other user
164
+ }
165
+ // --- End hang up ---
166
+
167
+ delete onlineUsers[socket.id]; // Remove from online list
168
+ delete usernameToSocketId[username]; // Remove from lookup map
169
+
170
+ socket.broadcast.emit('user_leave', username);
171
+ io.emit('user_list', getUsersPublicInfo()); // Send updated list
172
+ // Update typing status if the disconnected user was typing
173
+ if (userInfo.isTyping) {
174
+ socket.broadcast.emit('user_typing_status', getUsersTyping());
175
+ }
176
+ } else {
177
+ console.log('User disconnected before login:', socket.id);
178
+ }
179
+ });
180
+
181
+
182
+ // --- START: WebRTC Signaling Handlers ---
183
+
184
+ /**
185
+ * Initiates a call request from caller to target.
186
+ * data: { targetUsername: string }
187
+ */
188
+ socket.on('request-call', (data) => {
189
+ const callerInfo = onlineUsers[socket.id];
190
+ if (!callerInfo) return; // Should not happen if logged in
191
+
192
+ const targetUsername = data.targetUsername;
193
+ const targetSocketId = usernameToSocketId[targetUsername];
194
+ const targetInfo = targetSocketId ? onlineUsers[targetSocketId] : null;
195
+
196
+ console.log(`Call request from ${callerInfo.username} (${socket.id}) to ${targetUsername}`);
197
+
198
+ if (!targetInfo) {
199
+ console.log(`Call target ${targetUsername} not found or offline.`);
200
+ socket.emit('call-denied', { reason: 'User is offline.' });
201
+ return;
202
+ }
203
+
204
+ if (targetInfo.inCallWith) {
205
+ console.log(`Call target ${targetUsername} is already in a call.`);
206
+ socket.emit('call-denied', { reason: `${targetUsername} is busy.` });
207
+ return;
208
+ }
209
+
210
+ if (callerInfo.inCallWith) {
211
+ console.log(`Caller ${callerInfo.username} is already in a call.`);
212
+ socket.emit('call-denied', { reason: `You are already in a call.` });
213
+ return;
214
+ }
215
+
216
+ // Send call offer prompt to the target user
217
+ console.log(`Sending incoming call notification to ${targetUsername} (${targetSocketId})`);
218
+ io.to(targetSocketId).emit('incoming-call', {
219
+ callerUsername: callerInfo.username
220
+ });
221
+ });
222
+
223
+ /**
224
+ * Target user accepts the call.
225
+ * data: { callerUsername: string }
226
+ */
227
+ socket.on('accept-call', (data) => {
228
+ const acceptorInfo = onlineUsers[socket.id]; // The user accepting the call
229
+ if (!acceptorInfo) return;
230
+
231
+ const callerUsername = data.callerUsername;
232
+ const callerSocketId = usernameToSocketId[callerUsername];
233
+ const callerInfo = callerSocketId ? onlineUsers[callerSocketId] : null;
234
+
235
+ console.log(`${acceptorInfo.username} accepted call from ${callerUsername}`);
236
+
237
+ if (!callerInfo) {
238
+ console.log(`Original caller ${callerUsername} not found.`);
239
+ socket.emit('call-denied', { reason: 'Caller went offline.' });
240
+ return;
241
+ }
242
+
243
+ if (callerInfo.inCallWith || acceptorInfo.inCallWith) {
244
+ console.log(`Call conflict: ${callerUsername} or ${acceptorInfo.username} already in call.`);
245
+ // Notify both potential parties that call cannot proceed
246
+ socket.emit('call-denied', { reason: 'Call conflict or one party is busy.' });
247
+ io.to(callerSocketId).emit('call-denied', { reason: 'Call conflict or the other party became busy.'});
248
+ return;
249
+ }
250
+
251
+ // Mark both users as in call with each other
252
+ acceptorInfo.inCallWith = callerSocketId;
253
+ callerInfo.inCallWith = socket.id; // socket.id is the acceptor's socket id
254
+
255
+ // Notify the original caller to start the WebRTC offer process
256
+ io.to(callerSocketId).emit('call-accepted', {
257
+ acceptorUsername: acceptorInfo.username
258
+ });
259
+
260
+ // Notify the acceptor to wait for the offer
261
+ socket.emit('prepare-for-offer', {
262
+ callerUsername: callerInfo.username
263
+ });
264
+
265
+ console.log(`Call established between ${callerUsername} (${callerSocketId}) and ${acceptorInfo.username} (${socket.id})`);
266
+ });
267
+
268
+
269
+ /**
270
+ * Target user rejects the call.
271
+ * data: { callerUsername: string }
272
+ */
273
+ socket.on('reject-call', (data) => {
274
+ const rejectorInfo = onlineUsers[socket.id];
275
+ if (!rejectorInfo) return;
276
+
277
+ const callerUsername = data.callerUsername;
278
+ const callerSocketId = usernameToSocketId[callerUsername];
279
+
280
+ console.log(`${rejectorInfo.username} rejected call from ${callerUsername}`);
281
+
282
+ if (callerSocketId) {
283
+ io.to(callerSocketId).emit('call-rejected', {
284
+ rejectorUsername: rejectorInfo.username
285
+ });
286
+ }
287
+ });
288
+
289
+ /**
290
+ * Relays the WebRTC Offer from caller to acceptor.
291
+ * data: { targetUsername: string, offer: RTCSessionDescriptionInit }
292
+ */
293
+ socket.on('webrtc-offer', (data) => {
294
+ const callerInfo = onlineUsers[socket.id];
295
+ if (!callerInfo) return;
296
+
297
+ const targetUsername = data.targetUsername;
298
+ const targetSocketId = usernameToSocketId[targetUsername];
299
+
300
+ if (targetSocketId && onlineUsers[targetSocketId]?.inCallWith === socket.id) {
301
+ console.log(`Relaying WebRTC offer from ${callerInfo.username} to ${targetUsername}`);
302
+ io.to(targetSocketId).emit('webrtc-offer', {
303
+ offer: data.offer,
304
+ callerUsername: callerInfo.username // Send caller username with the offer
305
+ });
306
+ } else {
307
+ console.warn(`WebRTC Offer: Target ${targetUsername} not found or not in call with ${callerInfo.username}`);
308
+ }
309
+ });
310
+
311
+ /**
312
+ * Relays the WebRTC Answer from acceptor to caller.
313
+ * data: { targetUsername: string (original caller), answer: RTCSessionDescriptionInit }
314
+ */
315
+ socket.on('webrtc-answer', (data) => {
316
+ const acceptorInfo = onlineUsers[socket.id];
317
+ if (!acceptorInfo) return;
318
+
319
+ const targetUsername = data.targetUsername; // This is the original caller
320
+ const targetSocketId = usernameToSocketId[targetUsername];
321
+
322
+ if (targetSocketId && onlineUsers[targetSocketId]?.inCallWith === socket.id) {
323
+ console.log(`Relaying WebRTC answer from ${acceptorInfo.username} to ${targetUsername}`);
324
+ io.to(targetSocketId).emit('webrtc-answer', {
325
+ answer: data.answer,
326
+ acceptorUsername: acceptorInfo.username // Send acceptor username with the answer
327
+ });
328
+ } else {
329
+ console.warn(`WebRTC Answer: Target ${targetUsername} not found or not in call with ${acceptorInfo.username}`);
330
+ }
331
+ });
332
+
333
+ /**
334
+ * Relays ICE Candidates between peers.
335
+ * data: { targetUsername: string, candidate: RTCIceCandidateInit }
336
+ */
337
+ socket.on('webrtc-ice-candidate', (data) => {
338
+ const senderInfo = onlineUsers[socket.id];
339
+ if (!senderInfo) return;
340
+
341
+ const targetUsername = data.targetUsername;
342
+ const targetSocketId = usernameToSocketId[targetUsername];
343
+
344
+ // Check if the target is actually the one sender is in call with
345
+ if (targetSocketId && senderInfo.inCallWith === targetSocketId) {
346
+ // console.log(`Relaying ICE candidate from ${senderInfo.username} to ${targetUsername}`); // Too noisy
347
+ io.to(targetSocketId).emit('webrtc-ice-candidate', {
348
+ candidate: data.candidate
349
+ });
350
+ } else {
351
+ // console.warn(`ICE Candidate: Target ${targetUsername} not found or sender ${senderInfo.username} not in call with them.`);
352
+ }
353
+ });
354
+
355
+ /**
356
+ * Handles user explicitly hanging up the call.
357
+ * data: {} (might include target if needed, but server knows from inCallWith)
358
+ */
359
+ socket.on('hangup-call', () => {
360
+ const userInfo = onlineUsers[socket.id];
361
+ if (!userInfo || !userInfo.inCallWith) {
362
+ // Not in a call, nothing to hang up
363
+ return;
364
+ }
365
+
366
+ const targetSocketId = userInfo.inCallWith;
367
+ const targetInfo = onlineUsers[targetSocketId];
368
+
369
+ console.log(`${userInfo.username} is hanging up call with ${targetInfo?.username || 'unknown'}`);
370
+
371
+ // Notify the other user
372
+ if (targetInfo) {
373
+ io.to(targetSocketId).emit('call-ended');
374
+ targetInfo.inCallWith = null; // Clear target's call state
375
+ }
376
+
377
+ // Clear caller's call state
378
+ userInfo.inCallWith = null;
379
+
380
+ // Optional: send confirmation back to the hanger-upper
381
+ socket.emit('call-ended');
382
+ });
383
+
384
+ // --- END: WebRTC Signaling Handlers ---
385
+
386
+ });
387
+
388
+ // --- Helper Functions (Modified getUsersTyping) ---
389
+ function getUsersPublicInfo() {
390
+ // Return username and potentially busy status? For now just usernames.
391
+ return Object.values(onlineUsers).map(u => u.username);
392
+ }
393
+
394
+ // Modified to exclude the caller from the list sent back to them
395
+ function getUsersTyping(excludeSocketId = null) {
396
+ return Object.entries(onlineUsers)
397
+ .filter(([id, user]) => id !== excludeSocketId && user.isTyping)
398
+ .map(([id, user]) => user.username);
399
+ }
400
+
401
+ // --- Find Local IP (remains the same) ---
402
+ function getLocalIp() {
403
+ // ... (Keep existing getLocalIp function) ...
404
+ const { networkInterfaces } = require('os');
405
+ const nets = networkInterfaces();
406
+ for (const name of Object.keys(nets)) {
407
+ for (const net of nets[name]) {
408
+ if (net.family === 'IPv4' && !net.internal) {
409
+ return net.address;
410
+ }
411
+ }
412
+ }
413
+ return '127.0.0.1';
414
+ }
415
+ const localIp = getLocalIp();
416
+
417
+ // --- Start Server (IIFE remains the same) ---
418
+ (async () => {
419
+ registeredUsers = await loadUsers();
420
+ console.log(`Loaded ${Object.keys(registeredUsers).length} registered users.`);
421
+
422
+ // const PORT = process.env.PORT || 3000; // Find this line or similar
423
+ const PORT = process.env.PORT || 7860; // Change 3000 to 7860
424
+ server.listen(PORT, () => {
425
+ console.log(`Server running on port ${PORT}`);
426
+ console.log(`Access it locally at: http://localhost:${PORT}`);
427
+ if (localIp !== '127.0.0.1') {
428
+ console.log(`Access on local network (e.g., mobile): http://${localIp}:${PORT}`);
429
+ }
430
+ console.log(`Serving files from: ${PUBLIC_DIR}`);
431
+ console.log(`User data file: ${USERS_FILE_PATH}`);
432
+ console.warn('--- SECURITY WARNING: Storing plain text passwords in users.json! ---');
433
+ console.warn('--- WebRTC Note: Using public STUN servers. TURN server might be needed for reliability. ---');
434
+ });
435
+
436
+ server.on('error', (error) => {
437
+ console.error('Server failed to start:', error);
438
+ process.exit(1);
439
+ });
440
+ })();
441
+
442
+ // --- END: Additions/Modifications in app.js ---
data/users.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "pc": "123",
3
+ "mobile": "123",
4
+ "pc2": "123"
5
+ }
package-lock.json ADDED
@@ -0,0 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-app",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "chat-app",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "express": "^5.1.0",
13
+ "socket.io": "^4.8.1"
14
+ }
15
+ },
16
+ "node_modules/@socket.io/component-emitter": {
17
+ "version": "3.1.2",
18
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
19
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
20
+ },
21
+ "node_modules/@types/cors": {
22
+ "version": "2.8.17",
23
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
24
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
25
+ "dependencies": {
26
+ "@types/node": "*"
27
+ }
28
+ },
29
+ "node_modules/@types/node": {
30
+ "version": "22.15.2",
31
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
32
+ "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
33
+ "dependencies": {
34
+ "undici-types": "~6.21.0"
35
+ }
36
+ },
37
+ "node_modules/accepts": {
38
+ "version": "2.0.0",
39
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
40
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
41
+ "dependencies": {
42
+ "mime-types": "^3.0.0",
43
+ "negotiator": "^1.0.0"
44
+ },
45
+ "engines": {
46
+ "node": ">= 0.6"
47
+ }
48
+ },
49
+ "node_modules/base64id": {
50
+ "version": "2.0.0",
51
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
52
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
53
+ "engines": {
54
+ "node": "^4.5.0 || >= 5.9"
55
+ }
56
+ },
57
+ "node_modules/body-parser": {
58
+ "version": "2.2.0",
59
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
60
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
61
+ "dependencies": {
62
+ "bytes": "^3.1.2",
63
+ "content-type": "^1.0.5",
64
+ "debug": "^4.4.0",
65
+ "http-errors": "^2.0.0",
66
+ "iconv-lite": "^0.6.3",
67
+ "on-finished": "^2.4.1",
68
+ "qs": "^6.14.0",
69
+ "raw-body": "^3.0.0",
70
+ "type-is": "^2.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18"
74
+ }
75
+ },
76
+ "node_modules/bytes": {
77
+ "version": "3.1.2",
78
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
79
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
80
+ "engines": {
81
+ "node": ">= 0.8"
82
+ }
83
+ },
84
+ "node_modules/call-bind-apply-helpers": {
85
+ "version": "1.0.2",
86
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
87
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
88
+ "dependencies": {
89
+ "es-errors": "^1.3.0",
90
+ "function-bind": "^1.1.2"
91
+ },
92
+ "engines": {
93
+ "node": ">= 0.4"
94
+ }
95
+ },
96
+ "node_modules/call-bound": {
97
+ "version": "1.0.4",
98
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
99
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
100
+ "dependencies": {
101
+ "call-bind-apply-helpers": "^1.0.2",
102
+ "get-intrinsic": "^1.3.0"
103
+ },
104
+ "engines": {
105
+ "node": ">= 0.4"
106
+ },
107
+ "funding": {
108
+ "url": "https://github.com/sponsors/ljharb"
109
+ }
110
+ },
111
+ "node_modules/content-disposition": {
112
+ "version": "1.0.0",
113
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
114
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
115
+ "dependencies": {
116
+ "safe-buffer": "5.2.1"
117
+ },
118
+ "engines": {
119
+ "node": ">= 0.6"
120
+ }
121
+ },
122
+ "node_modules/content-type": {
123
+ "version": "1.0.5",
124
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
125
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
126
+ "engines": {
127
+ "node": ">= 0.6"
128
+ }
129
+ },
130
+ "node_modules/cookie": {
131
+ "version": "0.7.2",
132
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
133
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
134
+ "engines": {
135
+ "node": ">= 0.6"
136
+ }
137
+ },
138
+ "node_modules/cookie-signature": {
139
+ "version": "1.2.2",
140
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
141
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
142
+ "engines": {
143
+ "node": ">=6.6.0"
144
+ }
145
+ },
146
+ "node_modules/cors": {
147
+ "version": "2.8.5",
148
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
149
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
150
+ "dependencies": {
151
+ "object-assign": "^4",
152
+ "vary": "^1"
153
+ },
154
+ "engines": {
155
+ "node": ">= 0.10"
156
+ }
157
+ },
158
+ "node_modules/debug": {
159
+ "version": "4.4.0",
160
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
161
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
162
+ "dependencies": {
163
+ "ms": "^2.1.3"
164
+ },
165
+ "engines": {
166
+ "node": ">=6.0"
167
+ },
168
+ "peerDependenciesMeta": {
169
+ "supports-color": {
170
+ "optional": true
171
+ }
172
+ }
173
+ },
174
+ "node_modules/depd": {
175
+ "version": "2.0.0",
176
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
177
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
178
+ "engines": {
179
+ "node": ">= 0.8"
180
+ }
181
+ },
182
+ "node_modules/dunder-proto": {
183
+ "version": "1.0.1",
184
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
185
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
186
+ "dependencies": {
187
+ "call-bind-apply-helpers": "^1.0.1",
188
+ "es-errors": "^1.3.0",
189
+ "gopd": "^1.2.0"
190
+ },
191
+ "engines": {
192
+ "node": ">= 0.4"
193
+ }
194
+ },
195
+ "node_modules/ee-first": {
196
+ "version": "1.1.1",
197
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
198
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
199
+ },
200
+ "node_modules/encodeurl": {
201
+ "version": "2.0.0",
202
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
203
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
204
+ "engines": {
205
+ "node": ">= 0.8"
206
+ }
207
+ },
208
+ "node_modules/engine.io": {
209
+ "version": "6.6.4",
210
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
211
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
212
+ "dependencies": {
213
+ "@types/cors": "^2.8.12",
214
+ "@types/node": ">=10.0.0",
215
+ "accepts": "~1.3.4",
216
+ "base64id": "2.0.0",
217
+ "cookie": "~0.7.2",
218
+ "cors": "~2.8.5",
219
+ "debug": "~4.3.1",
220
+ "engine.io-parser": "~5.2.1",
221
+ "ws": "~8.17.1"
222
+ },
223
+ "engines": {
224
+ "node": ">=10.2.0"
225
+ }
226
+ },
227
+ "node_modules/engine.io-parser": {
228
+ "version": "5.2.3",
229
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
230
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
231
+ "engines": {
232
+ "node": ">=10.0.0"
233
+ }
234
+ },
235
+ "node_modules/engine.io/node_modules/accepts": {
236
+ "version": "1.3.8",
237
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
238
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
239
+ "dependencies": {
240
+ "mime-types": "~2.1.34",
241
+ "negotiator": "0.6.3"
242
+ },
243
+ "engines": {
244
+ "node": ">= 0.6"
245
+ }
246
+ },
247
+ "node_modules/engine.io/node_modules/debug": {
248
+ "version": "4.3.7",
249
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
250
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
251
+ "dependencies": {
252
+ "ms": "^2.1.3"
253
+ },
254
+ "engines": {
255
+ "node": ">=6.0"
256
+ },
257
+ "peerDependenciesMeta": {
258
+ "supports-color": {
259
+ "optional": true
260
+ }
261
+ }
262
+ },
263
+ "node_modules/engine.io/node_modules/mime-db": {
264
+ "version": "1.52.0",
265
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
266
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
267
+ "engines": {
268
+ "node": ">= 0.6"
269
+ }
270
+ },
271
+ "node_modules/engine.io/node_modules/mime-types": {
272
+ "version": "2.1.35",
273
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
274
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
275
+ "dependencies": {
276
+ "mime-db": "1.52.0"
277
+ },
278
+ "engines": {
279
+ "node": ">= 0.6"
280
+ }
281
+ },
282
+ "node_modules/engine.io/node_modules/negotiator": {
283
+ "version": "0.6.3",
284
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
285
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
286
+ "engines": {
287
+ "node": ">= 0.6"
288
+ }
289
+ },
290
+ "node_modules/es-define-property": {
291
+ "version": "1.0.1",
292
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
293
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
294
+ "engines": {
295
+ "node": ">= 0.4"
296
+ }
297
+ },
298
+ "node_modules/es-errors": {
299
+ "version": "1.3.0",
300
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
301
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
302
+ "engines": {
303
+ "node": ">= 0.4"
304
+ }
305
+ },
306
+ "node_modules/es-object-atoms": {
307
+ "version": "1.1.1",
308
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
309
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
310
+ "dependencies": {
311
+ "es-errors": "^1.3.0"
312
+ },
313
+ "engines": {
314
+ "node": ">= 0.4"
315
+ }
316
+ },
317
+ "node_modules/escape-html": {
318
+ "version": "1.0.3",
319
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
320
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
321
+ },
322
+ "node_modules/etag": {
323
+ "version": "1.8.1",
324
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
325
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
326
+ "engines": {
327
+ "node": ">= 0.6"
328
+ }
329
+ },
330
+ "node_modules/express": {
331
+ "version": "5.1.0",
332
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
333
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
334
+ "dependencies": {
335
+ "accepts": "^2.0.0",
336
+ "body-parser": "^2.2.0",
337
+ "content-disposition": "^1.0.0",
338
+ "content-type": "^1.0.5",
339
+ "cookie": "^0.7.1",
340
+ "cookie-signature": "^1.2.1",
341
+ "debug": "^4.4.0",
342
+ "encodeurl": "^2.0.0",
343
+ "escape-html": "^1.0.3",
344
+ "etag": "^1.8.1",
345
+ "finalhandler": "^2.1.0",
346
+ "fresh": "^2.0.0",
347
+ "http-errors": "^2.0.0",
348
+ "merge-descriptors": "^2.0.0",
349
+ "mime-types": "^3.0.0",
350
+ "on-finished": "^2.4.1",
351
+ "once": "^1.4.0",
352
+ "parseurl": "^1.3.3",
353
+ "proxy-addr": "^2.0.7",
354
+ "qs": "^6.14.0",
355
+ "range-parser": "^1.2.1",
356
+ "router": "^2.2.0",
357
+ "send": "^1.1.0",
358
+ "serve-static": "^2.2.0",
359
+ "statuses": "^2.0.1",
360
+ "type-is": "^2.0.1",
361
+ "vary": "^1.1.2"
362
+ },
363
+ "engines": {
364
+ "node": ">= 18"
365
+ },
366
+ "funding": {
367
+ "type": "opencollective",
368
+ "url": "https://opencollective.com/express"
369
+ }
370
+ },
371
+ "node_modules/finalhandler": {
372
+ "version": "2.1.0",
373
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
374
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
375
+ "dependencies": {
376
+ "debug": "^4.4.0",
377
+ "encodeurl": "^2.0.0",
378
+ "escape-html": "^1.0.3",
379
+ "on-finished": "^2.4.1",
380
+ "parseurl": "^1.3.3",
381
+ "statuses": "^2.0.1"
382
+ },
383
+ "engines": {
384
+ "node": ">= 0.8"
385
+ }
386
+ },
387
+ "node_modules/forwarded": {
388
+ "version": "0.2.0",
389
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
390
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
391
+ "engines": {
392
+ "node": ">= 0.6"
393
+ }
394
+ },
395
+ "node_modules/fresh": {
396
+ "version": "2.0.0",
397
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
398
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
399
+ "engines": {
400
+ "node": ">= 0.8"
401
+ }
402
+ },
403
+ "node_modules/function-bind": {
404
+ "version": "1.1.2",
405
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
406
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
407
+ "funding": {
408
+ "url": "https://github.com/sponsors/ljharb"
409
+ }
410
+ },
411
+ "node_modules/get-intrinsic": {
412
+ "version": "1.3.0",
413
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
414
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
415
+ "dependencies": {
416
+ "call-bind-apply-helpers": "^1.0.2",
417
+ "es-define-property": "^1.0.1",
418
+ "es-errors": "^1.3.0",
419
+ "es-object-atoms": "^1.1.1",
420
+ "function-bind": "^1.1.2",
421
+ "get-proto": "^1.0.1",
422
+ "gopd": "^1.2.0",
423
+ "has-symbols": "^1.1.0",
424
+ "hasown": "^2.0.2",
425
+ "math-intrinsics": "^1.1.0"
426
+ },
427
+ "engines": {
428
+ "node": ">= 0.4"
429
+ },
430
+ "funding": {
431
+ "url": "https://github.com/sponsors/ljharb"
432
+ }
433
+ },
434
+ "node_modules/get-proto": {
435
+ "version": "1.0.1",
436
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
437
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
438
+ "dependencies": {
439
+ "dunder-proto": "^1.0.1",
440
+ "es-object-atoms": "^1.0.0"
441
+ },
442
+ "engines": {
443
+ "node": ">= 0.4"
444
+ }
445
+ },
446
+ "node_modules/gopd": {
447
+ "version": "1.2.0",
448
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
449
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
450
+ "engines": {
451
+ "node": ">= 0.4"
452
+ },
453
+ "funding": {
454
+ "url": "https://github.com/sponsors/ljharb"
455
+ }
456
+ },
457
+ "node_modules/has-symbols": {
458
+ "version": "1.1.0",
459
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
460
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
461
+ "engines": {
462
+ "node": ">= 0.4"
463
+ },
464
+ "funding": {
465
+ "url": "https://github.com/sponsors/ljharb"
466
+ }
467
+ },
468
+ "node_modules/hasown": {
469
+ "version": "2.0.2",
470
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
471
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
472
+ "dependencies": {
473
+ "function-bind": "^1.1.2"
474
+ },
475
+ "engines": {
476
+ "node": ">= 0.4"
477
+ }
478
+ },
479
+ "node_modules/http-errors": {
480
+ "version": "2.0.0",
481
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
482
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
483
+ "dependencies": {
484
+ "depd": "2.0.0",
485
+ "inherits": "2.0.4",
486
+ "setprototypeof": "1.2.0",
487
+ "statuses": "2.0.1",
488
+ "toidentifier": "1.0.1"
489
+ },
490
+ "engines": {
491
+ "node": ">= 0.8"
492
+ }
493
+ },
494
+ "node_modules/iconv-lite": {
495
+ "version": "0.6.3",
496
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
497
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
498
+ "dependencies": {
499
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
500
+ },
501
+ "engines": {
502
+ "node": ">=0.10.0"
503
+ }
504
+ },
505
+ "node_modules/inherits": {
506
+ "version": "2.0.4",
507
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
508
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
509
+ },
510
+ "node_modules/ipaddr.js": {
511
+ "version": "1.9.1",
512
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
513
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
514
+ "engines": {
515
+ "node": ">= 0.10"
516
+ }
517
+ },
518
+ "node_modules/is-promise": {
519
+ "version": "4.0.0",
520
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
521
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
522
+ },
523
+ "node_modules/math-intrinsics": {
524
+ "version": "1.1.0",
525
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
526
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
527
+ "engines": {
528
+ "node": ">= 0.4"
529
+ }
530
+ },
531
+ "node_modules/media-typer": {
532
+ "version": "1.1.0",
533
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
534
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
535
+ "engines": {
536
+ "node": ">= 0.8"
537
+ }
538
+ },
539
+ "node_modules/merge-descriptors": {
540
+ "version": "2.0.0",
541
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
542
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
543
+ "engines": {
544
+ "node": ">=18"
545
+ },
546
+ "funding": {
547
+ "url": "https://github.com/sponsors/sindresorhus"
548
+ }
549
+ },
550
+ "node_modules/mime-db": {
551
+ "version": "1.54.0",
552
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
553
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
554
+ "engines": {
555
+ "node": ">= 0.6"
556
+ }
557
+ },
558
+ "node_modules/mime-types": {
559
+ "version": "3.0.1",
560
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
561
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
562
+ "dependencies": {
563
+ "mime-db": "^1.54.0"
564
+ },
565
+ "engines": {
566
+ "node": ">= 0.6"
567
+ }
568
+ },
569
+ "node_modules/ms": {
570
+ "version": "2.1.3",
571
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
572
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
573
+ },
574
+ "node_modules/negotiator": {
575
+ "version": "1.0.0",
576
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
577
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
578
+ "engines": {
579
+ "node": ">= 0.6"
580
+ }
581
+ },
582
+ "node_modules/object-assign": {
583
+ "version": "4.1.1",
584
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
585
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
586
+ "engines": {
587
+ "node": ">=0.10.0"
588
+ }
589
+ },
590
+ "node_modules/object-inspect": {
591
+ "version": "1.13.4",
592
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
593
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
594
+ "engines": {
595
+ "node": ">= 0.4"
596
+ },
597
+ "funding": {
598
+ "url": "https://github.com/sponsors/ljharb"
599
+ }
600
+ },
601
+ "node_modules/on-finished": {
602
+ "version": "2.4.1",
603
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
604
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
605
+ "dependencies": {
606
+ "ee-first": "1.1.1"
607
+ },
608
+ "engines": {
609
+ "node": ">= 0.8"
610
+ }
611
+ },
612
+ "node_modules/once": {
613
+ "version": "1.4.0",
614
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
615
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
616
+ "dependencies": {
617
+ "wrappy": "1"
618
+ }
619
+ },
620
+ "node_modules/parseurl": {
621
+ "version": "1.3.3",
622
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
623
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
624
+ "engines": {
625
+ "node": ">= 0.8"
626
+ }
627
+ },
628
+ "node_modules/path-to-regexp": {
629
+ "version": "8.2.0",
630
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
631
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
632
+ "engines": {
633
+ "node": ">=16"
634
+ }
635
+ },
636
+ "node_modules/proxy-addr": {
637
+ "version": "2.0.7",
638
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
639
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
640
+ "dependencies": {
641
+ "forwarded": "0.2.0",
642
+ "ipaddr.js": "1.9.1"
643
+ },
644
+ "engines": {
645
+ "node": ">= 0.10"
646
+ }
647
+ },
648
+ "node_modules/qs": {
649
+ "version": "6.14.0",
650
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
651
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
652
+ "dependencies": {
653
+ "side-channel": "^1.1.0"
654
+ },
655
+ "engines": {
656
+ "node": ">=0.6"
657
+ },
658
+ "funding": {
659
+ "url": "https://github.com/sponsors/ljharb"
660
+ }
661
+ },
662
+ "node_modules/range-parser": {
663
+ "version": "1.2.1",
664
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
665
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
666
+ "engines": {
667
+ "node": ">= 0.6"
668
+ }
669
+ },
670
+ "node_modules/raw-body": {
671
+ "version": "3.0.0",
672
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
673
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
674
+ "dependencies": {
675
+ "bytes": "3.1.2",
676
+ "http-errors": "2.0.0",
677
+ "iconv-lite": "0.6.3",
678
+ "unpipe": "1.0.0"
679
+ },
680
+ "engines": {
681
+ "node": ">= 0.8"
682
+ }
683
+ },
684
+ "node_modules/router": {
685
+ "version": "2.2.0",
686
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
687
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
688
+ "dependencies": {
689
+ "debug": "^4.4.0",
690
+ "depd": "^2.0.0",
691
+ "is-promise": "^4.0.0",
692
+ "parseurl": "^1.3.3",
693
+ "path-to-regexp": "^8.0.0"
694
+ },
695
+ "engines": {
696
+ "node": ">= 18"
697
+ }
698
+ },
699
+ "node_modules/safe-buffer": {
700
+ "version": "5.2.1",
701
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
702
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
703
+ "funding": [
704
+ {
705
+ "type": "github",
706
+ "url": "https://github.com/sponsors/feross"
707
+ },
708
+ {
709
+ "type": "patreon",
710
+ "url": "https://www.patreon.com/feross"
711
+ },
712
+ {
713
+ "type": "consulting",
714
+ "url": "https://feross.org/support"
715
+ }
716
+ ]
717
+ },
718
+ "node_modules/safer-buffer": {
719
+ "version": "2.1.2",
720
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
721
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
722
+ },
723
+ "node_modules/send": {
724
+ "version": "1.2.0",
725
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
726
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
727
+ "dependencies": {
728
+ "debug": "^4.3.5",
729
+ "encodeurl": "^2.0.0",
730
+ "escape-html": "^1.0.3",
731
+ "etag": "^1.8.1",
732
+ "fresh": "^2.0.0",
733
+ "http-errors": "^2.0.0",
734
+ "mime-types": "^3.0.1",
735
+ "ms": "^2.1.3",
736
+ "on-finished": "^2.4.1",
737
+ "range-parser": "^1.2.1",
738
+ "statuses": "^2.0.1"
739
+ },
740
+ "engines": {
741
+ "node": ">= 18"
742
+ }
743
+ },
744
+ "node_modules/serve-static": {
745
+ "version": "2.2.0",
746
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
747
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
748
+ "dependencies": {
749
+ "encodeurl": "^2.0.0",
750
+ "escape-html": "^1.0.3",
751
+ "parseurl": "^1.3.3",
752
+ "send": "^1.2.0"
753
+ },
754
+ "engines": {
755
+ "node": ">= 18"
756
+ }
757
+ },
758
+ "node_modules/setprototypeof": {
759
+ "version": "1.2.0",
760
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
761
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
762
+ },
763
+ "node_modules/side-channel": {
764
+ "version": "1.1.0",
765
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
766
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
767
+ "dependencies": {
768
+ "es-errors": "^1.3.0",
769
+ "object-inspect": "^1.13.3",
770
+ "side-channel-list": "^1.0.0",
771
+ "side-channel-map": "^1.0.1",
772
+ "side-channel-weakmap": "^1.0.2"
773
+ },
774
+ "engines": {
775
+ "node": ">= 0.4"
776
+ },
777
+ "funding": {
778
+ "url": "https://github.com/sponsors/ljharb"
779
+ }
780
+ },
781
+ "node_modules/side-channel-list": {
782
+ "version": "1.0.0",
783
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
784
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
785
+ "dependencies": {
786
+ "es-errors": "^1.3.0",
787
+ "object-inspect": "^1.13.3"
788
+ },
789
+ "engines": {
790
+ "node": ">= 0.4"
791
+ },
792
+ "funding": {
793
+ "url": "https://github.com/sponsors/ljharb"
794
+ }
795
+ },
796
+ "node_modules/side-channel-map": {
797
+ "version": "1.0.1",
798
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
799
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
800
+ "dependencies": {
801
+ "call-bound": "^1.0.2",
802
+ "es-errors": "^1.3.0",
803
+ "get-intrinsic": "^1.2.5",
804
+ "object-inspect": "^1.13.3"
805
+ },
806
+ "engines": {
807
+ "node": ">= 0.4"
808
+ },
809
+ "funding": {
810
+ "url": "https://github.com/sponsors/ljharb"
811
+ }
812
+ },
813
+ "node_modules/side-channel-weakmap": {
814
+ "version": "1.0.2",
815
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
816
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
817
+ "dependencies": {
818
+ "call-bound": "^1.0.2",
819
+ "es-errors": "^1.3.0",
820
+ "get-intrinsic": "^1.2.5",
821
+ "object-inspect": "^1.13.3",
822
+ "side-channel-map": "^1.0.1"
823
+ },
824
+ "engines": {
825
+ "node": ">= 0.4"
826
+ },
827
+ "funding": {
828
+ "url": "https://github.com/sponsors/ljharb"
829
+ }
830
+ },
831
+ "node_modules/socket.io": {
832
+ "version": "4.8.1",
833
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
834
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
835
+ "dependencies": {
836
+ "accepts": "~1.3.4",
837
+ "base64id": "~2.0.0",
838
+ "cors": "~2.8.5",
839
+ "debug": "~4.3.2",
840
+ "engine.io": "~6.6.0",
841
+ "socket.io-adapter": "~2.5.2",
842
+ "socket.io-parser": "~4.2.4"
843
+ },
844
+ "engines": {
845
+ "node": ">=10.2.0"
846
+ }
847
+ },
848
+ "node_modules/socket.io-adapter": {
849
+ "version": "2.5.5",
850
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
851
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
852
+ "dependencies": {
853
+ "debug": "~4.3.4",
854
+ "ws": "~8.17.1"
855
+ }
856
+ },
857
+ "node_modules/socket.io-adapter/node_modules/debug": {
858
+ "version": "4.3.7",
859
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
860
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
861
+ "dependencies": {
862
+ "ms": "^2.1.3"
863
+ },
864
+ "engines": {
865
+ "node": ">=6.0"
866
+ },
867
+ "peerDependenciesMeta": {
868
+ "supports-color": {
869
+ "optional": true
870
+ }
871
+ }
872
+ },
873
+ "node_modules/socket.io-parser": {
874
+ "version": "4.2.4",
875
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
876
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
877
+ "dependencies": {
878
+ "@socket.io/component-emitter": "~3.1.0",
879
+ "debug": "~4.3.1"
880
+ },
881
+ "engines": {
882
+ "node": ">=10.0.0"
883
+ }
884
+ },
885
+ "node_modules/socket.io-parser/node_modules/debug": {
886
+ "version": "4.3.7",
887
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
888
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
889
+ "dependencies": {
890
+ "ms": "^2.1.3"
891
+ },
892
+ "engines": {
893
+ "node": ">=6.0"
894
+ },
895
+ "peerDependenciesMeta": {
896
+ "supports-color": {
897
+ "optional": true
898
+ }
899
+ }
900
+ },
901
+ "node_modules/socket.io/node_modules/accepts": {
902
+ "version": "1.3.8",
903
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
904
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
905
+ "dependencies": {
906
+ "mime-types": "~2.1.34",
907
+ "negotiator": "0.6.3"
908
+ },
909
+ "engines": {
910
+ "node": ">= 0.6"
911
+ }
912
+ },
913
+ "node_modules/socket.io/node_modules/debug": {
914
+ "version": "4.3.7",
915
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
916
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
917
+ "dependencies": {
918
+ "ms": "^2.1.3"
919
+ },
920
+ "engines": {
921
+ "node": ">=6.0"
922
+ },
923
+ "peerDependenciesMeta": {
924
+ "supports-color": {
925
+ "optional": true
926
+ }
927
+ }
928
+ },
929
+ "node_modules/socket.io/node_modules/mime-db": {
930
+ "version": "1.52.0",
931
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
932
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
933
+ "engines": {
934
+ "node": ">= 0.6"
935
+ }
936
+ },
937
+ "node_modules/socket.io/node_modules/mime-types": {
938
+ "version": "2.1.35",
939
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
940
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
941
+ "dependencies": {
942
+ "mime-db": "1.52.0"
943
+ },
944
+ "engines": {
945
+ "node": ">= 0.6"
946
+ }
947
+ },
948
+ "node_modules/socket.io/node_modules/negotiator": {
949
+ "version": "0.6.3",
950
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
951
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
952
+ "engines": {
953
+ "node": ">= 0.6"
954
+ }
955
+ },
956
+ "node_modules/statuses": {
957
+ "version": "2.0.1",
958
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
959
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
960
+ "engines": {
961
+ "node": ">= 0.8"
962
+ }
963
+ },
964
+ "node_modules/toidentifier": {
965
+ "version": "1.0.1",
966
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
967
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
968
+ "engines": {
969
+ "node": ">=0.6"
970
+ }
971
+ },
972
+ "node_modules/type-is": {
973
+ "version": "2.0.1",
974
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
975
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
976
+ "dependencies": {
977
+ "content-type": "^1.0.5",
978
+ "media-typer": "^1.1.0",
979
+ "mime-types": "^3.0.0"
980
+ },
981
+ "engines": {
982
+ "node": ">= 0.6"
983
+ }
984
+ },
985
+ "node_modules/undici-types": {
986
+ "version": "6.21.0",
987
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
988
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
989
+ },
990
+ "node_modules/unpipe": {
991
+ "version": "1.0.0",
992
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
993
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
994
+ "engines": {
995
+ "node": ">= 0.8"
996
+ }
997
+ },
998
+ "node_modules/vary": {
999
+ "version": "1.1.2",
1000
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1001
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1002
+ "engines": {
1003
+ "node": ">= 0.8"
1004
+ }
1005
+ },
1006
+ "node_modules/wrappy": {
1007
+ "version": "1.0.2",
1008
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1009
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
1010
+ },
1011
+ "node_modules/ws": {
1012
+ "version": "8.17.1",
1013
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
1014
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
1015
+ "engines": {
1016
+ "node": ">=10.0.0"
1017
+ },
1018
+ "peerDependencies": {
1019
+ "bufferutil": "^4.0.1",
1020
+ "utf-8-validate": ">=5.0.2"
1021
+ },
1022
+ "peerDependenciesMeta": {
1023
+ "bufferutil": {
1024
+ "optional": true
1025
+ },
1026
+ "utf-8-validate": {
1027
+ "optional": true
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ }
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-app",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "express": "^5.1.0",
14
+ "socket.io": "^4.8.1"
15
+ }
16
+ }
public/index.html ADDED
@@ -0,0 +1,1382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WhatsApp Style Chat + Video</title>
7
+ <!-- Include Emoji Picker library -->
8
+ <script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
9
+ <style>
10
+ /* Basic Reset & Body Styling */
11
+ * { box-sizing: border-box; margin: 0; padding: 0; }
12
+ html, body { height: 100%; overflow: hidden; } /* Prevent scrolling */
13
+ body {
14
+ font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
15
+ background-color: #DADBD3; /* WhatsApp Web-like background */
16
+ display: flex;
17
+ justify-content: center;
18
+ align-items: center;
19
+ position: relative; /* Needed for absolute positioning of call container */
20
+ }
21
+
22
+ /* Container to hold the chat app */
23
+ .app-container {
24
+ width: 100%;
25
+ max-width: 800px;
26
+ height: 100%; /* Use full viewport height */
27
+ max-height: 95vh; /* Max height limit */
28
+ background-color: #E5DDD5; /* Chat background */
29
+ display: flex;
30
+ flex-direction: column;
31
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,0.06), 0 2px 5px 0 rgba(0,0,0,0.2);
32
+ overflow: hidden; /* Important for layout */
33
+ position: relative; /* Context for absolute video container */
34
+ }
35
+
36
+ /* --- Login Screen --- */
37
+ #login-screen {
38
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
39
+ text-align: center; padding: 40px; background-color: #f8f9fa; height: 100%;
40
+ }
41
+ #login-screen h1 { color: #444; margin-bottom: 30px; font-weight: 300; }
42
+ /* --- Login Screen Form --- */
43
+ #login-form {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ }
48
+ #login-screen input[type="text"],
49
+ #login-screen input[type="password"] { /* Style both inputs */
50
+ padding: 12px 15px;
51
+ margin-bottom: 20px;
52
+ border: 1px solid #ccc;
53
+ border-radius: 20px;
54
+ width: 250px;
55
+ font-size: 1em;
56
+ outline: none;
57
+ }
58
+ #login-screen input[type="text"]:focus,
59
+ #login-screen input[type="password"]:focus {
60
+ border-color: #075E54;
61
+ box-shadow: 0 0 0 2px rgba(7, 94, 84, 0.2);
62
+ }
63
+ #login-screen button {
64
+ padding: 12px 30px;
65
+ background-color: #075E54;
66
+ color: white;
67
+ border: none;
68
+ border-radius: 20px;
69
+ cursor: pointer;
70
+ font-size: 1em;
71
+ transition: background-color 0.2s;
72
+ }
73
+ #login-screen button:hover { background-color: #128C7E; }
74
+ /* --- Login Error Message --- */
75
+ #login-error {
76
+ color: #d9534f; /* Red color for errors */
77
+ margin-top: 15px;
78
+ font-size: 0.9em;
79
+ height: 1.2em; /* Reserve space even when empty */
80
+ text-align: center;
81
+ font-weight: bold;
82
+ min-width: 250px; /* Match input width roughly */
83
+ }
84
+
85
+ /* --- Chat Screen --- */
86
+ #chat-screen {
87
+ display: none; /* Hidden initially */
88
+ flex-direction: column;
89
+ height: 100%;
90
+ width: 100%;
91
+ }
92
+
93
+ /* Chat Header */
94
+ .chat-header {
95
+ background-color: #075E54;
96
+ color: white;
97
+ padding: 10px 15px;
98
+ display: flex;
99
+ align-items: center;
100
+ font-size: 1.1em;
101
+ min-height: 60px; /* Fixed height */
102
+ flex-shrink: 0; /* Prevent shrinking */
103
+ }
104
+ .chat-header h1 {
105
+ font-size: 1.2em;
106
+ font-weight: 500;
107
+ }
108
+
109
+ /* Messages Area */
110
+ #messages {
111
+ flex-grow: 1; /* Takes available space */
112
+ padding: 20px 5%; /* Padding left/right */
113
+ overflow-y: auto; /* Enable scrolling */
114
+ background-color: #E5DDD5; /* Default chat background color */
115
+ /* Optional: Add the WhatsApp background pattern */
116
+ /* background-image: url('https://user-images.githubusercontent.com/15075759/28719144-86dc0f70-73b1-11e7-911d-60d70fcded21.png'); */
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 5px; /* Small gap between elements (messages/separators) */
120
+ }
121
+
122
+ /* --- Date Separator --- */
123
+ .date-separator {
124
+ align-self: center;
125
+ background-color: #e1f7fb; /* Light blueish */
126
+ color: #586063;
127
+ font-size: 0.75em;
128
+ padding: 4px 10px;
129
+ border-radius: 8px;
130
+ margin: 15px 0 10px 0; /* Space around separator */
131
+ font-weight: 500;
132
+ box-shadow: 0 1px 1px rgba(0,0,0,0.05);
133
+ }
134
+
135
+ /* Individual Message Container Styling */
136
+ .message {
137
+ display: flex;
138
+ max-width: 70%; /* Max width of a message bubble + avatar */
139
+ align-items: flex-end; /* Align avatar and bubble bottom */
140
+ gap: 8px; /* Space between avatar and bubble */
141
+ margin-bottom: 5px; /* Space below each message */
142
+ position: relative; /* Added relative for call button */
143
+ }
144
+
145
+ /* --- Avatar Styling --- */
146
+ .avatar {
147
+ width: 30px;
148
+ height: 30px;
149
+ border-radius: 50%;
150
+ background-color: #ccc; /* Default bg */
151
+ color: white;
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ font-size: 0.8em;
156
+ font-weight: bold;
157
+ text-transform: uppercase;
158
+ flex-shrink: 0; /* Prevent shrinking */
159
+ }
160
+
161
+ /* Message Bubble Styling */
162
+ .message-bubble {
163
+ padding: 8px 12px;
164
+ border-radius: 8px;
165
+ position: relative; /* For timestamp positioning */
166
+ word-wrap: break-word; /* Wrap long words */
167
+ box-shadow: 0 1px 1px rgba(0,0,0,0.1);
168
+ min-width: 80px; /* Ensure timestamp fits well */
169
+ }
170
+
171
+ .message .username {
172
+ font-weight: bold;
173
+ font-size: 0.9em;
174
+ margin-bottom: 3px;
175
+ color: #075E54; /* Default color, can be adjusted */
176
+ display: block; /* Take its own line if needed */
177
+ }
178
+
179
+ .message .text {
180
+ font-size: 0.95em;
181
+ margin-right: 45px; /* Space for timestamp */
182
+ line-height: 1.4;
183
+ color: #303030; /* Main text color */
184
+ }
185
+
186
+ .message .time {
187
+ font-size: 0.7em;
188
+ color: #999;
189
+ position: absolute; /* Position relative to bubble */
190
+ bottom: 5px;
191
+ right: 8px;
192
+ white-space: nowrap; /* Prevent timestamp wrapping */
193
+ }
194
+
195
+ /* Incoming Message Styling */
196
+ .message.incoming {
197
+ align-self: flex-start; /* Align container to left */
198
+ }
199
+ .message.incoming .message-bubble {
200
+ background-color: #FFFFFF; /* White bubble */
201
+ border-top-left-radius: 0; /* Flat corner like WA */
202
+ }
203
+ /* Optionally assign different colors to incoming usernames */
204
+ .message.incoming .username {
205
+ /* Example: Use avatar color logic here if desired */
206
+ color: hsl(var(--user-hue, 0), 50%, 40%);
207
+ }
208
+ /* Hide avatar placeholder for outgoing messages */
209
+ .message.outgoing .avatar {
210
+ display: none;
211
+ }
212
+
213
+
214
+ /* Outgoing Message Styling */
215
+ .message.outgoing {
216
+ align-self: flex-end; /* Align container to right */
217
+ }
218
+ .message.outgoing .message-bubble {
219
+ background-color: #DCF8C6; /* Light green bubble */
220
+ border-top-right-radius: 0; /* Flat corner like WA */
221
+ }
222
+ /* Hide username for outgoing messages for cleaner look */
223
+ .message.outgoing .username {
224
+ display: none;
225
+ }
226
+ .message.outgoing .time {
227
+ color: #6aaa96; /* Slightly different time color */
228
+ }
229
+
230
+
231
+ /* System Message Styling */
232
+ .system-message {
233
+ align-self: center; /* Center align */
234
+ background-color: #E1F3FB; /* Light blue background */
235
+ color: #555;
236
+ font-size: 0.8em;
237
+ padding: 5px 10px;
238
+ border-radius: 10px;
239
+ margin: 10px 0;
240
+ font-style: italic;
241
+ box-shadow: 0 1px 1px rgba(0,0,0,0.05);
242
+ }
243
+
244
+ /* --- Call Button on Incoming Messages --- */
245
+ .call-button {
246
+ background: none; border: none; cursor: pointer;
247
+ font-size: 1.1em; color: #075E54; padding: 2px 5px;
248
+ position: absolute; /* Position near the message bubble */
249
+ top: -10px; /* Adjust as needed */
250
+ right: -25px; /* Adjust as needed */
251
+ display: none; /* Hide by default */
252
+ opacity: 0.7;
253
+ transition: opacity 0.2s;
254
+ z-index: 5; /* Ensure it's clickable over potential bubble margins */
255
+ }
256
+ .message.incoming:hover .call-button {
257
+ display: inline-block; /* Show on hover of incoming message */
258
+ }
259
+ .call-button:hover {
260
+ opacity: 1;
261
+ color: #128C7E;
262
+ }
263
+
264
+ /* --- Footer Area (Input + Typing Indicator) --- */
265
+ .chat-footer {
266
+ background-color: #F0F0F0; /* Light grey background for whole footer */
267
+ padding: 5px 15px 10px 15px; /* Padding top adjusted */
268
+ display: flex;
269
+ flex-direction: column; /* Stack input form and typing indicator */
270
+ flex-shrink: 0; /* Prevent shrinking */
271
+ border-top: 1px solid #e0e0e0; /* Subtle top border */
272
+ position: relative; /* Needed for emoji picker positioning context */
273
+ }
274
+
275
+ /* --- Typing Indicator --- */
276
+ #typing-indicator {
277
+ height: 20px; /* Reserve space */
278
+ font-size: 0.8em;
279
+ color: #777;
280
+ font-style: italic;
281
+ padding-left: 5px; /* Align with start of input area roughly */
282
+ min-height: 20px; /* Ensure it takes space even when empty */
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ white-space: nowrap;
286
+ visibility: hidden; /* Hide until someone is typing */
287
+ }
288
+
289
+ /* Message Input Form */
290
+ #message-form {
291
+ display: flex;
292
+ align-items: center;
293
+ width: 100%; /* Take full width within footer */
294
+ }
295
+
296
+ /* --- Emoji Button --- */
297
+ #emoji-button {
298
+ background: none;
299
+ border: none;
300
+ font-size: 1.6em; /* Larger emoji icon */
301
+ padding: 8px;
302
+ cursor: pointer;
303
+ color: #54656f;
304
+ margin-right: 5px;
305
+ flex-shrink: 0;
306
+ }
307
+ #emoji-button:hover {
308
+ color: #3b4a54;
309
+ }
310
+
311
+ /* Input Field */
312
+ #message-input {
313
+ flex-grow: 1;
314
+ padding: 10px 15px;
315
+ border: none;
316
+ border-radius: 20px; /* Rounded input field */
317
+ margin-right: 10px;
318
+ font-size: 1em;
319
+ outline: none;
320
+ background-color: #fff; /* White background for input */
321
+ }
322
+
323
+ /* Send Button */
324
+ #message-form button[type="submit"] {
325
+ background-color: #128C7E; /* WhatsApp Send Green */
326
+ color: white;
327
+ border: none;
328
+ border-radius: 50%; /* Circular button */
329
+ width: 44px;
330
+ height: 44px;
331
+ cursor: pointer;
332
+ font-size: 1.5em;
333
+ display: flex;
334
+ justify-content: center;
335
+ align-items: center;
336
+ transition: background-color 0.2s;
337
+ flex-shrink: 0;
338
+ /* Simple Send Icon using SVG */
339
+ padding-bottom: 2px; /* Adjust symbol position slightly */
340
+ }
341
+ /* Simple SVG Send Icon */
342
+ #message-form button[type="submit"] svg {
343
+ width: 24px;
344
+ height: 24px;
345
+ fill: white;
346
+ }
347
+
348
+ #message-form button[type="submit"]:hover {
349
+ background-color: #075E54; /* Darker green on hover */
350
+ }
351
+
352
+ /* --- Emoji Picker --- */
353
+ emoji-picker {
354
+ position: absolute;
355
+ bottom: 65px; /* Position above input area footer */
356
+ left: 10px; /* Adjust left positioning */
357
+ z-index: 10;
358
+ display: none; /* Hidden by default */
359
+ box-shadow: 0 4px 10px rgba(0,0,0,0.2);
360
+ border-radius: 8px;
361
+ border: 1px solid #ccc;
362
+ }
363
+ emoji-picker.visible {
364
+ display: block;
365
+ }
366
+
367
+
368
+ /* Hide the original separate user list block (if it existed) */
369
+ #user-list { display: none; }
370
+
371
+
372
+ /* --- Video Call Container --- */
373
+ #call-container {
374
+ position: absolute;
375
+ top: 0; left: 0; right: 0; bottom: 0;
376
+ background-color: rgba(0, 0, 0, 0.9); /* Dark overlay */
377
+ display: none; /* Hidden by default */
378
+ flex-direction: column;
379
+ justify-content: center;
380
+ align-items: center;
381
+ z-index: 100;
382
+ padding: 20px;
383
+ }
384
+ #call-container.active {
385
+ display: flex; /* Show when call is active */
386
+ }
387
+
388
+ #video-grid {
389
+ display: flex;
390
+ justify-content: center;
391
+ align-items: center;
392
+ gap: 10px;
393
+ width: 100%;
394
+ max-width: 700px; /* Limit width */
395
+ position: relative; /* For positioning local video */
396
+ flex-grow: 1; /* Take available space */
397
+ min-height: 200px; /* Ensure grid takes some space even before video loads */
398
+ }
399
+
400
+ #remote-video {
401
+ width: 100%; /* Take full width */
402
+ max-height: 80vh; /* Limit height */
403
+ background-color: #222;
404
+ border-radius: 8px;
405
+ object-fit: contain; /* Scale video nicely */
406
+ }
407
+
408
+ #local-video {
409
+ width: 25%; /* Smaller local video */
410
+ max-width: 150px;
411
+ position: absolute;
412
+ bottom: 15px;
413
+ right: 15px;
414
+ border: 2px solid rgba(255, 255, 255, 0.5);
415
+ border-radius: 5px;
416
+ background-color: #333;
417
+ object-fit: cover; /* Fill the small frame */
418
+ z-index: 101; /* Above remote video if overlapping */
419
+ }
420
+
421
+ #call-controls {
422
+ display: flex;
423
+ gap: 20px;
424
+ margin-top: 15px;
425
+ flex-shrink: 0; /* Prevent shrinking */
426
+ }
427
+
428
+ #call-controls button {
429
+ padding: 12px 18px;
430
+ border-radius: 50%; /* Circular buttons */
431
+ border: none;
432
+ cursor: pointer;
433
+ font-size: 1.4em; /* Icon size */
434
+ width: 55px;
435
+ height: 55px;
436
+ display: flex;
437
+ justify-content: center;
438
+ align-items: center;
439
+ transition: background-color 0.2s;
440
+ }
441
+
442
+ #hangup-button {
443
+ background-color: #ff4d4d; /* Red */
444
+ color: white;
445
+ }
446
+ #hangup-button:hover {
447
+ background-color: #e60000;
448
+ }
449
+
450
+ /* --- Incoming Call Notification --- */
451
+ #incoming-call-notification {
452
+ position: absolute;
453
+ top: 70px; /* Below header */
454
+ left: 50%;
455
+ transform: translateX(-50%);
456
+ background-color: #075E54;
457
+ color: white;
458
+ padding: 15px 25px;
459
+ border-radius: 8px;
460
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3);
461
+ z-index: 110;
462
+ display: none; /* Hidden by default */
463
+ text-align: center;
464
+ min-width: 280px;
465
+ }
466
+ #incoming-call-notification p {
467
+ margin-bottom: 15px;
468
+ font-size: 1.1em;
469
+ }
470
+ #incoming-call-controls button {
471
+ padding: 8px 15px;
472
+ margin: 0 10px;
473
+ border: none;
474
+ border-radius: 5px;
475
+ cursor: pointer;
476
+ font-size: 0.9em;
477
+ transition: background-color 0.2s;
478
+ }
479
+ #accept-call-button { background-color: #25D366; color: white; }
480
+ #reject-call-button { background-color: #ff4d4d; color: white; }
481
+ #accept-call-button:hover { background-color: #1DAF53; }
482
+ #reject-call-button:hover { background-color: #e60000; }
483
+
484
+ </style>
485
+ </head>
486
+ <body>
487
+ <!-- Main container for the app -->
488
+ <div class="app-container">
489
+
490
+ <!-- Login Screen View -->
491
+ <div id="login-screen">
492
+ <h1>Login or Register</h1>
493
+ <form id="login-form">
494
+ <input type="text" id="username-input" placeholder="Enter your username" required autocomplete="username">
495
+ <input type="password" id="password-input" placeholder="Enter your password" required autocomplete="current-password">
496
+ <button type="submit">Login / Register</button>
497
+ </form>
498
+ <!-- Area to display login errors -->
499
+ <div id="login-error"></div>
500
+ </div>
501
+
502
+ <!-- Chat Screen View (Initially Hidden) -->
503
+ <div id="chat-screen">
504
+ <!-- Chat Header -->
505
+ <div class="chat-header">
506
+ <h1 id="chat-title">Group Chat</h1>
507
+ </div>
508
+
509
+ <!-- Message Display Area -->
510
+ <div id="messages">
511
+ <!-- Messages, Date Separators, Avatars will be added here by JS -->
512
+ </div>
513
+
514
+ <!-- Footer: Typing Indicator + Input Form -->
515
+ <div class="chat-footer">
516
+ <div id="typing-indicator"></div> <!-- Typing indicator display -->
517
+ <form id="message-form">
518
+ <!-- Emoji toggle button -->
519
+ <button type="button" id="emoji-button">😀</button>
520
+ <!-- Message text input -->
521
+ <input type="text" id="message-input" placeholder="Type a message" required autocomplete="off">
522
+ <!-- Send message button -->
523
+ <button type="submit">
524
+ <svg viewBox="0 0 24 24"><path fill="currentColor" d="M1.101 21.757 23.8 12.028 1.101 2.3l.011 7.912 13.623 1.816-13.623 1.817-.011 7.912z"></path></svg>
525
+ </button>
526
+ </form>
527
+ </div>
528
+
529
+ <!-- Emoji Picker Element (positioned absolutely relative to footer) -->
530
+ <emoji-picker class="light"></emoji-picker>
531
+
532
+ </div> <!-- End #chat-screen -->
533
+
534
+ <!-- Video Call UI -->
535
+ <div id="call-container">
536
+ <div id="video-grid">
537
+ <video id="remote-video" playsinline autoplay></video>
538
+ <video id="local-video" playsinline autoplay muted></video> <!-- Muted local video -->
539
+ </div>
540
+ <div id="call-controls">
541
+ <button id="hangup-button">❌</button> <!-- Simple hangup icon -->
542
+ <!-- Add mute/video toggle buttons later if needed -->
543
+ </div>
544
+ </div>
545
+
546
+ <!-- Incoming Call Notification -->
547
+ <div id="incoming-call-notification">
548
+ <p id="incoming-call-text">Incoming call from User...</p>
549
+ <div id="incoming-call-controls">
550
+ <button id="accept-call-button">✔️ Accept</button>
551
+ <button id="reject-call-button">✖️ Reject</button>
552
+ </div>
553
+ </div>
554
+
555
+ </div> <!-- End .app-container -->
556
+
557
+ <!-- Include Socket.IO client library -->
558
+ <script src="/socket.io/socket.io.js"></script>
559
+ <!-- Main application JavaScript -->
560
+ <script>
561
+ const socket = io();
562
+ let currentUsername = ''; // Stores the username after successful login
563
+ let lastMessageDate = null; // Tracks date for separators
564
+ let typingTimeout = null; // Timer for typing indicator
565
+
566
+ // --- DOM Elements ---
567
+ const loginScreen = document.getElementById('login-screen');
568
+ const chatScreen = document.getElementById('chat-screen');
569
+ const loginForm = document.getElementById('login-form');
570
+ const usernameInput = document.getElementById('username-input');
571
+ const passwordInput = document.getElementById('password-input');
572
+ const loginError = document.getElementById('login-error');
573
+ const messagesDiv = document.getElementById('messages');
574
+ const messageForm = document.getElementById('message-form');
575
+ const messageInput = document.getElementById('message-input');
576
+ const chatTitle = document.getElementById('chat-title');
577
+ const typingIndicator = document.getElementById('typing-indicator');
578
+ const emojiButton = document.getElementById('emoji-button');
579
+ const emojiPicker = document.querySelector('emoji-picker');
580
+
581
+ // --- Video Call DOM Elements & State ---
582
+ const callContainer = document.getElementById('call-container');
583
+ const localVideo = document.getElementById('local-video');
584
+ const remoteVideo = document.getElementById('remote-video');
585
+ const hangupButton = document.getElementById('hangup-button');
586
+ const incomingCallNotification = document.getElementById('incoming-call-notification');
587
+ const incomingCallText = document.getElementById('incoming-call-text');
588
+ const acceptCallButton = document.getElementById('accept-call-button');
589
+ const rejectCallButton = document.getElementById('reject-call-button');
590
+
591
+ let localStream = null;
592
+ let peerConnection = null;
593
+ let isCallActive = false;
594
+ let otherUserInCall = null; // Stores the username of the person we are calling/is calling us
595
+ let pendingCallOffer = null; // Store offer if received before local media ready
596
+
597
+ // STUN servers configuration (using Google's public servers)
598
+ const iceServers = {
599
+ iceServers: [
600
+ { urls: 'stun:stun.l.google.com:19302' },
601
+ { urls: 'stun:stun1.l.google.com:19302' },
602
+ // Add TURN servers here if you have them for better reliability
603
+ ],
604
+ };
605
+ // --- END: Video Call DOM Elements & State ---
606
+
607
+
608
+ // --- Event Listeners (Chat + Call) ---
609
+
610
+ // Login form submission
611
+ loginForm.addEventListener('submit', (e) => {
612
+ e.preventDefault();
613
+ const username = usernameInput.value.trim();
614
+ const password = passwordInput.value; // Get password
615
+ loginError.textContent = ''; // Clear previous errors
616
+ if (username && password) {
617
+ socket.emit('attempt_login', { username, password });
618
+ } else {
619
+ loginError.textContent = 'Please enter both username and password.';
620
+ }
621
+ });
622
+
623
+ // Message form submission
624
+ messageForm.addEventListener('submit', (e) => {
625
+ e.preventDefault();
626
+ const text = messageInput.value.trim();
627
+ if (text && currentUsername) {
628
+ const timestamp = new Date().toISOString(); // Use ISO string for consistency
629
+ // Display own message immediately
630
+ addMessage(currentUsername, text, timestamp);
631
+ // Send message data to server
632
+ socket.emit('message', { text });
633
+ // Clear typing status immediately
634
+ if (typingTimeout) {
635
+ clearTimeout(typingTimeout);
636
+ typingTimeout = null;
637
+ socket.emit('stop_typing'); // Inform server typing stopped
638
+ }
639
+ messageInput.value = ''; // Clear input field
640
+ emojiPicker.classList.remove('visible'); // Hide emoji picker
641
+ messageInput.focus(); // Keep focus on input
642
+ }
643
+ });
644
+
645
+ // --- Typing Indicator Logic ---
646
+ messageInput.addEventListener('input', () => {
647
+ if (!currentUsername) return; // Ignore typing if not logged in
648
+ if (!typingTimeout) {
649
+ socket.emit('typing'); // Send 'typing' event only on the first keypress after pause
650
+ } else {
651
+ clearTimeout(typingTimeout); // Reset the timeout if already typing
652
+ }
653
+ // Set a timeout to send 'stop_typing' if no input for 1.5 seconds
654
+ typingTimeout = setTimeout(() => {
655
+ socket.emit('stop_typing');
656
+ typingTimeout = null; // Reset the timeout ID state
657
+ }, 1500);
658
+ });
659
+
660
+ // --- Emoji Picker Logic ---
661
+ emojiButton.addEventListener('click', () => {
662
+ emojiPicker.classList.toggle('visible');
663
+ if(emojiPicker.classList.contains('visible')) {
664
+ messageInput.focus(); // Keep focus on input when picker opens
665
+ }
666
+ });
667
+ emojiPicker.addEventListener('emoji-click', event => {
668
+ messageInput.value += event.detail.unicode;
669
+ emojiPicker.classList.remove('visible'); // Hide picker after selection
670
+ messageInput.focus(); // Return focus to input field
671
+ });
672
+ // Hide emoji picker if a click occurs outside
673
+ document.addEventListener('click', (event) => {
674
+ const isClickInsidePicker = emojiPicker.contains(event.target);
675
+ const isClickOnEmojiButton = (event.target === emojiButton || emojiButton.contains(event.target)); // Check button itself or icon inside
676
+
677
+ if (!isClickInsidePicker && !isClickOnEmojiButton && emojiPicker.classList.contains('visible')) {
678
+ emojiPicker.classList.remove('visible');
679
+ }
680
+ });
681
+
682
+
683
+ // --- Video Call Event Listeners ---
684
+
685
+ // Hang Up Button
686
+ hangupButton.addEventListener('click', () => {
687
+ hangUpCall(); // Cleans up locally and optionally notifies server
688
+ });
689
+
690
+ // Accept Incoming Call Button
691
+ acceptCallButton.addEventListener('click', () => {
692
+ const callerUsername = incomingCallNotification.dataset.caller;
693
+ if (callerUsername) {
694
+ console.log(`Accepting call from ${callerUsername}`);
695
+ hideIncomingCallNotification();
696
+ socket.emit('accept-call', { callerUsername });
697
+ // The server will respond with 'prepare-for-offer' or handle errors
698
+ } else {
699
+ console.error("Cannot accept call, caller username not found in notification data.");
700
+ hideIncomingCallNotification(); // Hide inconsistent notification
701
+ }
702
+ });
703
+
704
+ // Reject Incoming Call Button
705
+ rejectCallButton.addEventListener('click', () => {
706
+ const callerUsername = incomingCallNotification.dataset.caller;
707
+ if (callerUsername) {
708
+ console.log(`Rejecting call from ${callerUsername}`);
709
+ hideIncomingCallNotification();
710
+ socket.emit('reject-call', { callerUsername });
711
+ otherUserInCall = null; // Ensure state is cleared locally
712
+ } else {
713
+ console.error("Cannot reject call, caller username not found in notification data.");
714
+ hideIncomingCallNotification(); // Hide inconsistent notification
715
+ }
716
+ });
717
+
718
+ // --- END: Video Call Event Listeners ---
719
+
720
+
721
+ // --- Socket Event Handlers (Chat + Call) ---
722
+
723
+ // LOGIN SUCCESS
724
+ socket.on('login_success', (username) => {
725
+ currentUsername = username; // Store logged-in username
726
+ loginScreen.style.display = 'none'; // Hide login screen
727
+ chatScreen.style.display = 'flex'; // Show chat screen
728
+ chatTitle.textContent = `Chatting as ${currentUsername}`; // Update header
729
+ messagesDiv.innerHTML = ''; // Clear any previous messages
730
+ lastMessageDate = null; // Reset date separator tracking
731
+ messageInput.focus(); // Focus the message input field
732
+ requestNotificationPermission(); // Ask for notification permission
733
+ resetCallState(); // Ensure call state is clean on login
734
+ });
735
+
736
+ // LOGIN FAIL
737
+ socket.on('login_fail', (errorMessage) => {
738
+ loginError.textContent = errorMessage; // Show error message
739
+ passwordInput.value = ''; // Clear password field for retry
740
+ passwordInput.focus(); // Focus password field after error
741
+ });
742
+
743
+ // INCOMING MESSAGE
744
+ socket.on('message', (data) => {
745
+ // Only display if the message is from a different user
746
+ if (data.username !== currentUsername) {
747
+ addMessage(data.username, data.text, data.timestamp); // Pass ISO timestamp
748
+ showNotification(`${data.username}: ${data.text}`); // Show notification if tab not active
749
+ }
750
+ });
751
+
752
+ // USER JOIN/LEAVE/LIST
753
+ socket.on('user_join', (username) => {
754
+ if (username !== currentUsername && currentUsername) { // Also check if current user is logged in
755
+ addSystemMessage(`${username} has joined`);
756
+ }
757
+ });
758
+ socket.on('user_leave', (username) => {
759
+ if (username !== currentUsername && currentUsername) { // Check if current user is logged in
760
+ addSystemMessage(`${username} has left`);
761
+ // If the user who left was the one we were in a call with
762
+ if (isCallActive && otherUserInCall === username) {
763
+ console.log("Other user disconnected during call.");
764
+ addSystemMessage(`Call with ${username} ended (user disconnected).`);
765
+ hangUpCall(false); // Clean up locally, don't emit hangup again
766
+ }
767
+ }
768
+ });
769
+ socket.on('user_list', (users) => {
770
+ console.log("Online users:", users);
771
+ // Potential future use: update online status indicators, etc.
772
+ });
773
+
774
+ // TYPING STATUS
775
+ socket.on('user_typing_status', (typingUsers) => {
776
+ // Filter out the current user from the list of typers
777
+ const otherTypingUsers = typingUsers.filter(u => u !== currentUsername);
778
+ if (otherTypingUsers.length === 0) {
779
+ typingIndicator.textContent = ''; // Clear indicator text
780
+ typingIndicator.style.visibility = 'hidden'; // Hide the indicator element
781
+ } else {
782
+ let text = '';
783
+ if (otherTypingUsers.length === 1) {
784
+ text = `${otherTypingUsers[0]} is typing...`;
785
+ } else if (otherTypingUsers.length <= 3) {
786
+ text = `${otherTypingUsers.join(', ')} are typing...`;
787
+ } else {
788
+ text = 'Several people are typing...';
789
+ }
790
+ typingIndicator.textContent = text; // Set the indicator text
791
+ typingIndicator.style.visibility = 'visible'; // Make the indicator element visible
792
+ }
793
+ });
794
+
795
+
796
+ // --- START: Video Call Socket Handlers ---
797
+
798
+ // Notification of an incoming call
799
+ socket.on('incoming-call', ({ callerUsername }) => {
800
+ if (isCallActive || otherUserInCall || incomingCallNotification.style.display === 'block') {
801
+ // Already in a call, processing one, or notification already shown
802
+ console.warn("Received incoming call while busy or already notified, rejecting.");
803
+ socket.emit('reject-call', { callerUsername }); // Inform server to notify caller
804
+ return;
805
+ }
806
+ console.log(`Incoming call from ${callerUsername}`);
807
+ otherUserInCall = callerUsername; // Tentatively set the user we might talk to
808
+ showIncomingCallNotification(callerUsername);
809
+ });
810
+
811
+ // The user we called has accepted
812
+ socket.on('call-accepted', ({ acceptorUsername }) => {
813
+ if (!otherUserInCall || otherUserInCall !== acceptorUsername) {
814
+ console.warn(`Received call-accepted from ${acceptorUsername}, but expected ${otherUserInCall || 'nobody'}.`);
815
+ // Maybe the call was cancelled or timed out locally? Reset state just in case.
816
+ if (!isCallActive) resetCallState(); // Reset if not actually in call setup
817
+ return;
818
+ }
819
+ console.log(`${acceptorUsername} accepted the call. Starting WebRTC offer.`);
820
+ // otherUserInCall should already be set correctly from initiateCallRequest
821
+ startCallNegotiation(true); // Start negotiation as the caller (will create offer)
822
+ });
823
+
824
+ // We accepted a call, now prepare for the offer from the caller
825
+ socket.on('prepare-for-offer', ({ callerUsername }) => {
826
+ if (!otherUserInCall || otherUserInCall !== callerUsername) {
827
+ console.warn(`Received prepare-for-offer from ${callerUsername}, but expected ${otherUserInCall || 'nobody'}.`);
828
+ // Maybe the user cancelled/rejected before server processed accept? Reset state.
829
+ if (!isCallActive) resetCallState();
830
+ return;
831
+ }
832
+ console.log(`Call accepted. Preparing to receive offer from ${callerUsername}.`);
833
+ // otherUserInCall should be set from 'incoming-call' handler or accept button logic
834
+ startCallNegotiation(false); // Start negotiation as the callee (will wait for offer)
835
+ });
836
+
837
+
838
+ // The user we called rejected the call
839
+ socket.on('call-rejected', ({ rejectorUsername }) => {
840
+ if (otherUserInCall === rejectorUsername) {
841
+ console.log(`${rejectorUsername} rejected the call.`);
842
+ addSystemMessage(`Call rejected by ${rejectorUsername}.`);
843
+ resetCallState(); // Clean up our state since the call attempt failed
844
+ } else {
845
+ console.warn(`Received rejection from ${rejectorUsername}, but was expecting ${otherUserInCall || 'nobody'}`);
846
+ }
847
+ });
848
+
849
+ // The call could not proceed (user busy, offline, conflict)
850
+ socket.on('call-denied', ({ reason }) => {
851
+ console.log(`Call denied: ${reason}`);
852
+ addSystemMessage(`Call could not be started: ${reason}`);
853
+ hideIncomingCallNotification(); // Ensure notification is hidden
854
+ resetCallState(); // Clean up our state since the call attempt failed
855
+ });
856
+
857
+ // Receive WebRTC offer from the caller
858
+ socket.on('webrtc-offer', async ({ offer, callerUsername }) => {
859
+ if (otherUserInCall !== callerUsername) {
860
+ console.warn(`Received offer from ${callerUsername}, but currently expect call with ${otherUserInCall || 'nobody'}. Ignoring.`);
861
+ return;
862
+ }
863
+ if (!peerConnection) {
864
+ console.warn("Received offer, but PeerConnection is not ready. Storing offer.");
865
+ // Store the offer to handle once the peer connection is ready (in startCallNegotiation(false))
866
+ pendingCallOffer = { offer, callerUsername };
867
+ // If startCallNegotiation hasn't run yet, this will be picked up there.
868
+ // If it HAS run but PC isn't ready (unlikely), need more robust handling.
869
+ return;
870
+ }
871
+ console.log("Received WebRTC Offer from", callerUsername);
872
+ try {
873
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
874
+ console.log("Remote description (offer) set. Creating answer...");
875
+ const answer = await peerConnection.createAnswer();
876
+ await peerConnection.setLocalDescription(answer);
877
+ console.log("Local description (answer) set. Sending answer...");
878
+ socket.emit('webrtc-answer', { targetUsername: otherUserInCall, answer });
879
+ pendingCallOffer = null; // Clear any pending offer
880
+ } catch (error) {
881
+ console.error("Error handling offer:", error);
882
+ addSystemMessage("Error setting up call connection (offer).");
883
+ hangUpCall(); // Attempt cleanup
884
+ }
885
+ });
886
+
887
+ // Receive WebRTC answer from the acceptor
888
+ socket.on('webrtc-answer', async ({ answer, acceptorUsername }) => {
889
+ if (!peerConnection || otherUserInCall !== acceptorUsername) {
890
+ console.warn(`Received answer from ${acceptorUsername}, but not ready or wrong user (expected ${otherUserInCall || 'nobody'}).`);
891
+ return;
892
+ }
893
+ console.log("Received WebRTC Answer from", acceptorUsername);
894
+ try {
895
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
896
+ console.log("Remote description (answer) set. Connection should establish.");
897
+ } catch (error) {
898
+ console.error("Error handling answer:", error);
899
+ addSystemMessage("Error setting up call connection (answer).");
900
+ hangUpCall(); // Attempt cleanup
901
+ }
902
+ });
903
+
904
+ // Receive ICE candidate from the peer
905
+ socket.on('webrtc-ice-candidate', ({ candidate }) => {
906
+ if (!peerConnection || !peerConnection.remoteDescription) {
907
+ // Wait until remote description is set before adding candidates
908
+ console.warn("Received ICE candidate prematurely. Ignoring.");
909
+ return;
910
+ }
911
+ if (!candidate) {
912
+ console.log("Received null ICE candidate (end of candidates signal).");
913
+ return;
914
+ }
915
+ // console.log("Received ICE Candidate"); // Can be very noisy
916
+ try {
917
+ peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
918
+ .catch(e => console.error("Error adding received ICE candidate", e));
919
+ } catch (error) {
920
+ // Ignore errors like "Error processing ICE candidate" if state is wrong
921
+ if (!isCallActive) {
922
+ console.warn("Ignoring ICE candidate error as call is not active:", error.message);
923
+ } else {
924
+ console.error("Error processing received ICE candidate:", error);
925
+ }
926
+ }
927
+ });
928
+
929
+ // Call ended by the other user (hangup or disconnect) or server cleanup
930
+ socket.on('call-ended', () => {
931
+ console.log("Received 'call-ended' signal.");
932
+ if (isCallActive) {
933
+ addSystemMessage(`Call with ${otherUserInCall || 'user'} ended.`);
934
+ hangUpCall(false); // Clean up locally, don't emit hangup again
935
+ } else {
936
+ // Might receive this if call was rejected/denied and server initiated cleanup,
937
+ // or if local state was already cleaned up. Ensure UI is reset.
938
+ console.log("Call already ended locally or wasn't active. Ensuring cleanup.");
939
+ resetCallState();
940
+ hideIncomingCallNotification();
941
+ }
942
+ });
943
+
944
+
945
+ // --- END: Video Call Socket Handlers ---
946
+
947
+
948
+ // --- Helper Functions (Chat + Call) ---
949
+
950
+ // Add Regular Message (Includes Call Button)
951
+ function addMessage(username, text, timestampISO) {
952
+ const messageDate = new Date(timestampISO); // Parse ISO string
953
+ maybeAddDateSeparator(messageDate); // Check if a date separator is needed
954
+
955
+ const messageDiv = document.createElement('div');
956
+ messageDiv.classList.add('message');
957
+
958
+ const isOutgoing = (username === currentUsername);
959
+ messageDiv.classList.add(isOutgoing ? 'outgoing' : 'incoming');
960
+
961
+ // Add Avatar only for incoming messages
962
+ if (!isOutgoing) {
963
+ messageDiv.appendChild(createAvatar(username));
964
+ }
965
+
966
+ const bubble = document.createElement('div');
967
+ bubble.classList.add('message-bubble');
968
+
969
+ // Add username span only for incoming messages
970
+ if (!isOutgoing) {
971
+ const usernameSpan = document.createElement('span');
972
+ usernameSpan.className = 'username';
973
+ usernameSpan.textContent = username;
974
+ // Optional: Apply user-specific color
975
+ // const userHue = stringToHslColor(username, 0, 0, true);
976
+ // usernameSpan.style.setProperty('--user-hue', userHue);
977
+ bubble.appendChild(usernameSpan);
978
+
979
+ // --- Add Call Button for Incoming Messages ---
980
+ const callBtn = document.createElement('button');
981
+ callBtn.className = 'call-button';
982
+ callBtn.innerHTML = '📞'; // Phone icon
983
+ callBtn.title = `Call ${username}`;
984
+ callBtn.onclick = (e) => {
985
+ e.stopPropagation(); // Prevent triggering other click events if nested
986
+ initiateCallRequest(username);
987
+ };
988
+ // Append near the bubble, but within the message container for positioning
989
+ messageDiv.appendChild(callBtn);
990
+ // --- End Call Button ---
991
+ }
992
+
993
+ const textSpan = document.createElement('span');
994
+ textSpan.className = 'text';
995
+ textSpan.textContent = text; // Use textContent for security
996
+
997
+ const timeSpan = document.createElement('span');
998
+ timeSpan.className = 'time';
999
+ // Format time as HH:MM
1000
+ timeSpan.textContent = `${messageDate.getHours()}:${String(messageDate.getMinutes()).padStart(2, '0')}`;
1001
+
1002
+ bubble.appendChild(textSpan);
1003
+ bubble.appendChild(timeSpan);
1004
+ messageDiv.appendChild(bubble); // Add bubble after avatar/call button
1005
+
1006
+ messagesDiv.appendChild(messageDiv);
1007
+ scrollToBottom(); // Scroll down after adding message
1008
+ }
1009
+
1010
+ // Add System Message
1011
+ function addSystemMessage(text) {
1012
+ maybeAddDateSeparator(new Date()); // Check date for system messages too
1013
+ const message = document.createElement('div');
1014
+ message.className = 'system-message';
1015
+ message.textContent = text;
1016
+ messagesDiv.appendChild(message);
1017
+ scrollToBottom(); // Scroll down
1018
+ }
1019
+
1020
+ // --- Date Separator Logic ---
1021
+ function maybeAddDateSeparator(messageDate) {
1022
+ const messageDay = messageDate.toDateString();
1023
+ const lastDay = lastMessageDate ? lastMessageDate.toDateString() : null;
1024
+ if (messageDay !== lastDay) {
1025
+ const separator = document.createElement('div');
1026
+ separator.className = 'date-separator';
1027
+ separator.textContent = formatDateSeparator(messageDate); // Format the date text
1028
+ messagesDiv.appendChild(separator);
1029
+ lastMessageDate = messageDate; // Update the date of the last displayed item
1030
+ }
1031
+ }
1032
+ function formatDateSeparator(date) {
1033
+ const today = new Date();
1034
+ const yesterday = new Date(today);
1035
+ yesterday.setDate(today.getDate() - 1);
1036
+ if (date.toDateString() === today.toDateString()) { return 'Today'; }
1037
+ else if (date.toDateString() === yesterday.toDateString()) { return 'Yesterday'; }
1038
+ else { return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }); }
1039
+ }
1040
+
1041
+ // --- Avatar Creation Logic ---
1042
+ function createAvatar(username) {
1043
+ const avatar = document.createElement('div');
1044
+ avatar.className = 'avatar';
1045
+ const initials = username?.substring(0, 2).toUpperCase() || '?';
1046
+ avatar.textContent = initials;
1047
+ avatar.style.backgroundColor = stringToHslColor(username || '', 50, 60); // Saturation 50%, Lightness 60%
1048
+ return avatar;
1049
+ }
1050
+ function stringToHslColor(str, s, l, hueOnly = false) {
1051
+ if (!str) return hueOnly ? 0 : `hsl(0, ${s}%, ${l}%)`;
1052
+ let hash = 0;
1053
+ for (let i = 0; i < str.length; i++) {
1054
+ hash = str.charCodeAt(i) + ((hash << 5) - hash); hash = hash & hash;
1055
+ }
1056
+ const h = Math.abs(hash % 360);
1057
+ return hueOnly ? h : `hsl(${h}, ${s}%, ${l}%)`;
1058
+ }
1059
+
1060
+ // --- Scrolling ---
1061
+ function scrollToBottom() {
1062
+ requestAnimationFrame(() => {
1063
+ messagesDiv.scrollTo({ top: messagesDiv.scrollHeight, behavior: 'smooth' });
1064
+ });
1065
+ }
1066
+
1067
+ // --- Browser Notifications ---
1068
+ function requestNotificationPermission() {
1069
+ if ('Notification' in window && Notification.permission !== 'granted' && Notification.permission !== 'denied') {
1070
+ Notification.requestPermission().then(permission => {
1071
+ if (permission === 'granted') { console.log('Notification permission granted.'); }
1072
+ else { console.log('Notification permission denied.'); }
1073
+ });
1074
+ }
1075
+ }
1076
+ function showNotification(body) {
1077
+ if (!('Notification' in window) || Notification.permission !== 'granted') { return; }
1078
+ if (document.hidden) { // Only show if tab is not active
1079
+ const notification = new Notification('New Chat Message', {
1080
+ body: body,
1081
+ icon: '/favicon.ico' // Optional: place an icon named favicon.ico in your public folder
1082
+ });
1083
+ setTimeout(notification.close.bind(notification), 5000);
1084
+ notification.onclick = () => { window.focus(); };
1085
+ }
1086
+ }
1087
+
1088
+ // --- START: Video Call Helper Functions ---
1089
+
1090
+ // 1. Request to start a call with a user
1091
+ function initiateCallRequest(targetUsername) {
1092
+ if (isCallActive) {
1093
+ addSystemMessage("You are already in a call.");
1094
+ return;
1095
+ }
1096
+ if (otherUserInCall || incomingCallNotification.style.display === 'block') {
1097
+ addSystemMessage("Cannot start a new call while another is pending or incoming.");
1098
+ return;
1099
+ }
1100
+ if (targetUsername === currentUsername) {
1101
+ addSystemMessage("You cannot call yourself.");
1102
+ return;
1103
+ }
1104
+ console.log(`Requesting call with ${targetUsername}`);
1105
+ addSystemMessage(`Calling ${targetUsername}...`);
1106
+ otherUserInCall = targetUsername; // Tentatively set target for this call attempt
1107
+ socket.emit('request-call', { targetUsername });
1108
+ // Now wait for 'call-accepted', 'call-rejected', or 'call-denied' from server
1109
+ }
1110
+
1111
+ // 2. Start media streams and WebRTC negotiation (called after acceptance)
1112
+ async function startCallNegotiation(isCaller) {
1113
+ console.log(`Starting negotiation. Is Caller: ${isCaller}`);
1114
+ if (isCallActive) {
1115
+ console.warn("Negotiation requested but call is already active.");
1116
+ return; // Don't start negotiation again if already active
1117
+ }
1118
+ if (!otherUserInCall) {
1119
+ console.error("Cannot start negotiation, other user not set.");
1120
+ resetCallState(); // Reset if state is inconsistent
1121
+ return;
1122
+ }
1123
+
1124
+ addSystemMessage(`Starting video call with ${otherUserInCall}...`);
1125
+ showCallUI(); // Show the video elements and controls
1126
+
1127
+ try {
1128
+ // Get local camera/mic stream
1129
+ console.log("Requesting user media...");
1130
+ localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
1131
+ localVideo.srcObject = localStream;
1132
+ console.log("Local stream obtained.");
1133
+
1134
+ // Create PeerConnection
1135
+ createPeerConnection(); // This sets up the peerConnection object
1136
+
1137
+ // Add local tracks to the connection BEFORE creating offer/answer
1138
+ localStream.getTracks().forEach(track => {
1139
+ if (peerConnection) {
1140
+ peerConnection.addTrack(track, localStream);
1141
+ console.log(`Added local track: ${track.kind}`);
1142
+ }
1143
+ });
1144
+
1145
+ isCallActive = true; // Mark call as active *after* getting media/PC setup
1146
+
1147
+ if (isCaller) {
1148
+ // Caller creates the offer
1149
+ if (!peerConnection) throw new Error("PeerConnection not available for creating offer.");
1150
+ console.log("Creating offer...");
1151
+ const offer = await peerConnection.createOffer();
1152
+ await peerConnection.setLocalDescription(offer);
1153
+ console.log("Local description (offer) set. Sending offer...");
1154
+ socket.emit('webrtc-offer', { targetUsername: otherUserInCall, offer });
1155
+ } else {
1156
+ // Callee waits for the offer (handled by 'webrtc-offer' socket event)
1157
+ console.log("Waiting for offer from caller...");
1158
+ // If an offer arrived *before* we were ready (before PC was created), handle it now
1159
+ if (pendingCallOffer) {
1160
+ console.log("Handling pending offer received earlier.");
1161
+ await handlePendingOffer();
1162
+ }
1163
+ }
1164
+
1165
+ } catch (error) {
1166
+ console.error("Error starting call negotiation:", error);
1167
+ addSystemMessage(`Error starting call: ${error.message}. Please check camera/mic permissions.`);
1168
+ hangUpCall(); // Clean up on error
1169
+ }
1170
+ }
1171
+
1172
+
1173
+ // 3. Create the RTCPeerConnection object and set up listeners
1174
+ function createPeerConnection() {
1175
+ console.log("Creating PeerConnection with ICE servers:", iceServers);
1176
+ // Clean up any previous connection first
1177
+ if (peerConnection) {
1178
+ console.warn("Closing existing PeerConnection before creating new one.");
1179
+ peerConnection.close();
1180
+ }
1181
+
1182
+ peerConnection = new RTCPeerConnection(iceServers);
1183
+
1184
+ // Listener for ICE candidates generated by the browser
1185
+ peerConnection.onicecandidate = (event) => {
1186
+ if (event.candidate && otherUserInCall && isCallActive) { // Send only if call is active and peer known
1187
+ // console.log("Generated ICE Candidate:", event.candidate); // Noisy
1188
+ socket.emit('webrtc-ice-candidate', {
1189
+ targetUsername: otherUserInCall,
1190
+ candidate: event.candidate,
1191
+ });
1192
+ } else if (!event.candidate) {
1193
+ console.log("All ICE candidates have been sent.");
1194
+ }
1195
+ };
1196
+
1197
+ // Listener for when the remote peer adds a track (video/audio)
1198
+ peerConnection.ontrack = (event) => {
1199
+ console.log("Remote track received:", event.track.kind, "Stream count:", event.streams.length);
1200
+ if (event.streams && event.streams[0]) {
1201
+ console.log("Attaching remote stream to video element");
1202
+ remoteVideo.srcObject = event.streams[0];
1203
+ } else {
1204
+ // Fallback for browsers that might not bundle tracks into streams initially
1205
+ if (!remoteVideo.srcObject) {
1206
+ remoteVideo.srcObject = new MediaStream();
1207
+ }
1208
+ if(remoteVideo.srcObject.getVideoTracks().length === 0 && event.track.kind === 'video') {
1209
+ console.warn("Adding video track to remote stream.");
1210
+ remoteVideo.srcObject.addTrack(event.track);
1211
+ }
1212
+ if(remoteVideo.srcObject.getAudioTracks().length === 0 && event.track.kind === 'audio') {
1213
+ console.warn("Adding audio track to remote stream.");
1214
+ remoteVideo.srcObject.addTrack(event.track);
1215
+ }
1216
+ }
1217
+ };
1218
+
1219
+ // Listen for connection state changes
1220
+ peerConnection.oniceconnectionstatechange = () => {
1221
+ if (!peerConnection) return; // Connection might be closed already
1222
+ console.log(`ICE Connection State: ${peerConnection.iceConnectionState}`);
1223
+ if (peerConnection.iceConnectionState === 'failed' ||
1224
+ peerConnection.iceConnectionState === 'disconnected' ||
1225
+ peerConnection.iceConnectionState === 'closed') {
1226
+ if(isCallActive) { // Prevent cleanup if already hung up
1227
+ console.warn(`ICE connection issue: ${peerConnection.iceConnectionState}.`);
1228
+ // Avoid aggressive hangup on 'disconnected' as it might recover
1229
+ if (peerConnection.iceConnectionState === 'failed') {
1230
+ addSystemMessage("Call connection failed.");
1231
+ hangUpCall(); // Hang up on definite failure
1232
+ }
1233
+ }
1234
+ }
1235
+ };
1236
+
1237
+ peerConnection.onconnectionstatechange = () => {
1238
+ if (!peerConnection) return;
1239
+ console.log(`Connection State: ${peerConnection.connectionState}`);
1240
+ if (peerConnection.connectionState === 'failed' || peerConnection.connectionState === 'closed') {
1241
+ if (isCallActive) {
1242
+ console.warn(`Connection issue: ${peerConnection.connectionState}.`);
1243
+ addSystemMessage("Call connection lost or closed.");
1244
+ hangUpCall(false); // Clean up locally if connection definitively fails/closes
1245
+ }
1246
+ } else if (peerConnection.connectionState === 'connected') {
1247
+ console.log("Peers connected successfully!");
1248
+ // Call is fully established
1249
+ }
1250
+ };
1251
+ }
1252
+
1253
+ // 4. Handle incoming call notification UI
1254
+ function showIncomingCallNotification(callerUsername) {
1255
+ incomingCallText.textContent = `Incoming call from ${callerUsername}`;
1256
+ incomingCallNotification.dataset.caller = callerUsername; // Store caller name
1257
+ incomingCallNotification.style.display = 'block';
1258
+ }
1259
+
1260
+ function hideIncomingCallNotification() {
1261
+ incomingCallNotification.style.display = 'none';
1262
+ if (incomingCallNotification.dataset.caller) {
1263
+ delete incomingCallNotification.dataset.caller;
1264
+ }
1265
+ // If we hide the notification without accepting/rejecting, reset the potential peer
1266
+ if (!isCallActive && otherUserInCall === incomingCallNotification.dataset.caller) {
1267
+ otherUserInCall = null;
1268
+ }
1269
+ }
1270
+
1271
+ // 5. Show/Hide the main video call UI
1272
+ function showCallUI() {
1273
+ callContainer.classList.add('active');
1274
+ }
1275
+
1276
+ function hideCallUI() {
1277
+ callContainer.classList.remove('active');
1278
+ }
1279
+
1280
+ // 6. Clean up call state and resources
1281
+ function hangUpCall(notifyServer = true) {
1282
+ if (!isCallActive && !otherUserInCall && !localStream && !peerConnection) {
1283
+ // Already cleaned up or never started
1284
+ console.log("Hang up called, but no active call or resources found.");
1285
+ resetCallState(); // Ensure UI is hidden etc.
1286
+ return;
1287
+ }
1288
+ console.log("Hanging up call...");
1289
+
1290
+ if (isCallActive && notifyServer && otherUserInCall) {
1291
+ // Only notify server if the call was considered active and we know who to notify about
1292
+ console.log("Notifying server of hangup.");
1293
+ socket.emit('hangup-call'); // Inform the server/other peer
1294
+ } else {
1295
+ console.log("Hangup: Not notifying server (call not active, peer unknown, or notifyServer=false).")
1296
+ }
1297
+
1298
+ // Close peer connection
1299
+ if (peerConnection) {
1300
+ peerConnection.close();
1301
+ peerConnection = null;
1302
+ console.log("PeerConnection closed.");
1303
+ } else {
1304
+ console.log("Hangup: No PeerConnection to close.");
1305
+ }
1306
+
1307
+ // Stop local media tracks
1308
+ if (localStream) {
1309
+ localStream.getTracks().forEach(track => track.stop());
1310
+ localStream = null;
1311
+ console.log("Local stream stopped.");
1312
+ } else {
1313
+ console.log("Hangup: No local stream to stop.");
1314
+ }
1315
+
1316
+ // Reset video elements
1317
+ localVideo.srcObject = null;
1318
+ remoteVideo.srcObject = null;
1319
+
1320
+ // Reset state variables (critical!)
1321
+ const previouslyActive = isCallActive;
1322
+ isCallActive = false;
1323
+ otherUserInCall = null;
1324
+ pendingCallOffer = null;
1325
+
1326
+ // Hide UI elements
1327
+ hideCallUI();
1328
+ hideIncomingCallNotification(); // Ensure notification is hidden too
1329
+
1330
+ if (previouslyActive) {
1331
+ addSystemMessage("Call ended."); // Inform user only if call was active
1332
+ }
1333
+ console.log("Call cleanup complete.");
1334
+ }
1335
+
1336
+ // 7. Reset call state completely (use cautiously, e.g., on login)
1337
+ function resetCallState() {
1338
+ console.log("Resetting call state completely...");
1339
+ hangUpCall(false); // Clean up everything without notifying server
1340
+ }
1341
+
1342
+ // 8. Handle pending offer if received before negotiation started fully
1343
+ async function handlePendingOffer() {
1344
+ if (!peerConnection) {
1345
+ console.error("Cannot handle pending offer: PeerConnection is not ready.");
1346
+ pendingCallOffer = null; // Discard invalid offer
1347
+ resetCallState();
1348
+ return;
1349
+ }
1350
+ if (!pendingCallOffer || !otherUserInCall) {
1351
+ console.warn("Attempted to handle pending offer, but no offer or peer found.");
1352
+ return; // No pending offer or state mismatch
1353
+ }
1354
+
1355
+ const { offer, callerUsername } = pendingCallOffer;
1356
+ if (callerUsername !== otherUserInCall) {
1357
+ console.warn("Pending offer username mismatch. Ignoring.");
1358
+ pendingCallOffer = null;
1359
+ return;
1360
+ }
1361
+
1362
+ console.log("Handling pending WebRTC Offer received earlier.");
1363
+ try {
1364
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
1365
+ console.log("Pending: Remote description (offer) set. Creating answer...");
1366
+ const answer = await peerConnection.createAnswer();
1367
+ await peerConnection.setLocalDescription(answer);
1368
+ console.log("Pending: Local description (answer) set. Sending answer...");
1369
+ socket.emit('webrtc-answer', { targetUsername: otherUserInCall, answer });
1370
+ pendingCallOffer = null; // Clear the pending offer
1371
+ } catch (error) {
1372
+ console.error("Error handling pending offer:", error);
1373
+ addSystemMessage("Error setting up call connection (pending offer).");
1374
+ hangUpCall(); // Attempt cleanup
1375
+ }
1376
+ }
1377
+
1378
+ // --- END: Video Call Helper Functions ---
1379
+
1380
+ </script>
1381
+ </body>
1382
+ </html>