hermes-chat-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js ADDED
@@ -0,0 +1,1887 @@
1
+ import "./chunk-D42PTTYC.js";
2
+
3
+ // src/react/hooks/useMessages.ts
4
+ import { useState, useEffect, useCallback, useRef } from "react";
5
+ var useMessages = (client, roomId) => {
6
+ const [messages, setMessages] = useState([]);
7
+ const [loading, setLoading] = useState(false);
8
+ const [loadingMore, setLoadingMore] = useState(false);
9
+ const [hasMore, setHasMore] = useState(false);
10
+ const [error, setError] = useState(null);
11
+ const [typingUsers, setTypingUsers] = useState([]);
12
+ const oldestMessageId = useRef(void 0);
13
+ useEffect(() => {
14
+ if (!roomId || !client.isConnected) return;
15
+ setMessages([]);
16
+ setHasMore(false);
17
+ oldestMessageId.current = void 0;
18
+ setLoading(true);
19
+ setError(null);
20
+ client.getHistory(roomId).then(({ messages: msgs, hasMore: more }) => {
21
+ setMessages(msgs);
22
+ setHasMore(more);
23
+ if (msgs.length > 0) oldestMessageId.current = msgs[0]._id;
24
+ }).catch((err) => setError(err.message)).finally(() => setLoading(false));
25
+ }, [roomId, client.isConnected]);
26
+ useEffect(() => {
27
+ if (!roomId) return;
28
+ const onReceive = (msg) => {
29
+ if (msg.roomId !== roomId) return;
30
+ setMessages((prev) => {
31
+ if (prev.find((m) => m._id === msg._id)) return prev;
32
+ return [...prev, msg];
33
+ });
34
+ };
35
+ const onDeleted = ({
36
+ messageId
37
+ }) => {
38
+ setMessages(
39
+ (prev) => prev.map(
40
+ (m) => m._id === messageId ? { ...m, isDeleted: true, text: void 0 } : m
41
+ )
42
+ );
43
+ };
44
+ const onEdited = (msg) => {
45
+ setMessages((prev) => prev.map((m) => m._id === msg._id ? msg : m));
46
+ };
47
+ client.on("message:receive", onReceive);
48
+ client.on("message:deleted", onDeleted);
49
+ client.on("message:edited", onEdited);
50
+ return () => {
51
+ client.off("message:receive", onReceive);
52
+ client.off("message:deleted", onDeleted);
53
+ client.off("message:edited", onEdited);
54
+ };
55
+ }, [roomId, client]);
56
+ useEffect(() => {
57
+ const onReaction = ({ messageId, reactions }) => {
58
+ setMessages(
59
+ (prev) => prev.map((m) => m._id === messageId ? { ...m, reactions } : m)
60
+ );
61
+ };
62
+ client.on("reaction:updated", onReaction);
63
+ return () => {
64
+ client.off("reaction:updated", onReaction);
65
+ };
66
+ }, [client]);
67
+ useEffect(() => {
68
+ if (!roomId) return;
69
+ const onStarted = ({ userId, displayName, roomId: rid }) => {
70
+ if (rid !== roomId) return;
71
+ setTypingUsers((prev) => [
72
+ ...prev.filter((u) => u.userId !== userId),
73
+ { userId, displayName }
74
+ ]);
75
+ };
76
+ const onStopped = ({ userId, roomId: rid }) => {
77
+ if (rid !== roomId) return;
78
+ setTypingUsers((prev) => prev.filter((u) => u.userId !== userId));
79
+ };
80
+ client.on("typing:started", onStarted);
81
+ client.on("typing:stopped", onStopped);
82
+ return () => {
83
+ client.off("typing:started", onStarted);
84
+ client.off("typing:stopped", onStopped);
85
+ setTypingUsers([]);
86
+ };
87
+ }, [roomId, client]);
88
+ const loadMore = useCallback(async () => {
89
+ if (!roomId || loadingMore || !hasMore) return;
90
+ setLoadingMore(true);
91
+ try {
92
+ const { messages: older, hasMore: more } = await client.getHistory(
93
+ roomId,
94
+ oldestMessageId.current
95
+ );
96
+ setMessages((prev) => [...older, ...prev]);
97
+ setHasMore(more);
98
+ if (older.length > 0) oldestMessageId.current = older[0]._id;
99
+ } catch (err) {
100
+ setError(err.message);
101
+ } finally {
102
+ setLoadingMore(false);
103
+ }
104
+ }, [roomId, loadingMore, hasMore, client]);
105
+ const sendMessage = useCallback(
106
+ async (input) => {
107
+ if (!roomId) throw new Error("No room selected");
108
+ return client.sendMessage({ ...input, roomId });
109
+ },
110
+ [roomId, client]
111
+ );
112
+ const editMessage = useCallback(
113
+ async (messageId, text) => {
114
+ if (!roomId) throw new Error("No room selected");
115
+ return client.editMessage(messageId, roomId, text);
116
+ },
117
+ [roomId, client]
118
+ );
119
+ const deleteMessage = useCallback(
120
+ async (messageId) => {
121
+ if (!roomId) throw new Error("No room selected");
122
+ return client.deleteMessage(messageId, roomId);
123
+ },
124
+ [roomId, client]
125
+ );
126
+ const addReaction = useCallback(
127
+ async (messageId, emoji) => {
128
+ if (!roomId) throw new Error("No room selected");
129
+ return client.addReaction(messageId, roomId, emoji);
130
+ },
131
+ [roomId, client]
132
+ );
133
+ return {
134
+ messages,
135
+ loading,
136
+ loadingMore,
137
+ hasMore,
138
+ error,
139
+ typingUsers,
140
+ sendMessage,
141
+ editMessage,
142
+ deleteMessage,
143
+ addReaction,
144
+ loadMore
145
+ };
146
+ };
147
+
148
+ // src/react/hooks/useRooms.ts
149
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
150
+ var useRooms = (client) => {
151
+ const [rooms, setRooms] = useState2([]);
152
+ const [loading, setLoading] = useState2(true);
153
+ const [error, setError] = useState2(null);
154
+ const fetchedRef = useRef2(false);
155
+ const fetchRooms = useCallback2(async () => {
156
+ setLoading(true);
157
+ setError(null);
158
+ await new Promise((resolve, reject) => {
159
+ if (client.isConnected) return resolve();
160
+ let attempts = 0;
161
+ const interval = setInterval(() => {
162
+ attempts++;
163
+ if (client.isConnected) {
164
+ clearInterval(interval);
165
+ resolve();
166
+ } else if (attempts > 50) {
167
+ clearInterval(interval);
168
+ reject(new Error("Connection timeout"));
169
+ }
170
+ }, 100);
171
+ });
172
+ try {
173
+ const data = await client.getRooms();
174
+ console.log("[useRooms] fetched:", data.length, "rooms");
175
+ setRooms(data);
176
+ fetchedRef.current = true;
177
+ } catch (err) {
178
+ console.error("[useRooms] fetch error:", err.message);
179
+ setError(err.message);
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ }, [client]);
184
+ useEffect2(() => {
185
+ fetchRooms();
186
+ const onConnected = () => {
187
+ if (!fetchedRef.current) fetchRooms();
188
+ };
189
+ client.on("connected", onConnected);
190
+ return () => {
191
+ client.off("connected", onConnected);
192
+ };
193
+ }, [fetchRooms, client]);
194
+ useEffect2(() => {
195
+ const onCreated = (room) => {
196
+ setRooms((prev) => {
197
+ if (prev.find((r) => r._id === room._id)) return prev;
198
+ return [{ ...room, unreadCount: 0 }, ...prev];
199
+ });
200
+ };
201
+ const onDeleted = ({ roomId }) => setRooms((prev) => prev.filter((r) => r._id !== roomId));
202
+ const onMemberJoined = ({
203
+ roomId,
204
+ userId
205
+ }) => setRooms(
206
+ (prev) => prev.map(
207
+ (r) => r._id === roomId ? { ...r, members: [...r.members, userId] } : r
208
+ )
209
+ );
210
+ const onMemberLeft = ({
211
+ roomId,
212
+ userId
213
+ }) => setRooms(
214
+ (prev) => prev.map(
215
+ (r) => r._id === roomId ? { ...r, members: r.members.filter((m) => m !== userId) } : r
216
+ )
217
+ );
218
+ const onMessage = (msg) => setRooms((prev) => {
219
+ const idx = prev.findIndex((r) => r._id === msg.roomId);
220
+ if (idx === -1) return prev;
221
+ const updated = {
222
+ ...prev[idx],
223
+ lastMessage: msg,
224
+ lastActivity: msg.createdAt
225
+ };
226
+ return [updated, ...prev.filter((r) => r._id !== msg.roomId)];
227
+ });
228
+ client.on("room:created", onCreated);
229
+ client.on("room:deleted", onDeleted);
230
+ client.on("room:member:joined", onMemberJoined);
231
+ client.on("room:member:left", onMemberLeft);
232
+ client.on("message:receive", onMessage);
233
+ return () => {
234
+ client.off("room:created", onCreated);
235
+ client.off("room:deleted", onDeleted);
236
+ client.off("room:member:joined", onMemberJoined);
237
+ client.off("room:member:left", onMemberLeft);
238
+ client.off("message:receive", onMessage);
239
+ };
240
+ }, [client]);
241
+ const createDirect = useCallback2(
242
+ async (input) => {
243
+ const room = await client.createDirectRoom(input);
244
+ setRooms((prev) => {
245
+ if (prev.find((r) => r._id === room._id)) return prev;
246
+ return [{ ...room, unreadCount: 0 }, ...prev];
247
+ });
248
+ return room;
249
+ },
250
+ [client]
251
+ );
252
+ const createGroup = useCallback2(
253
+ async (input) => {
254
+ const room = await client.createGroupRoom(input);
255
+ setRooms((prev) => {
256
+ if (prev.find((r) => r._id === room._id)) return prev;
257
+ return [{ ...room, unreadCount: 0 }, ...prev];
258
+ });
259
+ return room;
260
+ },
261
+ [client]
262
+ );
263
+ const deleteRoom = useCallback2(
264
+ async (roomId) => {
265
+ await client.deleteRoom(roomId);
266
+ setRooms((prev) => prev.filter((r) => r._id !== roomId));
267
+ },
268
+ [client]
269
+ );
270
+ const addMember = useCallback2(
271
+ (roomId, userId) => client.addMember(roomId, userId),
272
+ [client]
273
+ );
274
+ const removeMember = useCallback2(
275
+ (roomId, userId) => client.removeMember(roomId, userId),
276
+ [client]
277
+ );
278
+ return {
279
+ rooms,
280
+ loading,
281
+ error,
282
+ createDirect,
283
+ createGroup,
284
+ deleteRoom,
285
+ addMember,
286
+ removeMember,
287
+ refetch: fetchRooms
288
+ };
289
+ };
290
+
291
+ // src/react/hooks/usePresence.ts
292
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3 } from "react";
293
+ var usePresence = (client) => {
294
+ const [onlineMap, setOnlineMap] = useState3(/* @__PURE__ */ new Map());
295
+ useEffect3(() => {
296
+ const onOnline = ({ userId }) => {
297
+ setOnlineMap((prev) => new Map(prev).set(userId, true));
298
+ };
299
+ const onOffline = ({ userId }) => {
300
+ setOnlineMap((prev) => new Map(prev).set(userId, false));
301
+ };
302
+ client.on("user:online", onOnline);
303
+ client.on("user:offline", onOffline);
304
+ return () => {
305
+ client.off("user:online", onOnline);
306
+ client.off("user:offline", onOffline);
307
+ };
308
+ }, [client]);
309
+ const isOnline = useCallback3(
310
+ (userId) => onlineMap.get(userId) ?? false,
311
+ [onlineMap]
312
+ );
313
+ const onlineUsers = Array.from(onlineMap.entries()).filter(([, online]) => online).map(([userId]) => userId);
314
+ return { isOnline, onlineUsers, onlineMap };
315
+ };
316
+
317
+ // src/react/hooks/useTyping.ts
318
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
319
+ var useTyping = (client, roomId) => {
320
+ const [typingUsers, setTypingUsers] = useState4(
321
+ /* @__PURE__ */ new Map()
322
+ );
323
+ const timeouts = useRef3(
324
+ /* @__PURE__ */ new Map()
325
+ );
326
+ const typingRef = useRef3(false);
327
+ const stopTimeout = useRef3(null);
328
+ useEffect4(() => {
329
+ if (!roomId) return;
330
+ const onStart = (event) => {
331
+ if (event.roomId !== roomId) return;
332
+ if (event.userId === client.currentUser?.userId) return;
333
+ setTypingUsers(
334
+ (prev) => new Map(prev).set(event.userId, event.displayName)
335
+ );
336
+ const existing = timeouts.current.get(event.userId);
337
+ if (existing) clearTimeout(existing);
338
+ const t = setTimeout(() => {
339
+ setTypingUsers((prev) => {
340
+ const next = new Map(prev);
341
+ next.delete(event.userId);
342
+ return next;
343
+ });
344
+ }, 4e3);
345
+ timeouts.current.set(event.userId, t);
346
+ };
347
+ const onStop = (event) => {
348
+ if (event.roomId !== roomId) return;
349
+ setTypingUsers((prev) => {
350
+ const next = new Map(prev);
351
+ next.delete(event.userId);
352
+ return next;
353
+ });
354
+ const existing = timeouts.current.get(event.userId);
355
+ if (existing) clearTimeout(existing);
356
+ timeouts.current.delete(event.userId);
357
+ };
358
+ client.on("typing:started", onStart);
359
+ client.on("typing:stopped", onStop);
360
+ return () => {
361
+ client.off("typing:started", onStart);
362
+ client.off("typing:stopped", onStop);
363
+ timeouts.current.forEach(clearTimeout);
364
+ timeouts.current.clear();
365
+ };
366
+ }, [roomId, client]);
367
+ const startTyping = useCallback4(() => {
368
+ if (!roomId) return;
369
+ if (!typingRef.current) {
370
+ client.startTyping(roomId);
371
+ typingRef.current = true;
372
+ }
373
+ if (stopTimeout.current) clearTimeout(stopTimeout.current);
374
+ stopTimeout.current = setTimeout(() => {
375
+ client.stopTyping(roomId);
376
+ typingRef.current = false;
377
+ }, 3e3);
378
+ }, [roomId, client]);
379
+ const stopTyping = useCallback4(() => {
380
+ if (!roomId) return;
381
+ if (stopTimeout.current) clearTimeout(stopTimeout.current);
382
+ if (typingRef.current) {
383
+ client.stopTyping(roomId);
384
+ typingRef.current = false;
385
+ }
386
+ }, [roomId, client]);
387
+ const typingText = (() => {
388
+ const names = Array.from(typingUsers.values());
389
+ if (names.length === 0) return null;
390
+ if (names.length === 1) return `${names[0]} is typing...`;
391
+ if (names.length === 2) return `${names[0]} and ${names[1]} are typing...`;
392
+ return `${names[0]} and ${names.length - 1} others are typing...`;
393
+ })();
394
+ return {
395
+ typingUsers,
396
+ typingText,
397
+ isAnyoneTyping: typingUsers.size > 0,
398
+ startTyping,
399
+ stopTyping
400
+ };
401
+ };
402
+
403
+ // src/react/hooks/useReadReceipts.ts
404
+ import { useState as useState5, useEffect as useEffect5, useCallback as useCallback5 } from "react";
405
+ var useReadReceipts = (client, roomId) => {
406
+ const [receipts, setReceipts] = useState5(/* @__PURE__ */ new Map());
407
+ useEffect5(() => {
408
+ if (!roomId) return;
409
+ const onReceipt = (event) => {
410
+ if (event.roomId !== roomId) return;
411
+ setReceipts((prev) => {
412
+ const next = new Map(prev);
413
+ const existing = next.get(event.lastMessageId) ?? /* @__PURE__ */ new Set();
414
+ existing.add(event.userId);
415
+ next.set(event.lastMessageId, existing);
416
+ return next;
417
+ });
418
+ };
419
+ client.on("receipt:updated", onReceipt);
420
+ return () => client.off("receipt:updated", onReceipt);
421
+ }, [roomId, client]);
422
+ const markSeen = useCallback5(
423
+ async (lastMessageId) => {
424
+ if (!roomId) return;
425
+ await client.markSeen(roomId, lastMessageId);
426
+ },
427
+ [roomId, client]
428
+ );
429
+ const seenBy = useCallback5(
430
+ (messageId) => {
431
+ return Array.from(receipts.get(messageId) ?? []);
432
+ },
433
+ [receipts]
434
+ );
435
+ return { markSeen, seenBy, receipts };
436
+ };
437
+
438
+ // src/react/hooks/useReactions.ts
439
+ import { useCallback as useCallback6 } from "react";
440
+ var useReactions = (client, roomId) => {
441
+ const react = useCallback6(
442
+ async (messageId, emoji) => {
443
+ if (!roomId) throw new Error("No room selected");
444
+ await client.addReaction(messageId, roomId, emoji);
445
+ },
446
+ [roomId, client]
447
+ );
448
+ const hasReacted = useCallback6(
449
+ (reactions, emoji) => {
450
+ const userId = client.currentUser?.userId;
451
+ if (!userId) return false;
452
+ return reactions.find((r) => r.emoji === emoji)?.users.includes(userId) ?? false;
453
+ },
454
+ [client]
455
+ );
456
+ const getCount = useCallback6(
457
+ (reactions, emoji) => {
458
+ return reactions.find((r) => r.emoji === emoji)?.users.length ?? 0;
459
+ },
460
+ []
461
+ );
462
+ const getEmojis = useCallback6((reactions) => {
463
+ return reactions.filter((r) => r.users.length > 0).map((r) => r.emoji);
464
+ }, []);
465
+ return { react, hasReacted, getCount, getEmojis };
466
+ };
467
+
468
+ // src/react/hooks/useUpload.ts
469
+ import { useState as useState6, useCallback as useCallback7 } from "react";
470
+ var useUpload = (client) => {
471
+ const [uploading, setUploading] = useState6(false);
472
+ const [error, setError] = useState6(null);
473
+ const [lastUpload, setLastUpload] = useState6(null);
474
+ const upload = useCallback7(
475
+ async (file) => {
476
+ setUploading(true);
477
+ setError(null);
478
+ try {
479
+ const result = await client.uploadFile(file);
480
+ setLastUpload(result);
481
+ return result;
482
+ } catch (err) {
483
+ setError(err.message);
484
+ return null;
485
+ } finally {
486
+ setUploading(false);
487
+ }
488
+ },
489
+ [client]
490
+ );
491
+ const sendFile = useCallback7(
492
+ async (roomId, file, replyTo) => {
493
+ setUploading(true);
494
+ setError(null);
495
+ try {
496
+ const uploaded = await client.uploadFile(file);
497
+ setLastUpload(uploaded);
498
+ const message = await client.sendMessage({
499
+ roomId,
500
+ type: uploaded.type,
501
+ url: uploaded.url,
502
+ fileName: uploaded.fileName,
503
+ fileSize: uploaded.fileSize,
504
+ mimeType: uploaded.mimeType,
505
+ thumbnail: uploaded.thumbnail,
506
+ replyTo
507
+ });
508
+ return message;
509
+ } catch (err) {
510
+ setError(err.message);
511
+ return null;
512
+ } finally {
513
+ setUploading(false);
514
+ }
515
+ },
516
+ [client]
517
+ );
518
+ const validate = useCallback7((file, maxMb = 50) => {
519
+ if (file.size > maxMb * 1024 * 1024) {
520
+ return `File too large. Max size is ${maxMb}MB.`;
521
+ }
522
+ const allowed = [
523
+ "image/jpeg",
524
+ "image/png",
525
+ "image/gif",
526
+ "image/webp",
527
+ "video/mp4",
528
+ "video/webm",
529
+ "audio/mpeg",
530
+ "audio/ogg",
531
+ "audio/wav",
532
+ "application/pdf",
533
+ "application/msword",
534
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
535
+ "text/plain"
536
+ ];
537
+ if (!allowed.includes(file.type)) {
538
+ return `File type not supported: ${file.type}`;
539
+ }
540
+ return null;
541
+ }, []);
542
+ return { upload, sendFile, validate, uploading, error, lastUpload };
543
+ };
544
+
545
+ // src/react/components/MessageList.tsx
546
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState7 } from "react";
547
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
548
+ var formatTime = (iso) => new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
549
+ var formatFileSize = (bytes) => {
550
+ if (!bytes) return "";
551
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
552
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
553
+ return `${bytes} B`;
554
+ };
555
+ var REACTION_EMOJIS = [
556
+ "\u{1FAE0}",
557
+ "\u{1F979}",
558
+ "\u{1FAE1}",
559
+ "\u{1F90C}",
560
+ "\u{1FAF6}",
561
+ "\u{1F480}",
562
+ "\u{1F525}",
563
+ "\u2728",
564
+ "\u{1FAE3}",
565
+ "\u{1F62E}\u200D\u{1F4A8}",
566
+ "\u{1FA84}",
567
+ "\u{1F972}",
568
+ "\u{1F485}",
569
+ "\u{1FAE6}",
570
+ "\u{1F92F}",
571
+ "\u{1F31A}",
572
+ "\u{1F441}\uFE0F",
573
+ "\u{1FAC0}",
574
+ "\u{1F98B}",
575
+ "\u{1FA90}"
576
+ ];
577
+ var EmojiPicker = ({ onPick, onClose, isOwn }) => {
578
+ const ref = useRef4(null);
579
+ useEffect6(() => {
580
+ const handler = (e) => {
581
+ if (ref.current && !ref.current.contains(e.target)) onClose();
582
+ };
583
+ document.addEventListener("mousedown", handler);
584
+ return () => document.removeEventListener("mousedown", handler);
585
+ }, [onClose]);
586
+ return /* @__PURE__ */ jsx(
587
+ "div",
588
+ {
589
+ ref,
590
+ style: {
591
+ position: "absolute",
592
+ bottom: "calc(100% + 8px)",
593
+ [isOwn ? "right" : "left"]: 0,
594
+ zIndex: 100,
595
+ background: "#1a1a2e",
596
+ border: "1px solid rgba(255,255,255,0.1)",
597
+ borderRadius: 14,
598
+ padding: "8px 10px",
599
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
600
+ display: "grid",
601
+ gridTemplateColumns: "repeat(5, 1fr)",
602
+ gap: 4,
603
+ animation: "hermes-pop 0.15s ease"
604
+ },
605
+ children: REACTION_EMOJIS.map((emoji) => /* @__PURE__ */ jsx(
606
+ "button",
607
+ {
608
+ onClick: () => {
609
+ onPick(emoji);
610
+ onClose();
611
+ },
612
+ style: {
613
+ background: "none",
614
+ border: "none",
615
+ cursor: "pointer",
616
+ fontSize: 20,
617
+ padding: "4px",
618
+ borderRadius: 8,
619
+ lineHeight: 1,
620
+ transition: "transform 0.1s, background 0.1s"
621
+ },
622
+ onMouseEnter: (e) => {
623
+ e.currentTarget.style.transform = "scale(1.3)";
624
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
625
+ },
626
+ onMouseLeave: (e) => {
627
+ e.currentTarget.style.transform = "scale(1)";
628
+ e.currentTarget.style.background = "none";
629
+ },
630
+ children: emoji
631
+ },
632
+ emoji
633
+ ))
634
+ }
635
+ );
636
+ };
637
+ var TypingIndicator = ({ typingUsers }) => {
638
+ if (!typingUsers.length) return null;
639
+ const text = typingUsers.length === 1 ? `${typingUsers[0].displayName} is typing` : typingUsers.length === 2 ? `${typingUsers[0].displayName} and ${typingUsers[1].displayName} are typing` : `${typingUsers[0].displayName} and ${typingUsers.length - 1} others are typing`;
640
+ return /* @__PURE__ */ jsxs(
641
+ "div",
642
+ {
643
+ style: {
644
+ display: "flex",
645
+ alignItems: "center",
646
+ gap: 8,
647
+ padding: "6px 16px 2px",
648
+ minHeight: 28
649
+ },
650
+ children: [
651
+ /* @__PURE__ */ jsx(
652
+ "div",
653
+ {
654
+ style: {
655
+ display: "flex",
656
+ alignItems: "center",
657
+ gap: 3,
658
+ background: "#f0f0f0",
659
+ borderRadius: 12,
660
+ padding: "6px 10px"
661
+ },
662
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(
663
+ "span",
664
+ {
665
+ style: {
666
+ width: 6,
667
+ height: 6,
668
+ borderRadius: "50%",
669
+ background: "#999",
670
+ display: "block",
671
+ animation: `hermes-bounce 1.2s ease-in-out ${i * 0.18}s infinite`
672
+ }
673
+ },
674
+ i
675
+ ))
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: "#999" }, children: text })
679
+ ]
680
+ }
681
+ );
682
+ };
683
+ var DefaultMessage = ({ message, isOwn, onEdit, onDelete, onReact, onReply, renderAvatar }) => {
684
+ const [hovered, setHovered] = useState7(false);
685
+ const [pickerOpen, setPickerOpen] = useState7(false);
686
+ if (message.isDeleted) {
687
+ return /* @__PURE__ */ jsx(
688
+ "div",
689
+ {
690
+ style: {
691
+ opacity: 0.5,
692
+ fontStyle: "italic",
693
+ padding: "4px 16px",
694
+ fontSize: 13
695
+ },
696
+ children: "This message was deleted."
697
+ }
698
+ );
699
+ }
700
+ return /* @__PURE__ */ jsxs(
701
+ "div",
702
+ {
703
+ onMouseEnter: () => setHovered(true),
704
+ onMouseLeave: () => {
705
+ setHovered(false);
706
+ },
707
+ style: {
708
+ display: "flex",
709
+ flexDirection: isOwn ? "row-reverse" : "row",
710
+ alignItems: "flex-end",
711
+ gap: 8,
712
+ marginBottom: 4,
713
+ position: "relative"
714
+ },
715
+ children: [
716
+ !isOwn && /* @__PURE__ */ jsx("div", { style: { flexShrink: 0 }, children: renderAvatar ? renderAvatar(message.senderId) : /* @__PURE__ */ jsx(
717
+ "div",
718
+ {
719
+ style: {
720
+ width: 32,
721
+ height: 32,
722
+ borderRadius: "50%",
723
+ background: "#e0e0e0",
724
+ display: "flex",
725
+ alignItems: "center",
726
+ justifyContent: "center",
727
+ fontSize: 12,
728
+ fontWeight: 600
729
+ },
730
+ children: message.senderId.slice(-2).toUpperCase()
731
+ }
732
+ ) }),
733
+ /* @__PURE__ */ jsxs(
734
+ "div",
735
+ {
736
+ style: {
737
+ maxWidth: "70%",
738
+ display: "flex",
739
+ flexDirection: "column",
740
+ alignItems: isOwn ? "flex-end" : "flex-start"
741
+ },
742
+ children: [
743
+ (onEdit || onDelete || onReact || onReply) && /* @__PURE__ */ jsxs(
744
+ "div",
745
+ {
746
+ style: {
747
+ display: "flex",
748
+ flexDirection: isOwn ? "row-reverse" : "row",
749
+ gap: 2,
750
+ marginBottom: 4,
751
+ opacity: hovered ? 1 : 0,
752
+ pointerEvents: hovered ? "auto" : "none",
753
+ transition: "opacity 0.15s ease",
754
+ position: "relative"
755
+ },
756
+ children: [
757
+ onReact && /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
758
+ /* @__PURE__ */ jsx(
759
+ ActionBtn,
760
+ {
761
+ onClick: () => setPickerOpen((p) => !p),
762
+ title: "React",
763
+ children: "\u{1FAE0}"
764
+ }
765
+ ),
766
+ pickerOpen && /* @__PURE__ */ jsx(
767
+ EmojiPicker,
768
+ {
769
+ isOwn,
770
+ onPick: (emoji) => onReact(message._id, emoji),
771
+ onClose: () => setPickerOpen(false)
772
+ }
773
+ )
774
+ ] }),
775
+ onReply && /* @__PURE__ */ jsx(ActionBtn, { onClick: () => onReply(message), title: "Reply", children: "\u21A9" }),
776
+ isOwn && onEdit && message.type === "text" && /* @__PURE__ */ jsx(
777
+ ActionBtn,
778
+ {
779
+ onClick: () => {
780
+ const text = window.prompt("Edit message:", message.text);
781
+ if (text) onEdit(message._id, text);
782
+ },
783
+ title: "Edit",
784
+ children: "\u270F\uFE0F"
785
+ }
786
+ ),
787
+ isOwn && onDelete && /* @__PURE__ */ jsx(ActionBtn, { onClick: () => onDelete(message._id), title: "Delete", children: "\u{1F5D1}" })
788
+ ]
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsxs(
792
+ "div",
793
+ {
794
+ style: {
795
+ padding: "8px 12px",
796
+ borderRadius: isOwn ? "16px 16px 4px 16px" : "16px 16px 16px 4px",
797
+ background: isOwn ? "#0084ff" : "#f0f0f0",
798
+ color: isOwn ? "#fff" : "#000"
799
+ },
800
+ children: [
801
+ message.replyTo && /* @__PURE__ */ jsx(
802
+ "div",
803
+ {
804
+ style: {
805
+ borderLeft: "3px solid rgba(255,255,255,0.4)",
806
+ paddingLeft: 8,
807
+ marginBottom: 6,
808
+ fontSize: 12,
809
+ opacity: 0.75
810
+ },
811
+ children: "Replying to a message"
812
+ }
813
+ ),
814
+ message.type === "text" && /* @__PURE__ */ jsxs("p", { style: { margin: 0, wordBreak: "break-word" }, children: [
815
+ message.text,
816
+ message.editedAt && /* @__PURE__ */ jsx("span", { style: { fontSize: 10, opacity: 0.6, marginLeft: 6 }, children: "(edited)" })
817
+ ] }),
818
+ message.type === "link" && /* @__PURE__ */ jsxs("div", { children: [
819
+ message.text && /* @__PURE__ */ jsx("p", { style: { margin: "0 0 4px" }, children: message.text }),
820
+ /* @__PURE__ */ jsx(
821
+ "a",
822
+ {
823
+ href: message.url,
824
+ target: "_blank",
825
+ rel: "noopener noreferrer",
826
+ style: {
827
+ color: isOwn ? "#cce4ff" : "#0084ff",
828
+ wordBreak: "break-all"
829
+ },
830
+ children: message.url
831
+ }
832
+ )
833
+ ] }),
834
+ message.type === "image" && /* @__PURE__ */ jsx(
835
+ "img",
836
+ {
837
+ src: message.url,
838
+ alt: message.fileName || "image",
839
+ style: { maxWidth: "100%", borderRadius: 8, display: "block" }
840
+ }
841
+ ),
842
+ message.type === "video" && /* @__PURE__ */ jsx(
843
+ "video",
844
+ {
845
+ src: message.url,
846
+ controls: true,
847
+ style: { maxWidth: "100%", borderRadius: 8 }
848
+ }
849
+ ),
850
+ message.type === "audio" && /* @__PURE__ */ jsx("audio", { src: message.url, controls: true, style: { width: "100%" } }),
851
+ message.type === "document" && /* @__PURE__ */ jsxs(
852
+ "a",
853
+ {
854
+ href: message.url,
855
+ target: "_blank",
856
+ rel: "noopener noreferrer",
857
+ style: {
858
+ display: "flex",
859
+ alignItems: "center",
860
+ gap: 8,
861
+ color: isOwn ? "#fff" : "#333",
862
+ textDecoration: "none"
863
+ },
864
+ children: [
865
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 24 }, children: "\u{1F4C4}" }),
866
+ /* @__PURE__ */ jsxs("div", { children: [
867
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: 13 }, children: message.fileName }),
868
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: formatFileSize(message.fileSize) })
869
+ ] })
870
+ ]
871
+ }
872
+ ),
873
+ /* @__PURE__ */ jsx(
874
+ "div",
875
+ {
876
+ style: {
877
+ fontSize: 10,
878
+ opacity: 0.6,
879
+ textAlign: "right",
880
+ marginTop: 4
881
+ },
882
+ children: formatTime(message.createdAt)
883
+ }
884
+ )
885
+ ]
886
+ }
887
+ ),
888
+ message.reactions?.filter((r) => r.users.length > 0).length > 0 && /* @__PURE__ */ jsx(
889
+ "div",
890
+ {
891
+ style: { display: "flex", gap: 4, flexWrap: "wrap", marginTop: 4 },
892
+ children: message.reactions.filter((r) => r.users.length > 0).map((r) => /* @__PURE__ */ jsxs(
893
+ "span",
894
+ {
895
+ onClick: () => onReact?.(message._id, r.emoji),
896
+ style: {
897
+ background: "#f0f0f0",
898
+ border: "1px solid rgba(0,0,0,0.08)",
899
+ borderRadius: 20,
900
+ padding: "2px 8px",
901
+ fontSize: 13,
902
+ cursor: "pointer",
903
+ display: "flex",
904
+ alignItems: "center",
905
+ gap: 4,
906
+ transition: "transform 0.1s",
907
+ userSelect: "none"
908
+ },
909
+ onMouseEnter: (e) => e.currentTarget.style.transform = "scale(1.1)",
910
+ onMouseLeave: (e) => e.currentTarget.style.transform = "scale(1)",
911
+ children: [
912
+ r.emoji,
913
+ /* @__PURE__ */ jsx(
914
+ "span",
915
+ {
916
+ style: { fontSize: 11, fontWeight: 600, color: "#555" },
917
+ children: r.users.length
918
+ }
919
+ )
920
+ ]
921
+ },
922
+ r.emoji
923
+ ))
924
+ }
925
+ )
926
+ ]
927
+ }
928
+ )
929
+ ]
930
+ }
931
+ );
932
+ };
933
+ var ActionBtn = ({ onClick, title, children }) => /* @__PURE__ */ jsx(
934
+ "button",
935
+ {
936
+ onClick,
937
+ title,
938
+ style: {
939
+ background: "#fff",
940
+ border: "1px solid rgba(0,0,0,0.1)",
941
+ borderRadius: 8,
942
+ cursor: "pointer",
943
+ fontSize: 14,
944
+ padding: "3px 6px",
945
+ lineHeight: 1,
946
+ boxShadow: "0 1px 4px rgba(0,0,0,0.1)",
947
+ transition: "transform 0.1s"
948
+ },
949
+ onMouseEnter: (e) => e.currentTarget.style.transform = "scale(1.15)",
950
+ onMouseLeave: (e) => e.currentTarget.style.transform = "scale(1)",
951
+ children
952
+ }
953
+ );
954
+ var MessageList = ({
955
+ messages,
956
+ currentUser,
957
+ loading = false,
958
+ loadingMore = false,
959
+ hasMore = false,
960
+ onLoadMore,
961
+ onEdit,
962
+ onDelete,
963
+ onReact,
964
+ onReply,
965
+ renderMessage,
966
+ renderAvatar,
967
+ className = "",
968
+ autoScroll = true,
969
+ typingUsers = []
970
+ }) => {
971
+ const bottomRef = useRef4(null);
972
+ const containerRef = useRef4(null);
973
+ useEffect6(() => {
974
+ if (autoScroll && bottomRef.current) {
975
+ bottomRef.current.scrollIntoView({ behavior: "smooth" });
976
+ }
977
+ }, [messages, autoScroll]);
978
+ useEffect6(() => {
979
+ const container = containerRef.current;
980
+ if (!container || !onLoadMore) return;
981
+ const onScroll = () => {
982
+ if (container.scrollTop === 0 && hasMore && !loadingMore) onLoadMore();
983
+ };
984
+ container.addEventListener("scroll", onScroll);
985
+ return () => container.removeEventListener("scroll", onScroll);
986
+ }, [hasMore, loadingMore, onLoadMore]);
987
+ if (loading) {
988
+ return /* @__PURE__ */ jsx(
989
+ "div",
990
+ {
991
+ style: {
992
+ display: "flex",
993
+ alignItems: "center",
994
+ justifyContent: "center",
995
+ height: "100%"
996
+ },
997
+ children: "Loading messages..."
998
+ }
999
+ );
1000
+ }
1001
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1002
+ /* @__PURE__ */ jsx("style", { children: `
1003
+ @keyframes hermes-bounce {
1004
+ 0%, 80%, 100% { transform: translateY(0); }
1005
+ 40% { transform: translateY(-5px); }
1006
+ }
1007
+ @keyframes hermes-pop {
1008
+ from { opacity: 0; transform: scale(0.85); }
1009
+ to { opacity: 1; transform: scale(1); }
1010
+ }
1011
+ ` }),
1012
+ /* @__PURE__ */ jsxs(
1013
+ "div",
1014
+ {
1015
+ ref: containerRef,
1016
+ className: `hermes-message-list ${className}`,
1017
+ style: {
1018
+ overflowY: "auto",
1019
+ display: "flex",
1020
+ flexDirection: "column",
1021
+ height: "100%",
1022
+ padding: "16px"
1023
+ },
1024
+ children: [
1025
+ hasMore && /* @__PURE__ */ jsx("div", { style: { textAlign: "center", marginBottom: 12 }, children: loadingMore ? /* @__PURE__ */ jsx("span", { style: { fontSize: 12, opacity: 0.5 }, children: "Loading older messages..." }) : /* @__PURE__ */ jsx(
1026
+ "button",
1027
+ {
1028
+ onClick: onLoadMore,
1029
+ style: {
1030
+ background: "none",
1031
+ border: "1px solid #ddd",
1032
+ borderRadius: 12,
1033
+ padding: "4px 12px",
1034
+ cursor: "pointer",
1035
+ fontSize: 12
1036
+ },
1037
+ children: "Load older messages"
1038
+ }
1039
+ ) }),
1040
+ messages.length === 0 && /* @__PURE__ */ jsx(
1041
+ "div",
1042
+ {
1043
+ style: {
1044
+ textAlign: "center",
1045
+ opacity: 0.4,
1046
+ margin: "auto",
1047
+ fontSize: 14
1048
+ },
1049
+ children: "No messages yet. Say hello! \u{1F44B}"
1050
+ }
1051
+ ),
1052
+ messages.map((message) => {
1053
+ const isOwn = message.senderId === currentUser.userId;
1054
+ return /* @__PURE__ */ jsx("div", { style: { marginBottom: 8 }, children: renderMessage ? renderMessage(message, isOwn) : /* @__PURE__ */ jsx(
1055
+ DefaultMessage,
1056
+ {
1057
+ message,
1058
+ isOwn,
1059
+ onEdit,
1060
+ onDelete,
1061
+ onReact,
1062
+ onReply,
1063
+ renderAvatar
1064
+ }
1065
+ ) }, message._id);
1066
+ }),
1067
+ /* @__PURE__ */ jsx(TypingIndicator, { typingUsers }),
1068
+ /* @__PURE__ */ jsx("div", { ref: bottomRef })
1069
+ ]
1070
+ }
1071
+ )
1072
+ ] });
1073
+ };
1074
+
1075
+ // src/react/components/ChatInput.tsx
1076
+ import { useState as useState8, useRef as useRef5, useCallback as useCallback8 } from "react";
1077
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1078
+ var ChatInput = ({
1079
+ onSendText,
1080
+ onSendFile,
1081
+ onTypingStart,
1082
+ onTypingStop,
1083
+ replyingTo,
1084
+ onCancelReply,
1085
+ disabled = false,
1086
+ placeholder = "Type a message...",
1087
+ maxLength = 4e3,
1088
+ className = "",
1089
+ inputClassName = "",
1090
+ renderAttachIcon,
1091
+ renderSendIcon
1092
+ }) => {
1093
+ const [text, setText] = useState8("");
1094
+ const [sending, setSending] = useState8(false);
1095
+ const fileRef = useRef5(null);
1096
+ const textareaRef = useRef5(null);
1097
+ const resizeTextarea = useCallback8(() => {
1098
+ const el = textareaRef.current;
1099
+ if (!el) return;
1100
+ el.style.height = "auto";
1101
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
1102
+ }, []);
1103
+ const handleChange = (e) => {
1104
+ setText(e.target.value);
1105
+ resizeTextarea();
1106
+ onTypingStart?.();
1107
+ };
1108
+ const handleSend = async () => {
1109
+ const trimmed = text.trim();
1110
+ if (!trimmed || sending || disabled) return;
1111
+ setSending(true);
1112
+ try {
1113
+ await onSendText(trimmed);
1114
+ setText("");
1115
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
1116
+ onTypingStop?.();
1117
+ } finally {
1118
+ setSending(false);
1119
+ }
1120
+ };
1121
+ const handleKeyDown = (e) => {
1122
+ if (e.key === "Enter" && !e.shiftKey) {
1123
+ e.preventDefault();
1124
+ handleSend();
1125
+ }
1126
+ };
1127
+ const handleFileChange = async (e) => {
1128
+ const file = e.target.files?.[0];
1129
+ if (!file || !onSendFile) return;
1130
+ await onSendFile(file);
1131
+ if (fileRef.current) fileRef.current.value = "";
1132
+ };
1133
+ return /* @__PURE__ */ jsxs2(
1134
+ "div",
1135
+ {
1136
+ className: `hermes-chat-input ${className}`,
1137
+ style: {
1138
+ display: "flex",
1139
+ flexDirection: "column",
1140
+ padding: "8px 12px",
1141
+ borderTop: "1px solid #e0e0e0"
1142
+ },
1143
+ children: [
1144
+ replyingTo && /* @__PURE__ */ jsxs2(
1145
+ "div",
1146
+ {
1147
+ className: "hermes-chat-input__reply",
1148
+ style: {
1149
+ display: "flex",
1150
+ alignItems: "center",
1151
+ justifyContent: "space-between",
1152
+ padding: "6px 10px",
1153
+ marginBottom: 6,
1154
+ background: "#f5f5f5",
1155
+ borderRadius: 8,
1156
+ borderLeft: "3px solid #0084ff",
1157
+ fontSize: 12
1158
+ },
1159
+ children: [
1160
+ /* @__PURE__ */ jsxs2("div", { style: { overflow: "hidden" }, children: [
1161
+ /* @__PURE__ */ jsx2("span", { style: { fontWeight: 600, marginRight: 4 }, children: "Replying to:" }),
1162
+ /* @__PURE__ */ jsx2("span", { style: { opacity: 0.7 }, children: replyingTo.type === "text" ? replyingTo.text?.slice(0, 60) : `[${replyingTo.type}]` })
1163
+ ] }),
1164
+ /* @__PURE__ */ jsx2(
1165
+ "button",
1166
+ {
1167
+ onClick: onCancelReply,
1168
+ style: {
1169
+ background: "none",
1170
+ border: "none",
1171
+ cursor: "pointer",
1172
+ fontSize: 16,
1173
+ lineHeight: 1
1174
+ },
1175
+ children: "\u2715"
1176
+ }
1177
+ )
1178
+ ]
1179
+ }
1180
+ ),
1181
+ /* @__PURE__ */ jsxs2(
1182
+ "div",
1183
+ {
1184
+ className: "hermes-chat-input__row",
1185
+ style: { display: "flex", alignItems: "flex-end", gap: 8 },
1186
+ children: [
1187
+ onSendFile && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1188
+ /* @__PURE__ */ jsx2(
1189
+ "button",
1190
+ {
1191
+ onClick: () => fileRef.current?.click(),
1192
+ disabled,
1193
+ className: "hermes-chat-input__attach",
1194
+ style: {
1195
+ background: "none",
1196
+ border: "none",
1197
+ cursor: "pointer",
1198
+ padding: 6,
1199
+ flexShrink: 0,
1200
+ opacity: disabled ? 0.4 : 1
1201
+ },
1202
+ children: renderAttachIcon ? renderAttachIcon() : /* @__PURE__ */ jsx2(
1203
+ "svg",
1204
+ {
1205
+ width: "20",
1206
+ height: "20",
1207
+ viewBox: "0 0 24 24",
1208
+ fill: "none",
1209
+ stroke: "currentColor",
1210
+ strokeWidth: "2",
1211
+ strokeLinecap: "round",
1212
+ strokeLinejoin: "round",
1213
+ children: /* @__PURE__ */ jsx2("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" })
1214
+ }
1215
+ )
1216
+ }
1217
+ ),
1218
+ /* @__PURE__ */ jsx2(
1219
+ "input",
1220
+ {
1221
+ ref: fileRef,
1222
+ type: "file",
1223
+ style: { display: "none" },
1224
+ onChange: handleFileChange,
1225
+ accept: "image/*,video/*,audio/*,.pdf,.doc,.docx,.txt"
1226
+ }
1227
+ )
1228
+ ] }),
1229
+ /* @__PURE__ */ jsx2(
1230
+ "textarea",
1231
+ {
1232
+ ref: textareaRef,
1233
+ value: text,
1234
+ onChange: handleChange,
1235
+ onKeyDown: handleKeyDown,
1236
+ onBlur: () => onTypingStop?.(),
1237
+ placeholder,
1238
+ disabled,
1239
+ maxLength,
1240
+ rows: 1,
1241
+ className: `hermes-chat-input__textarea ${inputClassName}`,
1242
+ style: {
1243
+ flex: 1,
1244
+ resize: "none",
1245
+ border: "1px solid #e0e0e0",
1246
+ borderRadius: 20,
1247
+ padding: "8px 14px",
1248
+ fontSize: 14,
1249
+ lineHeight: 1.5,
1250
+ outline: "none",
1251
+ overflow: "hidden",
1252
+ background: disabled ? "#f5f5f5" : "#fff"
1253
+ }
1254
+ }
1255
+ ),
1256
+ /* @__PURE__ */ jsx2(
1257
+ "button",
1258
+ {
1259
+ onClick: handleSend,
1260
+ disabled: !text.trim() || sending || disabled,
1261
+ className: "hermes-chat-input__send",
1262
+ style: {
1263
+ background: "none",
1264
+ border: "none",
1265
+ cursor: "pointer",
1266
+ padding: 6,
1267
+ flexShrink: 0,
1268
+ opacity: !text.trim() || sending || disabled ? 0.4 : 1
1269
+ },
1270
+ children: renderSendIcon ? renderSendIcon() : /* @__PURE__ */ jsx2("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1271
+ }
1272
+ )
1273
+ ]
1274
+ }
1275
+ ),
1276
+ text.length > maxLength * 0.8 && /* @__PURE__ */ jsxs2(
1277
+ "div",
1278
+ {
1279
+ style: {
1280
+ fontSize: 10,
1281
+ textAlign: "right",
1282
+ opacity: 0.5,
1283
+ marginTop: 2
1284
+ },
1285
+ children: [
1286
+ text.length,
1287
+ "/",
1288
+ maxLength
1289
+ ]
1290
+ }
1291
+ )
1292
+ ]
1293
+ }
1294
+ );
1295
+ };
1296
+
1297
+ // src/react/components/RoomList.tsx
1298
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1299
+ var formatLastActivity = (iso) => {
1300
+ const date = new Date(iso);
1301
+ const now = /* @__PURE__ */ new Date();
1302
+ const diffMs = now.getTime() - date.getTime();
1303
+ const diffMins = Math.floor(diffMs / 6e4);
1304
+ const diffHours = Math.floor(diffMins / 60);
1305
+ const diffDays = Math.floor(diffHours / 24);
1306
+ if (diffMins < 1) return "now";
1307
+ if (diffMins < 60) return `${diffMins}m`;
1308
+ if (diffHours < 24) return `${diffHours}h`;
1309
+ if (diffDays < 7) return `${diffDays}d`;
1310
+ return date.toLocaleDateString();
1311
+ };
1312
+ var getRoomName = (room, currentUserId) => {
1313
+ if (room.type === "group") return room.name ?? "Group";
1314
+ const other = room.members.find((m) => m !== currentUserId);
1315
+ return other ?? "Direct Message";
1316
+ };
1317
+ var getLastMessagePreview = (room) => {
1318
+ const msg = room.lastMessage;
1319
+ if (!msg) return "No messages yet";
1320
+ if (msg.isDeleted) return "Message deleted";
1321
+ if (msg.type === "text") return msg.text?.slice(0, 50) ?? "";
1322
+ if (msg.type === "image") return "\u{1F4F7} Image";
1323
+ if (msg.type === "video") return "\u{1F3A5} Video";
1324
+ if (msg.type === "audio") return "\u{1F3B5} Audio";
1325
+ if (msg.type === "document") return `\u{1F4C4} ${msg.fileName ?? "File"}`;
1326
+ if (msg.type === "link") return `\u{1F517} ${msg.url}`;
1327
+ return "";
1328
+ };
1329
+ var DefaultRoomItem = ({ room, isActive, currentUserId, renderAvatar, itemClassName }) => /* @__PURE__ */ jsxs3(
1330
+ "div",
1331
+ {
1332
+ className: `hermes-room-item ${isActive ? "hermes-room-item--active" : ""} ${itemClassName ?? ""}`,
1333
+ style: {
1334
+ display: "flex",
1335
+ alignItems: "center",
1336
+ gap: 10,
1337
+ padding: "10px 12px",
1338
+ cursor: "pointer",
1339
+ background: isActive ? "rgba(0,132,255,0.08)" : "transparent",
1340
+ borderLeft: isActive ? "3px solid #0084ff" : "3px solid transparent"
1341
+ },
1342
+ children: [
1343
+ /* @__PURE__ */ jsx3("div", { style: { flexShrink: 0 }, children: renderAvatar ? renderAvatar(room) : /* @__PURE__ */ jsx3(
1344
+ "div",
1345
+ {
1346
+ style: {
1347
+ width: 42,
1348
+ height: 42,
1349
+ borderRadius: "50%",
1350
+ background: "#e0e0e0",
1351
+ display: "flex",
1352
+ alignItems: "center",
1353
+ justifyContent: "center",
1354
+ fontWeight: 700,
1355
+ fontSize: 16
1356
+ },
1357
+ children: room.type === "group" ? "G" : "D"
1358
+ }
1359
+ ) }),
1360
+ /* @__PURE__ */ jsxs3("div", { style: { flex: 1, overflow: "hidden" }, children: [
1361
+ /* @__PURE__ */ jsxs3(
1362
+ "div",
1363
+ {
1364
+ style: {
1365
+ display: "flex",
1366
+ justifyContent: "space-between",
1367
+ alignItems: "baseline"
1368
+ },
1369
+ children: [
1370
+ /* @__PURE__ */ jsx3(
1371
+ "span",
1372
+ {
1373
+ style: {
1374
+ fontWeight: 600,
1375
+ fontSize: 14,
1376
+ overflow: "hidden",
1377
+ textOverflow: "ellipsis",
1378
+ whiteSpace: "nowrap"
1379
+ },
1380
+ children: getRoomName(room, currentUserId)
1381
+ }
1382
+ ),
1383
+ /* @__PURE__ */ jsx3(
1384
+ "span",
1385
+ {
1386
+ style: { fontSize: 11, opacity: 0.5, flexShrink: 0, marginLeft: 4 },
1387
+ children: formatLastActivity(room.lastActivity)
1388
+ }
1389
+ )
1390
+ ]
1391
+ }
1392
+ ),
1393
+ /* @__PURE__ */ jsxs3(
1394
+ "div",
1395
+ {
1396
+ style: {
1397
+ display: "flex",
1398
+ justifyContent: "space-between",
1399
+ alignItems: "center",
1400
+ marginTop: 2
1401
+ },
1402
+ children: [
1403
+ /* @__PURE__ */ jsx3(
1404
+ "span",
1405
+ {
1406
+ style: {
1407
+ fontSize: 13,
1408
+ opacity: 0.6,
1409
+ overflow: "hidden",
1410
+ textOverflow: "ellipsis",
1411
+ whiteSpace: "nowrap"
1412
+ },
1413
+ children: getLastMessagePreview(room)
1414
+ }
1415
+ ),
1416
+ room.unreadCount > 0 && /* @__PURE__ */ jsx3(
1417
+ "span",
1418
+ {
1419
+ style: {
1420
+ background: "#0084ff",
1421
+ color: "#fff",
1422
+ borderRadius: 10,
1423
+ fontSize: 11,
1424
+ fontWeight: 700,
1425
+ padding: "1px 7px",
1426
+ flexShrink: 0,
1427
+ marginLeft: 4
1428
+ },
1429
+ children: room.unreadCount > 99 ? "99+" : room.unreadCount
1430
+ }
1431
+ )
1432
+ ]
1433
+ }
1434
+ )
1435
+ ] })
1436
+ ]
1437
+ }
1438
+ );
1439
+ var RoomList = ({
1440
+ rooms,
1441
+ activeRoomId,
1442
+ currentUserId,
1443
+ loading = false,
1444
+ onSelectRoom,
1445
+ onCreateDirect,
1446
+ onCreateGroup,
1447
+ renderRoomItem,
1448
+ renderAvatar,
1449
+ renderEmpty,
1450
+ className = "",
1451
+ itemClassName = ""
1452
+ }) => {
1453
+ return /* @__PURE__ */ jsxs3(
1454
+ "div",
1455
+ {
1456
+ className: `hermes-room-list ${className}`,
1457
+ style: {
1458
+ display: "flex",
1459
+ flexDirection: "column",
1460
+ height: "100%",
1461
+ overflowY: "auto"
1462
+ },
1463
+ children: [
1464
+ (onCreateDirect || onCreateGroup) && /* @__PURE__ */ jsxs3(
1465
+ "div",
1466
+ {
1467
+ style: {
1468
+ display: "flex",
1469
+ gap: 8,
1470
+ padding: "10px 12px",
1471
+ borderBottom: "1px solid #e0e0e0"
1472
+ },
1473
+ children: [
1474
+ onCreateDirect && /* @__PURE__ */ jsx3(
1475
+ "button",
1476
+ {
1477
+ onClick: onCreateDirect,
1478
+ style: {
1479
+ flex: 1,
1480
+ background: "#0084ff",
1481
+ color: "#fff",
1482
+ border: "none",
1483
+ borderRadius: 8,
1484
+ padding: "8px 10px",
1485
+ cursor: "pointer",
1486
+ fontSize: 13,
1487
+ fontWeight: 600
1488
+ },
1489
+ children: "+ Direct"
1490
+ }
1491
+ ),
1492
+ onCreateGroup && /* @__PURE__ */ jsx3(
1493
+ "button",
1494
+ {
1495
+ onClick: onCreateGroup,
1496
+ style: {
1497
+ flex: 1,
1498
+ background: "none",
1499
+ border: "1px solid #e0e0e0",
1500
+ borderRadius: 8,
1501
+ padding: "8px 10px",
1502
+ cursor: "pointer",
1503
+ fontSize: 13,
1504
+ fontWeight: 600
1505
+ },
1506
+ children: "+ Group"
1507
+ }
1508
+ )
1509
+ ]
1510
+ }
1511
+ ),
1512
+ loading && /* @__PURE__ */ jsx3("div", { style: { padding: "12px 16px", opacity: 0.5, fontSize: 13 }, children: "Loading rooms..." }),
1513
+ !loading && rooms.length === 0 && /* @__PURE__ */ jsx3(
1514
+ "div",
1515
+ {
1516
+ style: {
1517
+ textAlign: "center",
1518
+ padding: 24,
1519
+ opacity: 0.4,
1520
+ fontSize: 13
1521
+ },
1522
+ children: renderEmpty ? renderEmpty() : "No conversations yet."
1523
+ }
1524
+ ),
1525
+ !loading && rooms.map((room) => {
1526
+ const isActive = room._id === activeRoomId;
1527
+ return /* @__PURE__ */ jsx3("div", { onClick: () => onSelectRoom(room), children: renderRoomItem ? renderRoomItem(room, isActive) : /* @__PURE__ */ jsx3(
1528
+ DefaultRoomItem,
1529
+ {
1530
+ room,
1531
+ isActive,
1532
+ currentUserId,
1533
+ renderAvatar,
1534
+ itemClassName
1535
+ }
1536
+ ) }, room._id);
1537
+ })
1538
+ ]
1539
+ }
1540
+ );
1541
+ };
1542
+
1543
+ // src/react/components/TypingIndicator.tsx
1544
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1545
+ var TypingIndicator2 = ({
1546
+ typingText,
1547
+ className = ""
1548
+ }) => {
1549
+ if (!typingText) return null;
1550
+ return /* @__PURE__ */ jsxs4(
1551
+ "div",
1552
+ {
1553
+ className: `hermes-typing-indicator ${className}`,
1554
+ style: {
1555
+ display: "flex",
1556
+ alignItems: "center",
1557
+ gap: 6,
1558
+ padding: "4px 16px",
1559
+ minHeight: 24
1560
+ },
1561
+ children: [
1562
+ /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 3 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx4(
1563
+ "span",
1564
+ {
1565
+ style: {
1566
+ width: 6,
1567
+ height: 6,
1568
+ borderRadius: "50%",
1569
+ background: "#aaa",
1570
+ display: "block",
1571
+ animation: `hermes-bounce 1.2s ease-in-out ${i * 0.2}s infinite`
1572
+ }
1573
+ },
1574
+ i
1575
+ )) }),
1576
+ /* @__PURE__ */ jsx4("span", { style: { fontSize: 12, opacity: 0.6 }, children: typingText }),
1577
+ /* @__PURE__ */ jsx4("style", { children: `
1578
+ @keyframes hermes-bounce {
1579
+ 0%, 80%, 100% { transform: translateY(0); }
1580
+ 40% { transform: translateY(-4px); }
1581
+ }
1582
+ ` })
1583
+ ]
1584
+ }
1585
+ );
1586
+ };
1587
+
1588
+ // src/react/components/OnlineBadge.tsx
1589
+ import { jsx as jsx5 } from "react/jsx-runtime";
1590
+ var OnlineBadge = ({
1591
+ isOnline,
1592
+ size = 10,
1593
+ className = ""
1594
+ }) => /* @__PURE__ */ jsx5(
1595
+ "span",
1596
+ {
1597
+ className: `hermes-online-badge ${isOnline ? "hermes-online-badge--online" : "hermes-online-badge--offline"} ${className}`,
1598
+ "data-online": isOnline,
1599
+ style: {
1600
+ display: "inline-block",
1601
+ width: size,
1602
+ height: size,
1603
+ borderRadius: "50%",
1604
+ background: isOnline ? "#22c55e" : "#d1d5db",
1605
+ boxShadow: isOnline ? "0 0 0 2px #fff" : "none",
1606
+ flexShrink: 0
1607
+ }
1608
+ }
1609
+ );
1610
+
1611
+ // src/react/components/ReactionPicker.tsx
1612
+ import { useState as useState9, useRef as useRef6, useEffect as useEffect7 } from "react";
1613
+ import EmojiPicker2, { Theme } from "emoji-picker-react";
1614
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1615
+ var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F525}", "\u{1F389}", "\u{1F44F}"];
1616
+ var ReactionPicker = ({
1617
+ onSelect,
1618
+ currentReactions = [],
1619
+ currentUserId,
1620
+ emojis = DEFAULT_EMOJIS,
1621
+ className = "",
1622
+ align = "left"
1623
+ }) => {
1624
+ const [showPicker, setShowPicker] = useState9(false);
1625
+ const containerRef = useRef6(null);
1626
+ const hasReacted = (emoji) => {
1627
+ if (!currentUserId) return false;
1628
+ return currentReactions.find((r) => r.emoji === emoji)?.users.includes(currentUserId) ?? false;
1629
+ };
1630
+ const handleEmojiClick = (emojiData) => {
1631
+ onSelect(emojiData.emoji);
1632
+ setShowPicker(false);
1633
+ };
1634
+ useEffect7(() => {
1635
+ const handleOutsideClick = (e) => {
1636
+ if (!containerRef.current) return;
1637
+ const target = e.target;
1638
+ if (!containerRef.current.contains(target)) {
1639
+ setShowPicker(false);
1640
+ }
1641
+ };
1642
+ if (showPicker) {
1643
+ window.addEventListener("click", handleOutsideClick);
1644
+ }
1645
+ return () => window.removeEventListener("click", handleOutsideClick);
1646
+ }, [showPicker]);
1647
+ return /* @__PURE__ */ jsxs5(
1648
+ "div",
1649
+ {
1650
+ ref: containerRef,
1651
+ style: { position: "relative", display: "inline-block" },
1652
+ className,
1653
+ children: [
1654
+ /* @__PURE__ */ jsxs5(
1655
+ "div",
1656
+ {
1657
+ style: {
1658
+ display: "flex",
1659
+ gap: 4,
1660
+ flexWrap: "wrap",
1661
+ padding: "6px 8px",
1662
+ background: "#111",
1663
+ borderRadius: 12,
1664
+ border: "1px solid rgba(255,255,255,0.08)"
1665
+ },
1666
+ children: [
1667
+ emojis.map((emoji) => /* @__PURE__ */ jsx6(
1668
+ "button",
1669
+ {
1670
+ onClick: () => onSelect(emoji),
1671
+ style: {
1672
+ background: hasReacted(emoji) ? "rgba(57,255,20,0.12)" : "transparent",
1673
+ border: hasReacted(emoji) ? "1px solid rgba(57,255,20,0.35)" : "1px solid transparent",
1674
+ borderRadius: 8,
1675
+ padding: "4px 6px",
1676
+ cursor: "pointer",
1677
+ fontSize: 18,
1678
+ lineHeight: 1,
1679
+ transition: "transform 0.12s ease"
1680
+ },
1681
+ onMouseEnter: (e) => e.currentTarget.style.transform = "scale(1.2)",
1682
+ onMouseLeave: (e) => e.currentTarget.style.transform = "scale(1)",
1683
+ children: emoji
1684
+ },
1685
+ emoji
1686
+ )),
1687
+ /* @__PURE__ */ jsx6(
1688
+ "button",
1689
+ {
1690
+ onClick: (e) => {
1691
+ e.stopPropagation();
1692
+ setShowPicker((v) => !v);
1693
+ },
1694
+ style: {
1695
+ borderRadius: 8,
1696
+ padding: "4px 6px",
1697
+ cursor: "pointer",
1698
+ fontSize: 18,
1699
+ border: "1px solid transparent",
1700
+ background: "transparent"
1701
+ },
1702
+ children: "\u2795"
1703
+ }
1704
+ )
1705
+ ]
1706
+ }
1707
+ ),
1708
+ showPicker && /* @__PURE__ */ jsx6(
1709
+ "div",
1710
+ {
1711
+ onMouseDown: (e) => e.stopPropagation(),
1712
+ onClick: (e) => e.stopPropagation(),
1713
+ style: {
1714
+ position: "absolute",
1715
+ bottom: "calc(100% + 6px)",
1716
+ [align === "right" ? "right" : "left"]: 0,
1717
+ zIndex: 50,
1718
+ animation: "hermes-pop 0.15s ease"
1719
+ },
1720
+ children: /* @__PURE__ */ jsx6(
1721
+ EmojiPicker2,
1722
+ {
1723
+ theme: Theme.DARK,
1724
+ onEmojiClick: handleEmojiClick,
1725
+ height: 440,
1726
+ width: 360,
1727
+ searchPlaceHolder: "Search emoji...",
1728
+ lazyLoadEmojis: true
1729
+ }
1730
+ )
1731
+ }
1732
+ ),
1733
+ /* @__PURE__ */ jsx6("style", { children: `
1734
+ @keyframes hermes-pop {
1735
+ from {
1736
+ opacity: 0;
1737
+ transform: scale(0.85);
1738
+ }
1739
+ to {
1740
+ opacity: 1;
1741
+ transform: scale(1);
1742
+ }
1743
+ }
1744
+ ` })
1745
+ ]
1746
+ }
1747
+ );
1748
+ };
1749
+
1750
+ // src/react/components/MediaMessage.tsx
1751
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1752
+ var formatFileSize2 = (bytes) => {
1753
+ if (!bytes) return "";
1754
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1755
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1756
+ return `${bytes} B`;
1757
+ };
1758
+ var MediaMessage = ({
1759
+ message,
1760
+ className = "",
1761
+ maxWidth = 300
1762
+ }) => {
1763
+ if (!message.url) return null;
1764
+ return /* @__PURE__ */ jsxs6(
1765
+ "div",
1766
+ {
1767
+ className: `hermes-media-message hermes-media-message--${message.type} ${className}`,
1768
+ style: { maxWidth },
1769
+ children: [
1770
+ message.type === "image" && /* @__PURE__ */ jsx7(
1771
+ "img",
1772
+ {
1773
+ src: message.url,
1774
+ alt: message.fileName ?? "image",
1775
+ style: {
1776
+ width: "100%",
1777
+ borderRadius: 10,
1778
+ display: "block",
1779
+ cursor: "pointer"
1780
+ },
1781
+ onClick: () => window.open(message.url, "_blank")
1782
+ }
1783
+ ),
1784
+ message.type === "video" && /* @__PURE__ */ jsx7(
1785
+ "video",
1786
+ {
1787
+ src: message.url,
1788
+ poster: message.thumbnail,
1789
+ controls: true,
1790
+ style: { width: "100%", borderRadius: 10 }
1791
+ }
1792
+ ),
1793
+ message.type === "audio" && /* @__PURE__ */ jsxs6(
1794
+ "div",
1795
+ {
1796
+ style: { display: "flex", alignItems: "center", gap: 8, padding: 8 },
1797
+ children: [
1798
+ /* @__PURE__ */ jsx7("span", { style: { fontSize: 20 }, children: "\u{1F3B5}" }),
1799
+ /* @__PURE__ */ jsx7("audio", { src: message.url, controls: true, style: { flex: 1, height: 36 } })
1800
+ ]
1801
+ }
1802
+ ),
1803
+ message.type === "document" && /* @__PURE__ */ jsxs6(
1804
+ "a",
1805
+ {
1806
+ href: message.url,
1807
+ target: "_blank",
1808
+ rel: "noopener noreferrer",
1809
+ style: {
1810
+ display: "flex",
1811
+ alignItems: "center",
1812
+ gap: 10,
1813
+ padding: "10px 12px",
1814
+ borderRadius: 10,
1815
+ border: "1px solid #e0e0e0",
1816
+ textDecoration: "none",
1817
+ color: "inherit"
1818
+ },
1819
+ children: [
1820
+ /* @__PURE__ */ jsx7("span", { style: { fontSize: 28, flexShrink: 0 }, children: "\u{1F4C4}" }),
1821
+ /* @__PURE__ */ jsxs6("div", { style: { overflow: "hidden" }, children: [
1822
+ /* @__PURE__ */ jsx7(
1823
+ "div",
1824
+ {
1825
+ style: {
1826
+ fontWeight: 600,
1827
+ fontSize: 13,
1828
+ overflow: "hidden",
1829
+ textOverflow: "ellipsis",
1830
+ whiteSpace: "nowrap"
1831
+ },
1832
+ children: message.fileName ?? "Document"
1833
+ }
1834
+ ),
1835
+ /* @__PURE__ */ jsxs6("div", { style: { fontSize: 11, opacity: 0.6 }, children: [
1836
+ formatFileSize2(message.fileSize),
1837
+ " \xB7 Click to download"
1838
+ ] })
1839
+ ] })
1840
+ ]
1841
+ }
1842
+ ),
1843
+ message.type === "link" && /* @__PURE__ */ jsxs6(
1844
+ "a",
1845
+ {
1846
+ href: message.url,
1847
+ target: "_blank",
1848
+ rel: "noopener noreferrer",
1849
+ style: {
1850
+ display: "flex",
1851
+ alignItems: "center",
1852
+ gap: 8,
1853
+ padding: "8px 12px",
1854
+ borderRadius: 10,
1855
+ border: "1px solid #e0e0e0",
1856
+ textDecoration: "none",
1857
+ color: "#0084ff",
1858
+ wordBreak: "break-all",
1859
+ fontSize: 13
1860
+ },
1861
+ children: [
1862
+ "\u{1F517} ",
1863
+ message.url
1864
+ ]
1865
+ }
1866
+ )
1867
+ ]
1868
+ }
1869
+ );
1870
+ };
1871
+ export {
1872
+ ChatInput,
1873
+ MediaMessage,
1874
+ MessageList,
1875
+ OnlineBadge,
1876
+ ReactionPicker,
1877
+ RoomList,
1878
+ TypingIndicator2 as TypingIndicator,
1879
+ useMessages,
1880
+ usePresence,
1881
+ useReactions,
1882
+ useReadReceipts,
1883
+ useRooms,
1884
+ useTyping,
1885
+ useUpload
1886
+ };
1887
+ //# sourceMappingURL=react.js.map