@scalemule/chat 0.0.5 → 0.0.8

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 CHANGED
@@ -1,8 +1,1067 @@
1
- import { ChatClient } from './chunk-ZLMMNFZL.js';
2
- export { ChatClient } from './chunk-ZLMMNFZL.js';
3
- import { createContext, useState, useEffect, useCallback, useRef, useContext } from 'react';
4
- import { jsx } from 'react/jsx-runtime';
1
+ import { ChatClient } from './chunk-5O5YLRJL.js';
2
+ export { ChatClient } from './chunk-5O5YLRJL.js';
3
+ import React4, { createContext, useState, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { createPortal } from 'react-dom';
5
6
 
7
+ function ChatInput({
8
+ onSend,
9
+ onTypingChange,
10
+ onUploadAttachment,
11
+ placeholder = "Type a message..."
12
+ }) {
13
+ const [content, setContent] = useState("");
14
+ const [attachments, setAttachments] = useState([]);
15
+ const [isSending, setIsSending] = useState(false);
16
+ const [isDragging, setIsDragging] = useState(false);
17
+ const fileInputRef = useRef(null);
18
+ const typingTimeoutRef = useRef(null);
19
+ const readyAttachments = useMemo(
20
+ () => attachments.filter((attachment) => attachment.attachment).map((attachment) => attachment.attachment),
21
+ [attachments]
22
+ );
23
+ const uploadingCount = attachments.filter((attachment) => !attachment.attachment && !attachment.error).length;
24
+ const emitTyping = () => {
25
+ onTypingChange?.(true);
26
+ if (typingTimeoutRef.current) {
27
+ clearTimeout(typingTimeoutRef.current);
28
+ }
29
+ typingTimeoutRef.current = setTimeout(() => {
30
+ onTypingChange?.(false);
31
+ }, 2500);
32
+ };
33
+ const handleFiles = async (fileList) => {
34
+ if (!onUploadAttachment) return;
35
+ const files = Array.from(fileList);
36
+ for (const file of files) {
37
+ const id = `${file.name}:${file.size}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
38
+ setAttachments((current) => [
39
+ ...current,
40
+ {
41
+ id,
42
+ fileName: file.name,
43
+ progress: 0
44
+ }
45
+ ]);
46
+ const result = await onUploadAttachment(file, (progress) => {
47
+ setAttachments(
48
+ (current) => current.map(
49
+ (attachment) => attachment.id === id ? { ...attachment, progress } : attachment
50
+ )
51
+ );
52
+ });
53
+ setAttachments(
54
+ (current) => current.map((attachment) => {
55
+ if (attachment.id !== id) return attachment;
56
+ if (result?.data) {
57
+ return {
58
+ ...attachment,
59
+ progress: 100,
60
+ attachment: result.data
61
+ };
62
+ }
63
+ return {
64
+ ...attachment,
65
+ error: result?.error?.message ?? "Upload failed"
66
+ };
67
+ })
68
+ );
69
+ }
70
+ };
71
+ const handleSubmit = async () => {
72
+ const trimmed = content.trim();
73
+ if (!trimmed && !readyAttachments.length || isSending || uploadingCount > 0) return;
74
+ setIsSending(true);
75
+ try {
76
+ await onSend(trimmed, readyAttachments);
77
+ setContent("");
78
+ setAttachments([]);
79
+ onTypingChange?.(false);
80
+ } finally {
81
+ setIsSending(false);
82
+ }
83
+ };
84
+ return /* @__PURE__ */ jsxs(
85
+ "div",
86
+ {
87
+ onDragOver: (event) => {
88
+ event.preventDefault();
89
+ if (onUploadAttachment) {
90
+ setIsDragging(true);
91
+ }
92
+ },
93
+ onDragLeave: () => setIsDragging(false),
94
+ onDrop: (event) => {
95
+ event.preventDefault();
96
+ setIsDragging(false);
97
+ void handleFiles(event.dataTransfer.files);
98
+ },
99
+ style: {
100
+ borderTop: "1px solid var(--sm-border-color, #e5e7eb)",
101
+ background: isDragging ? "rgba(37, 99, 235, 0.06)" : "var(--sm-surface, #fff)",
102
+ padding: 12,
103
+ display: "flex",
104
+ flexDirection: "column",
105
+ gap: 10
106
+ },
107
+ children: [
108
+ attachments.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: attachments.map((attachment) => /* @__PURE__ */ jsxs(
109
+ "div",
110
+ {
111
+ style: {
112
+ display: "inline-flex",
113
+ alignItems: "center",
114
+ gap: 8,
115
+ padding: "6px 10px",
116
+ borderRadius: 999,
117
+ background: "var(--sm-surface-muted, #f8fafc)",
118
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
119
+ fontSize: 12
120
+ },
121
+ children: [
122
+ /* @__PURE__ */ jsx("span", { children: attachment.fileName }),
123
+ /* @__PURE__ */ jsx("span", { style: { color: attachment.error ? "#dc2626" : "var(--sm-muted-text, #6b7280)" }, children: attachment.error ?? `${attachment.progress}%` }),
124
+ /* @__PURE__ */ jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ onClick: () => {
129
+ setAttachments((current) => current.filter((item) => item.id !== attachment.id));
130
+ },
131
+ style: {
132
+ border: "none",
133
+ background: "transparent",
134
+ cursor: "pointer",
135
+ color: "var(--sm-muted-text, #6b7280)"
136
+ },
137
+ children: "x"
138
+ }
139
+ )
140
+ ]
141
+ },
142
+ attachment.id
143
+ )) }) : null,
144
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, alignItems: "flex-end" }, children: [
145
+ /* @__PURE__ */ jsx(
146
+ "textarea",
147
+ {
148
+ value: content,
149
+ onChange: (event) => {
150
+ setContent(event.target.value);
151
+ emitTyping();
152
+ },
153
+ onKeyDown: (event) => {
154
+ if (event.key === "Enter" && !event.shiftKey) {
155
+ event.preventDefault();
156
+ void handleSubmit();
157
+ }
158
+ },
159
+ rows: 1,
160
+ placeholder,
161
+ style: {
162
+ flex: 1,
163
+ minHeight: 44,
164
+ maxHeight: 120,
165
+ resize: "vertical",
166
+ borderRadius: 14,
167
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
168
+ padding: "12px 14px",
169
+ font: "inherit",
170
+ color: "var(--sm-text-color, #111827)"
171
+ }
172
+ }
173
+ ),
174
+ onUploadAttachment ? /* @__PURE__ */ jsxs(Fragment, { children: [
175
+ /* @__PURE__ */ jsx(
176
+ "input",
177
+ {
178
+ ref: fileInputRef,
179
+ type: "file",
180
+ hidden: true,
181
+ multiple: true,
182
+ accept: "image/*,video/*,audio/*",
183
+ onChange: (event) => {
184
+ if (event.target.files) {
185
+ void handleFiles(event.target.files);
186
+ event.target.value = "";
187
+ }
188
+ }
189
+ }
190
+ ),
191
+ /* @__PURE__ */ jsx(
192
+ "button",
193
+ {
194
+ type: "button",
195
+ onClick: () => fileInputRef.current?.click(),
196
+ "aria-label": "Attach files",
197
+ style: {
198
+ width: 44,
199
+ height: 44,
200
+ borderRadius: 14,
201
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
202
+ background: "var(--sm-surface, #fff)",
203
+ cursor: "pointer",
204
+ color: "var(--sm-text-color, #111827)"
205
+ },
206
+ children: "+"
207
+ }
208
+ )
209
+ ] }) : null,
210
+ /* @__PURE__ */ jsx(
211
+ "button",
212
+ {
213
+ type: "button",
214
+ onClick: () => void handleSubmit(),
215
+ disabled: isSending || uploadingCount > 0 || !content.trim() && !readyAttachments.length,
216
+ style: {
217
+ height: 44,
218
+ padding: "0 16px",
219
+ borderRadius: 14,
220
+ border: "none",
221
+ background: "var(--sm-primary, #2563eb)",
222
+ color: "#fff",
223
+ cursor: isSending ? "wait" : "pointer",
224
+ opacity: isSending || uploadingCount > 0 ? 0.75 : 1
225
+ },
226
+ children: "Send"
227
+ }
228
+ )
229
+ ] })
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ var QUICK_REACTIONS = ["\u2764\uFE0F", "\u{1F602}", "\u{1F44D}", "\u{1F525}", "\u{1F62E}", "\u{1F622}", "\u{1F44F}", "\u{1F64C}"];
235
+ function EmojiPicker({
236
+ onSelect,
237
+ onClose,
238
+ anchorRef,
239
+ emojis = QUICK_REACTIONS
240
+ }) {
241
+ const ref = useRef(null);
242
+ const [position, setPosition] = useState(null);
243
+ useEffect(() => {
244
+ const anchor = anchorRef.current;
245
+ if (!anchor) return;
246
+ const rect = anchor.getBoundingClientRect();
247
+ const pickerWidth = emojis.length * 36 + 16;
248
+ let left = rect.left + rect.width / 2 - pickerWidth / 2;
249
+ if (left < 8) left = 8;
250
+ if (left + pickerWidth > window.innerWidth - 8)
251
+ left = window.innerWidth - 8 - pickerWidth;
252
+ setPosition({ top: rect.top - 8, left });
253
+ }, [anchorRef, emojis.length]);
254
+ useEffect(() => {
255
+ function handleClickOutside(e) {
256
+ if (ref.current && !ref.current.contains(e.target) && anchorRef.current && !anchorRef.current.contains(e.target)) {
257
+ onClose();
258
+ }
259
+ }
260
+ document.addEventListener("mousedown", handleClickOutside);
261
+ return () => document.removeEventListener("mousedown", handleClickOutside);
262
+ }, [onClose, anchorRef]);
263
+ if (!position) return null;
264
+ return createPortal(
265
+ /* @__PURE__ */ jsx(
266
+ "div",
267
+ {
268
+ ref,
269
+ style: {
270
+ position: "fixed",
271
+ top: position.top,
272
+ left: position.left,
273
+ transform: "translateY(-100%)",
274
+ background: "var(--sm-surface, #fff)",
275
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
276
+ borderRadius: 12,
277
+ boxShadow: "0 10px 25px rgba(15, 23, 42, 0.12)",
278
+ padding: 8,
279
+ display: "flex",
280
+ gap: 4,
281
+ zIndex: 9999
282
+ },
283
+ children: emojis.map((emoji) => /* @__PURE__ */ jsx(
284
+ "button",
285
+ {
286
+ onClick: () => {
287
+ onSelect(emoji);
288
+ onClose();
289
+ },
290
+ type: "button",
291
+ "aria-label": `React with ${emoji}`,
292
+ style: {
293
+ width: 32,
294
+ height: 32,
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ borderRadius: 8,
299
+ border: "none",
300
+ background: "transparent",
301
+ cursor: "pointer",
302
+ fontSize: 18,
303
+ transition: "background 0.15s ease"
304
+ },
305
+ onMouseEnter: (e) => {
306
+ e.target.style.background = "var(--sm-surface-muted, #f8fafc)";
307
+ },
308
+ onMouseLeave: (e) => {
309
+ e.target.style.background = "transparent";
310
+ },
311
+ children: emoji
312
+ },
313
+ emoji
314
+ ))
315
+ }
316
+ ),
317
+ document.body
318
+ );
319
+ }
320
+
321
+ // src/react-components/utils.ts
322
+ var timeFormatter = new Intl.DateTimeFormat(void 0, {
323
+ hour: "numeric",
324
+ minute: "2-digit"
325
+ });
326
+ var dateFormatter = new Intl.DateTimeFormat(void 0, {
327
+ month: "short",
328
+ day: "numeric",
329
+ year: "numeric"
330
+ });
331
+ function formatMessageTime(value) {
332
+ if (!value) return "";
333
+ return timeFormatter.format(new Date(value));
334
+ }
335
+ function formatDayLabel(value) {
336
+ if (!value) return "";
337
+ return dateFormatter.format(new Date(value));
338
+ }
339
+ function isSameDay(left, right) {
340
+ if (!left || !right) return false;
341
+ const leftDate = new Date(left);
342
+ const rightDate = new Date(right);
343
+ return leftDate.getFullYear() === rightDate.getFullYear() && leftDate.getMonth() === rightDate.getMonth() && leftDate.getDate() === rightDate.getDate();
344
+ }
345
+ function renderAttachment(messageId, attachment) {
346
+ const key = `${messageId}:${attachment.file_id}`;
347
+ const url = attachment.presigned_url;
348
+ if (!url) {
349
+ return /* @__PURE__ */ jsx(
350
+ "div",
351
+ {
352
+ style: {
353
+ padding: "10px 12px",
354
+ borderRadius: 12,
355
+ background: "rgba(255,255,255,0.16)",
356
+ fontSize: 13
357
+ },
358
+ children: attachment.file_name
359
+ },
360
+ key
361
+ );
362
+ }
363
+ if (attachment.mime_type.startsWith("image/")) {
364
+ return /* @__PURE__ */ jsx(
365
+ "img",
366
+ {
367
+ src: url,
368
+ alt: attachment.file_name,
369
+ loading: "lazy",
370
+ style: {
371
+ display: "block",
372
+ maxWidth: "100%",
373
+ borderRadius: 12,
374
+ marginTop: 8
375
+ }
376
+ },
377
+ key
378
+ );
379
+ }
380
+ if (attachment.mime_type.startsWith("video/")) {
381
+ return /* @__PURE__ */ jsx(
382
+ "video",
383
+ {
384
+ controls: true,
385
+ src: url,
386
+ style: {
387
+ display: "block",
388
+ width: "100%",
389
+ maxWidth: 320,
390
+ borderRadius: 12,
391
+ marginTop: 8
392
+ }
393
+ },
394
+ key
395
+ );
396
+ }
397
+ if (attachment.mime_type.startsWith("audio/")) {
398
+ return /* @__PURE__ */ jsx(
399
+ "audio",
400
+ {
401
+ controls: true,
402
+ src: url,
403
+ style: {
404
+ display: "block",
405
+ width: "100%",
406
+ marginTop: 8
407
+ }
408
+ },
409
+ key
410
+ );
411
+ }
412
+ return /* @__PURE__ */ jsx(
413
+ "a",
414
+ {
415
+ href: url,
416
+ target: "_blank",
417
+ rel: "noreferrer",
418
+ style: {
419
+ display: "inline-block",
420
+ marginTop: 8,
421
+ color: "inherit",
422
+ fontSize: 13
423
+ },
424
+ children: attachment.file_name
425
+ },
426
+ key
427
+ );
428
+ }
429
+ function ChatMessageItem({
430
+ message,
431
+ currentUserId,
432
+ onAddReaction,
433
+ onRemoveReaction,
434
+ onReport,
435
+ highlight = false
436
+ }) {
437
+ const [showPicker, setShowPicker] = useState(false);
438
+ const isOwn = Boolean(currentUserId && message.sender_id === currentUserId);
439
+ const canReact = Boolean(onAddReaction || onRemoveReaction);
440
+ const reactionEntries = useMemo(() => message.reactions ?? [], [message.reactions]);
441
+ return /* @__PURE__ */ jsxs(
442
+ "div",
443
+ {
444
+ style: {
445
+ display: "flex",
446
+ flexDirection: "column",
447
+ alignItems: isOwn ? "flex-end" : "flex-start",
448
+ gap: 6
449
+ },
450
+ children: [
451
+ /* @__PURE__ */ jsxs(
452
+ "div",
453
+ {
454
+ style: {
455
+ maxWidth: "min(82%, 560px)",
456
+ padding: message.attachments?.length ? 10 : "10px 12px",
457
+ borderRadius: "var(--sm-border-radius, 16px)",
458
+ background: isOwn ? "var(--sm-own-bubble, #2563eb)" : "var(--sm-other-bubble, #f3f4f6)",
459
+ color: isOwn ? "var(--sm-own-text, #fff)" : "var(--sm-other-text, #111827)",
460
+ boxShadow: highlight ? "0 0 0 2px rgba(37, 99, 235, 0.22)" : "none",
461
+ transition: "box-shadow 0.2s ease"
462
+ },
463
+ children: [
464
+ message.content ? /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 14 }, children: message.content }) : null,
465
+ message.attachments?.map((attachment) => renderAttachment(message.id, attachment))
466
+ ]
467
+ }
468
+ ),
469
+ /* @__PURE__ */ jsxs(
470
+ "div",
471
+ {
472
+ style: {
473
+ display: "flex",
474
+ alignItems: "center",
475
+ gap: 8,
476
+ color: "var(--sm-muted-text, #6b7280)",
477
+ fontSize: 12
478
+ },
479
+ children: [
480
+ /* @__PURE__ */ jsx("span", { children: formatMessageTime(message.created_at) }),
481
+ message.is_edited ? /* @__PURE__ */ jsx("span", { children: "edited" }) : null,
482
+ onReport && !isOwn ? /* @__PURE__ */ jsx(
483
+ "button",
484
+ {
485
+ type: "button",
486
+ onClick: () => void onReport(message.id),
487
+ style: {
488
+ border: "none",
489
+ background: "transparent",
490
+ color: "inherit",
491
+ cursor: "pointer",
492
+ fontSize: 12,
493
+ padding: 0
494
+ },
495
+ children: "Report"
496
+ }
497
+ ) : null,
498
+ canReact ? /* @__PURE__ */ jsx(
499
+ "button",
500
+ {
501
+ type: "button",
502
+ onClick: () => setShowPicker((value) => !value),
503
+ style: {
504
+ border: "none",
505
+ background: "transparent",
506
+ color: "inherit",
507
+ cursor: "pointer",
508
+ fontSize: 12,
509
+ padding: 0
510
+ },
511
+ children: "React"
512
+ }
513
+ ) : null
514
+ ]
515
+ }
516
+ ),
517
+ showPicker && canReact ? /* @__PURE__ */ jsx(
518
+ EmojiPicker,
519
+ {
520
+ onSelect: (emoji) => {
521
+ setShowPicker(false);
522
+ void onAddReaction?.(message.id, emoji);
523
+ }
524
+ }
525
+ ) : null,
526
+ reactionEntries.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: reactionEntries.map((reaction) => {
527
+ const reacted = Boolean(currentUserId && reaction.user_ids.includes(currentUserId));
528
+ return /* @__PURE__ */ jsxs(
529
+ "button",
530
+ {
531
+ type: "button",
532
+ onClick: () => {
533
+ if (reacted) {
534
+ void onRemoveReaction?.(message.id, reaction.emoji);
535
+ return;
536
+ }
537
+ void onAddReaction?.(message.id, reaction.emoji);
538
+ },
539
+ style: {
540
+ border: reacted ? "1px solid rgba(37, 99, 235, 0.4)" : "1px solid var(--sm-border-color, #e5e7eb)",
541
+ background: reacted ? "rgba(37, 99, 235, 0.08)" : "var(--sm-surface, #fff)",
542
+ color: "var(--sm-text-color, #111827)",
543
+ borderRadius: 999,
544
+ padding: "4px 8px",
545
+ cursor: "pointer",
546
+ fontSize: 12
547
+ },
548
+ children: [
549
+ reaction.emoji,
550
+ " ",
551
+ reaction.count
552
+ ]
553
+ },
554
+ `${message.id}:${reaction.emoji}`
555
+ );
556
+ }) }) : null
557
+ ]
558
+ }
559
+ );
560
+ }
561
+ function getUnreadIndex(messages, unreadSince) {
562
+ if (!unreadSince) return -1;
563
+ return messages.findIndex((message) => new Date(message.created_at).getTime() > new Date(unreadSince).getTime());
564
+ }
565
+ function ChatMessageList({
566
+ messages,
567
+ currentUserId,
568
+ unreadSince,
569
+ scrollToUnreadOnMount = true,
570
+ onAddReaction,
571
+ onRemoveReaction,
572
+ onReport,
573
+ emptyState
574
+ }) {
575
+ const containerRef = useRef(null);
576
+ const unreadMarkerRef = useRef(null);
577
+ const lastMessageCountRef = useRef(messages.length);
578
+ const didScrollToUnreadRef = useRef(false);
579
+ const [showJumpToLatest, setShowJumpToLatest] = useState(false);
580
+ const unreadIndex = useMemo(() => getUnreadIndex(messages, unreadSince), [messages, unreadSince]);
581
+ useEffect(() => {
582
+ const container = containerRef.current;
583
+ if (!container) return;
584
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
585
+ const shouldStickToBottom = distanceFromBottom < 80;
586
+ if (messages.length > lastMessageCountRef.current && shouldStickToBottom) {
587
+ container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
588
+ setShowJumpToLatest(false);
589
+ } else if (messages.length > lastMessageCountRef.current) {
590
+ setShowJumpToLatest(true);
591
+ }
592
+ lastMessageCountRef.current = messages.length;
593
+ }, [messages]);
594
+ useEffect(() => {
595
+ if (!scrollToUnreadOnMount || unreadIndex < 0 || didScrollToUnreadRef.current) {
596
+ return;
597
+ }
598
+ unreadMarkerRef.current?.scrollIntoView({ block: "center" });
599
+ didScrollToUnreadRef.current = true;
600
+ }, [scrollToUnreadOnMount, unreadIndex, messages.length]);
601
+ if (!messages.length) {
602
+ return /* @__PURE__ */ jsx(
603
+ "div",
604
+ {
605
+ style: {
606
+ flex: 1,
607
+ display: "flex",
608
+ alignItems: "center",
609
+ justifyContent: "center",
610
+ color: "var(--sm-muted-text, #6b7280)",
611
+ fontSize: 14,
612
+ padding: 24
613
+ },
614
+ children: emptyState ?? "No messages yet"
615
+ }
616
+ );
617
+ }
618
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, minHeight: 0 }, children: [
619
+ /* @__PURE__ */ jsx(
620
+ "div",
621
+ {
622
+ ref: containerRef,
623
+ onScroll: (event) => {
624
+ const element = event.currentTarget;
625
+ const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
626
+ setShowJumpToLatest(distanceFromBottom > 120);
627
+ },
628
+ style: {
629
+ height: "100%",
630
+ overflowY: "auto",
631
+ padding: 16,
632
+ display: "flex",
633
+ flexDirection: "column",
634
+ gap: 12,
635
+ background: "var(--sm-surface-muted, #f8fafc)"
636
+ },
637
+ children: messages.map((message, index) => {
638
+ const previousMessage = messages[index - 1];
639
+ const showDateDivider = !previousMessage || !isSameDay(previousMessage.created_at, message.created_at);
640
+ const showUnreadDivider = unreadIndex === index;
641
+ return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
642
+ showDateDivider ? /* @__PURE__ */ jsx(
643
+ "div",
644
+ {
645
+ style: {
646
+ alignSelf: "center",
647
+ fontSize: 12,
648
+ color: "var(--sm-muted-text, #6b7280)",
649
+ padding: "4px 10px",
650
+ borderRadius: 999,
651
+ background: "rgba(148, 163, 184, 0.12)"
652
+ },
653
+ children: formatDayLabel(message.created_at)
654
+ }
655
+ ) : null,
656
+ showUnreadDivider ? /* @__PURE__ */ jsxs(
657
+ "div",
658
+ {
659
+ ref: unreadMarkerRef,
660
+ style: {
661
+ display: "flex",
662
+ alignItems: "center",
663
+ gap: 10,
664
+ color: "var(--sm-primary, #2563eb)",
665
+ fontSize: 12,
666
+ fontWeight: 600
667
+ },
668
+ children: [
669
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, height: 1, background: "rgba(37, 99, 235, 0.28)" } }),
670
+ /* @__PURE__ */ jsx("span", { children: "New messages" }),
671
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, height: 1, background: "rgba(37, 99, 235, 0.28)" } })
672
+ ]
673
+ }
674
+ ) : null,
675
+ /* @__PURE__ */ jsx(
676
+ ChatMessageItem,
677
+ {
678
+ message,
679
+ currentUserId,
680
+ onAddReaction,
681
+ onRemoveReaction,
682
+ onReport,
683
+ highlight: showUnreadDivider
684
+ }
685
+ )
686
+ ] }, message.id);
687
+ })
688
+ }
689
+ ),
690
+ showJumpToLatest ? /* @__PURE__ */ jsx(
691
+ "button",
692
+ {
693
+ type: "button",
694
+ onClick: () => {
695
+ containerRef.current?.scrollTo({
696
+ top: containerRef.current.scrollHeight,
697
+ behavior: "smooth"
698
+ });
699
+ setShowJumpToLatest(false);
700
+ },
701
+ style: {
702
+ position: "absolute",
703
+ right: 16,
704
+ bottom: 16,
705
+ border: "none",
706
+ borderRadius: 999,
707
+ background: "var(--sm-primary, #2563eb)",
708
+ color: "#fff",
709
+ padding: "10px 14px",
710
+ cursor: "pointer",
711
+ boxShadow: "0 12px 28px rgba(37, 99, 235, 0.28)"
712
+ },
713
+ children: "New messages"
714
+ }
715
+ ) : null
716
+ ] });
717
+ }
718
+
719
+ // src/react-components/theme.ts
720
+ function themeToStyle(theme) {
721
+ return {
722
+ "--sm-primary": theme?.primary ?? "#2563eb",
723
+ "--sm-own-bubble": theme?.ownBubble ?? theme?.primary ?? "#2563eb",
724
+ "--sm-own-text": theme?.ownText ?? "#ffffff",
725
+ "--sm-other-bubble": theme?.otherBubble ?? "#f3f4f6",
726
+ "--sm-other-text": theme?.otherText ?? "#111827",
727
+ "--sm-surface": theme?.surface ?? "#ffffff",
728
+ "--sm-surface-muted": theme?.surfaceMuted ?? "#f8fafc",
729
+ "--sm-border-color": theme?.borderColor ?? "#e5e7eb",
730
+ "--sm-text-color": theme?.textColor ?? "#111827",
731
+ "--sm-muted-text": theme?.mutedText ?? "#6b7280",
732
+ "--sm-border-radius": typeof theme?.borderRadius === "number" ? `${theme.borderRadius}px` : theme?.borderRadius ?? "16px",
733
+ "--sm-font-family": theme?.fontFamily ?? 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
734
+ };
735
+ }
736
+ function inferMessageType(content, attachments) {
737
+ if (!attachments.length) return "text";
738
+ if (!content && attachments.every((attachment) => attachment.mime_type.startsWith("image/"))) {
739
+ return "image";
740
+ }
741
+ return "file";
742
+ }
743
+ function ChatThread({
744
+ conversationId,
745
+ theme,
746
+ currentUserId,
747
+ title = "Chat",
748
+ subtitle
749
+ }) {
750
+ const client = useChatClient();
751
+ const resolvedUserId = currentUserId ?? client.userId;
752
+ const {
753
+ messages,
754
+ readStatuses,
755
+ isLoading,
756
+ error,
757
+ sendMessage,
758
+ markRead,
759
+ addReaction,
760
+ removeReaction,
761
+ reportMessage,
762
+ uploadAttachment
763
+ } = useChat(conversationId);
764
+ const { typingUsers, sendTyping } = useTyping(conversationId);
765
+ const { members } = usePresence(conversationId);
766
+ const ownReadStatus = useMemo(
767
+ () => resolvedUserId ? readStatuses.find((status) => status.user_id === resolvedUserId)?.last_read_at : void 0,
768
+ [readStatuses, resolvedUserId]
769
+ );
770
+ const otherTypingUsers = useMemo(
771
+ () => typingUsers.filter((userId) => userId !== resolvedUserId),
772
+ [typingUsers, resolvedUserId]
773
+ );
774
+ const activeMembers = useMemo(
775
+ () => members.filter((member) => member.userId !== resolvedUserId),
776
+ [members, resolvedUserId]
777
+ );
778
+ useEffect(() => {
779
+ if (!messages.length) return;
780
+ void markRead();
781
+ }, [markRead, messages.length, messages[messages.length - 1]?.id]);
782
+ return /* @__PURE__ */ jsxs(
783
+ "div",
784
+ {
785
+ "data-scalemule-chat": "",
786
+ style: {
787
+ ...themeToStyle(theme),
788
+ display: "flex",
789
+ flexDirection: "column",
790
+ height: "100%",
791
+ minHeight: 320,
792
+ borderRadius: "var(--sm-border-radius, 16px)",
793
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
794
+ background: "var(--sm-surface, #fff)",
795
+ color: "var(--sm-text-color, #111827)",
796
+ fontFamily: "var(--sm-font-family)",
797
+ overflow: "hidden"
798
+ },
799
+ children: [
800
+ /* @__PURE__ */ jsxs(
801
+ "div",
802
+ {
803
+ style: {
804
+ padding: "16px 18px",
805
+ borderBottom: "1px solid var(--sm-border-color, #e5e7eb)",
806
+ display: "flex",
807
+ justifyContent: "space-between",
808
+ alignItems: "center",
809
+ gap: 16
810
+ },
811
+ children: [
812
+ /* @__PURE__ */ jsxs("div", { children: [
813
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 16, fontWeight: 700 }, children: title }),
814
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, color: "var(--sm-muted-text, #6b7280)" }, children: otherTypingUsers.length ? "Typing..." : subtitle ?? (activeMembers.length ? `${activeMembers.length} online` : "No one online") })
815
+ ] }),
816
+ /* @__PURE__ */ jsxs(
817
+ "div",
818
+ {
819
+ style: {
820
+ display: "inline-flex",
821
+ alignItems: "center",
822
+ gap: 8,
823
+ fontSize: 12,
824
+ color: "var(--sm-muted-text, #6b7280)"
825
+ },
826
+ children: [
827
+ /* @__PURE__ */ jsx(
828
+ "span",
829
+ {
830
+ style: {
831
+ width: 10,
832
+ height: 10,
833
+ borderRadius: 999,
834
+ background: activeMembers.length ? "#22c55e" : "#94a3b8"
835
+ }
836
+ }
837
+ ),
838
+ activeMembers.length ? "Online" : "Away"
839
+ ]
840
+ }
841
+ )
842
+ ]
843
+ }
844
+ ),
845
+ error ? /* @__PURE__ */ jsx(
846
+ "div",
847
+ {
848
+ style: {
849
+ padding: "10px 16px",
850
+ fontSize: 13,
851
+ color: "#b91c1c",
852
+ background: "#fef2f2",
853
+ borderBottom: "1px solid #fecaca"
854
+ },
855
+ children: error
856
+ }
857
+ ) : null,
858
+ /* @__PURE__ */ jsx(
859
+ ChatMessageList,
860
+ {
861
+ messages,
862
+ currentUserId: resolvedUserId,
863
+ unreadSince: ownReadStatus,
864
+ onAddReaction: (messageId, emoji) => void addReaction(messageId, emoji),
865
+ onRemoveReaction: (messageId, emoji) => void removeReaction(messageId, emoji),
866
+ onReport: (messageId) => void reportMessage(messageId, "other"),
867
+ emptyState: isLoading ? "Loading messages..." : "Start the conversation"
868
+ }
869
+ ),
870
+ /* @__PURE__ */ jsx(
871
+ "div",
872
+ {
873
+ style: {
874
+ minHeight: otherTypingUsers.length ? 28 : 0,
875
+ padding: otherTypingUsers.length ? "0 16px 8px" : 0,
876
+ fontSize: 12,
877
+ color: "var(--sm-muted-text, #6b7280)"
878
+ },
879
+ children: otherTypingUsers.length ? "Someone is typing..." : null
880
+ }
881
+ ),
882
+ /* @__PURE__ */ jsx(
883
+ ChatInput,
884
+ {
885
+ onSend: async (content, attachments) => {
886
+ await sendMessage(content, {
887
+ attachments,
888
+ message_type: inferMessageType(content, attachments)
889
+ });
890
+ },
891
+ onTypingChange: (isTyping) => {
892
+ sendTyping(isTyping);
893
+ },
894
+ onUploadAttachment: uploadAttachment
895
+ }
896
+ )
897
+ ]
898
+ }
899
+ );
900
+ }
901
+ function formatPreview(conversation) {
902
+ if (conversation.last_message_preview) {
903
+ return conversation.last_message_preview;
904
+ }
905
+ return conversation.name ?? conversation.id;
906
+ }
907
+ function ConversationList({
908
+ conversationType,
909
+ selectedConversationId,
910
+ onSelect,
911
+ theme,
912
+ title = "Conversations"
913
+ }) {
914
+ const { conversations, isLoading } = useConversations({
915
+ conversationType
916
+ });
917
+ const [search, setSearch] = useState("");
918
+ const filtered = useMemo(() => {
919
+ const query = search.trim().toLowerCase();
920
+ if (!query) return conversations;
921
+ return conversations.filter((conversation) => {
922
+ const haystack = [
923
+ conversation.name,
924
+ conversation.last_message_preview,
925
+ conversation.counterparty_user_id
926
+ ].filter(Boolean).join(" ").toLowerCase();
927
+ return haystack.includes(query);
928
+ });
929
+ }, [conversations, search]);
930
+ return /* @__PURE__ */ jsxs(
931
+ "div",
932
+ {
933
+ "data-scalemule-chat": "",
934
+ style: {
935
+ ...themeToStyle(theme),
936
+ display: "flex",
937
+ flexDirection: "column",
938
+ height: "100%",
939
+ minHeight: 280,
940
+ borderRadius: "var(--sm-border-radius, 16px)",
941
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
942
+ background: "var(--sm-surface, #fff)",
943
+ color: "var(--sm-text-color, #111827)",
944
+ fontFamily: "var(--sm-font-family)",
945
+ overflow: "hidden"
946
+ },
947
+ children: [
948
+ /* @__PURE__ */ jsxs(
949
+ "div",
950
+ {
951
+ style: {
952
+ padding: 16,
953
+ borderBottom: "1px solid var(--sm-border-color, #e5e7eb)",
954
+ display: "flex",
955
+ flexDirection: "column",
956
+ gap: 10
957
+ },
958
+ children: [
959
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 16, fontWeight: 700 }, children: title }),
960
+ /* @__PURE__ */ jsx(
961
+ "input",
962
+ {
963
+ value: search,
964
+ onChange: (event) => setSearch(event.target.value),
965
+ placeholder: "Search conversations",
966
+ style: {
967
+ width: "100%",
968
+ borderRadius: 12,
969
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
970
+ padding: "10px 12px",
971
+ font: "inherit"
972
+ }
973
+ }
974
+ )
975
+ ]
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto" }, children: isLoading ? /* @__PURE__ */ jsx(
979
+ "div",
980
+ {
981
+ style: {
982
+ padding: 24,
983
+ fontSize: 14,
984
+ color: "var(--sm-muted-text, #6b7280)"
985
+ },
986
+ children: "Loading conversations..."
987
+ }
988
+ ) : !filtered.length ? /* @__PURE__ */ jsx(
989
+ "div",
990
+ {
991
+ style: {
992
+ padding: 24,
993
+ fontSize: 14,
994
+ color: "var(--sm-muted-text, #6b7280)"
995
+ },
996
+ children: "No conversations found"
997
+ }
998
+ ) : filtered.map((conversation) => {
999
+ const selected = conversation.id === selectedConversationId;
1000
+ return /* @__PURE__ */ jsxs(
1001
+ "button",
1002
+ {
1003
+ type: "button",
1004
+ onClick: () => onSelect?.(conversation),
1005
+ style: {
1006
+ width: "100%",
1007
+ border: "none",
1008
+ borderBottom: "1px solid var(--sm-border-color, #e5e7eb)",
1009
+ padding: 16,
1010
+ textAlign: "left",
1011
+ background: selected ? "rgba(37, 99, 235, 0.08)" : "transparent",
1012
+ cursor: "pointer",
1013
+ display: "flex",
1014
+ flexDirection: "column",
1015
+ gap: 6
1016
+ },
1017
+ children: [
1018
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [
1019
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 14, fontWeight: 600 }, children: conversation.name ?? conversation.counterparty_user_id ?? "Conversation" }),
1020
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
1021
+ conversation.is_muted ? /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: "var(--sm-muted-text, #6b7280)" }, children: "Muted" }) : null,
1022
+ conversation.unread_count ? /* @__PURE__ */ jsx(
1023
+ "span",
1024
+ {
1025
+ style: {
1026
+ minWidth: 22,
1027
+ height: 22,
1028
+ borderRadius: 999,
1029
+ background: "var(--sm-primary, #2563eb)",
1030
+ color: "#fff",
1031
+ display: "inline-flex",
1032
+ alignItems: "center",
1033
+ justifyContent: "center",
1034
+ fontSize: 12,
1035
+ fontWeight: 700,
1036
+ padding: "0 6px"
1037
+ },
1038
+ children: conversation.unread_count
1039
+ }
1040
+ ) : null
1041
+ ] })
1042
+ ] }),
1043
+ /* @__PURE__ */ jsx(
1044
+ "div",
1045
+ {
1046
+ style: {
1047
+ fontSize: 13,
1048
+ color: "var(--sm-muted-text, #6b7280)",
1049
+ overflow: "hidden",
1050
+ textOverflow: "ellipsis",
1051
+ whiteSpace: "nowrap"
1052
+ },
1053
+ children: formatPreview(conversation)
1054
+ }
1055
+ )
1056
+ ]
1057
+ },
1058
+ conversation.id
1059
+ );
1060
+ }) })
1061
+ ]
1062
+ }
1063
+ );
1064
+ }
6
1065
  var ChatContext = createContext(null);
7
1066
  function useChatContext() {
8
1067
  const ctx = useContext(ChatContext);
@@ -16,11 +1075,14 @@ function ChatProvider({ config, children }) {
16
1075
  client.destroy();
17
1076
  };
18
1077
  }, [client]);
19
- return /* @__PURE__ */ jsx(ChatContext.Provider, { value: { client }, children });
1078
+ return /* @__PURE__ */ jsx(ChatContext.Provider, { value: { client, config }, children });
20
1079
  }
21
1080
  function useChatClient() {
22
1081
  return useChatContext().client;
23
1082
  }
1083
+ function useChatConfig() {
1084
+ return useChatContext().config;
1085
+ }
24
1086
  function useConnection() {
25
1087
  const { client } = useChatContext();
26
1088
  const [status, setStatus] = useState(client.status);
@@ -40,6 +1102,7 @@ function useConnection() {
40
1102
  function useChat(conversationId) {
41
1103
  const { client } = useChatContext();
42
1104
  const [messages, setMessages] = useState([]);
1105
+ const [readStatuses, setReadStatuses] = useState([]);
43
1106
  const [isLoading, setIsLoading] = useState(false);
44
1107
  const [error, setError] = useState(null);
45
1108
  const [hasMore, setHasMore] = useState(false);
@@ -60,6 +1123,11 @@ function useChat(conversationId) {
60
1123
  } else if (result.error) {
61
1124
  setError(result.error.message);
62
1125
  }
1126
+ const readStatusResult = await client.getReadStatus(conversationId);
1127
+ if (cancelled) return;
1128
+ if (readStatusResult.data?.statuses) {
1129
+ setReadStatuses(readStatusResult.data.statuses);
1130
+ }
63
1131
  setIsLoading(false);
64
1132
  unsub = client.subscribeToConversation(conversationId);
65
1133
  client.connect();
@@ -73,10 +1141,7 @@ function useChat(conversationId) {
73
1141
  if (!conversationId) return;
74
1142
  return client.on("message", ({ message, conversationId: convId }) => {
75
1143
  if (convId === conversationId) {
76
- setMessages((prev) => {
77
- if (prev.some((m) => m.id === message.id)) return prev;
78
- return [...prev, message];
79
- });
1144
+ setMessages([...client.getCachedMessages(conversationId)]);
80
1145
  }
81
1146
  });
82
1147
  }, [client, conversationId]);
@@ -84,7 +1149,7 @@ function useChat(conversationId) {
84
1149
  if (!conversationId) return;
85
1150
  return client.on("message:updated", ({ message, conversationId: convId }) => {
86
1151
  if (convId === conversationId) {
87
- setMessages((prev) => prev.map((m) => m.id === message.id ? message : m));
1152
+ setMessages([...client.getCachedMessages(conversationId)]);
88
1153
  }
89
1154
  });
90
1155
  }, [client, conversationId]);
@@ -92,10 +1157,33 @@ function useChat(conversationId) {
92
1157
  if (!conversationId) return;
93
1158
  return client.on("message:deleted", ({ messageId, conversationId: convId }) => {
94
1159
  if (convId === conversationId) {
95
- setMessages((prev) => prev.filter((m) => m.id !== messageId));
1160
+ setMessages([...client.getCachedMessages(conversationId)]);
1161
+ }
1162
+ });
1163
+ }, [client, conversationId]);
1164
+ useEffect(() => {
1165
+ if (!conversationId) return;
1166
+ return client.on("reaction", ({ conversationId: convId }) => {
1167
+ if (convId === conversationId) {
1168
+ setMessages([...client.getCachedMessages(conversationId)]);
96
1169
  }
97
1170
  });
98
1171
  }, [client, conversationId]);
1172
+ useEffect(() => {
1173
+ if (!conversationId) return;
1174
+ return client.on("read", ({ conversationId: convId, userId, lastReadAt }) => {
1175
+ if (convId !== conversationId) return;
1176
+ setReadStatuses((prev) => {
1177
+ const existingIndex = prev.findIndex((status) => status.user_id === userId);
1178
+ if (existingIndex < 0) {
1179
+ return [...prev, { user_id: userId, last_read_at: lastReadAt }];
1180
+ }
1181
+ return prev.map(
1182
+ (status) => status.user_id === userId ? { ...status, last_read_at: lastReadAt } : status
1183
+ );
1184
+ });
1185
+ });
1186
+ }, [client, conversationId]);
99
1187
  const sendMessage = useCallback(
100
1188
  async (content, options) => {
101
1189
  if (!conversationId) return;
@@ -112,17 +1200,128 @@ function useChat(conversationId) {
112
1200
  setHasMore(result.data.has_more ?? false);
113
1201
  }
114
1202
  }, [client, conversationId, messages]);
1203
+ const editMessage = useCallback(
1204
+ async (messageId, content) => {
1205
+ const result = await client.editMessage(messageId, content);
1206
+ if (result.error) {
1207
+ setError(result.error.message);
1208
+ }
1209
+ return result;
1210
+ },
1211
+ [client]
1212
+ );
1213
+ const deleteMessage = useCallback(
1214
+ async (messageId) => {
1215
+ const result = await client.deleteMessage(messageId);
1216
+ if (result.error) {
1217
+ setError(result.error.message);
1218
+ }
1219
+ return result;
1220
+ },
1221
+ [client]
1222
+ );
1223
+ const addReaction = useCallback(
1224
+ async (messageId, emoji) => {
1225
+ const result = await client.addReaction(messageId, emoji);
1226
+ if (result.error) {
1227
+ setError(result.error.message);
1228
+ }
1229
+ return result;
1230
+ },
1231
+ [client]
1232
+ );
1233
+ const removeReaction = useCallback(
1234
+ async (messageId, emoji) => {
1235
+ const result = await client.removeReaction(messageId, emoji);
1236
+ if (result.error) {
1237
+ setError(result.error.message);
1238
+ }
1239
+ return result;
1240
+ },
1241
+ [client]
1242
+ );
1243
+ const uploadAttachment = useCallback(
1244
+ async (file, onProgress, signal) => {
1245
+ const result = await client.uploadAttachment(file, onProgress, signal);
1246
+ if (result.error) {
1247
+ setError(result.error.message);
1248
+ }
1249
+ return result;
1250
+ },
1251
+ [client]
1252
+ );
1253
+ const refreshAttachmentUrl = useCallback(
1254
+ async (messageId, fileId) => {
1255
+ const result = await client.refreshAttachmentUrl(messageId, fileId);
1256
+ if (result.error) {
1257
+ setError(result.error.message);
1258
+ }
1259
+ return result;
1260
+ },
1261
+ [client]
1262
+ );
1263
+ const reportMessage = useCallback(
1264
+ async (messageId, reason, description) => {
1265
+ const result = await client.reportMessage(messageId, reason, description);
1266
+ if (result.error) {
1267
+ setError(result.error.message);
1268
+ }
1269
+ return result;
1270
+ },
1271
+ [client]
1272
+ );
1273
+ const muteConversation = useCallback(
1274
+ async (mutedUntil) => {
1275
+ if (!conversationId) return;
1276
+ const result = await client.muteConversation(conversationId, mutedUntil);
1277
+ if (result.error) {
1278
+ setError(result.error.message);
1279
+ }
1280
+ return result;
1281
+ },
1282
+ [client, conversationId]
1283
+ );
1284
+ const unmuteConversation = useCallback(async () => {
1285
+ if (!conversationId) return;
1286
+ const result = await client.unmuteConversation(conversationId);
1287
+ if (result.error) {
1288
+ setError(result.error.message);
1289
+ }
1290
+ return result;
1291
+ }, [client, conversationId]);
1292
+ const getReadStatus = useCallback(async () => {
1293
+ if (!conversationId) return;
1294
+ const result = await client.getReadStatus(conversationId);
1295
+ if (result.data?.statuses) {
1296
+ setReadStatuses(result.data.statuses);
1297
+ } else if (result.error) {
1298
+ setError(result.error.message);
1299
+ }
1300
+ return result;
1301
+ }, [client, conversationId]);
115
1302
  const markRead = useCallback(async () => {
116
1303
  if (!conversationId) return;
117
1304
  await client.markRead(conversationId);
118
- }, [client, conversationId]);
1305
+ await getReadStatus();
1306
+ }, [client, conversationId, getReadStatus]);
119
1307
  return {
120
1308
  messages,
1309
+ readStatuses,
121
1310
  isLoading,
122
1311
  error,
123
1312
  hasMore,
124
1313
  sendMessage,
125
1314
  loadMore,
1315
+ editMessage,
1316
+ deleteMessage,
1317
+ addReaction,
1318
+ removeReaction,
1319
+ uploadAttachment,
1320
+ refreshAttachmentUrl,
1321
+ reportMessage,
1322
+ muteConversation,
1323
+ unmuteConversation,
1324
+ getReadStatus,
126
1325
  markRead
127
1326
  };
128
1327
  }
@@ -298,4 +1497,4 @@ function useUnreadCount() {
298
1497
  return { totalUnread };
299
1498
  }
300
1499
 
301
- export { ChatProvider, useChat, useChatClient, useConnection, useConversations, usePresence, useTyping, useUnreadCount };
1500
+ export { ChatInput, ChatMessageItem, ChatMessageList, ChatProvider, ChatThread, ConversationList, EmojiPicker, useChat, useChatClient, useChatConfig, useConnection, useConversations, usePresence, useTyping, useUnreadCount };