@scalemule/chat 0.0.7 → 0.0.9

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,556 +1,1523 @@
1
1
  import { ChatClient } from './chunk-5O5YLRJL.js';
2
2
  export { ChatClient } from './chunk-5O5YLRJL.js';
3
- import React3, { createContext, useState, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
3
+ import React4, { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { createPortal } from 'react-dom';
5
6
 
6
7
  function ChatInput({
7
8
  onSend,
8
9
  onTypingChange,
9
10
  onUploadAttachment,
10
- placeholder = "Type a message..."
11
+ onDeleteAttachment,
12
+ onValidateFile,
13
+ placeholder = "Type a message...",
14
+ disabled = false,
15
+ maxAttachments = 5,
16
+ accept = "image/*,video/*"
11
17
  }) {
12
- const [content, setContent] = useState("");
18
+ const [text, setText] = useState("");
13
19
  const [attachments, setAttachments] = useState([]);
20
+ const [isDragOver, setIsDragOver] = useState(false);
14
21
  const [isSending, setIsSending] = useState(false);
15
- const [isDragging, setIsDragging] = useState(false);
22
+ const textareaRef = useRef(null);
16
23
  const fileInputRef = useRef(null);
17
- const typingTimeoutRef = useRef(null);
18
- const readyAttachments = useMemo(
19
- () => attachments.filter((attachment) => attachment.attachment).map((attachment) => attachment.attachment),
20
- [attachments]
24
+ const typingTimerRef = useRef(null);
25
+ const dragCounterRef = useRef(0);
26
+ const hasReadyAttachments = attachments.some((a) => a.status === "ready");
27
+ const hasUploadingAttachments = attachments.some(
28
+ (a) => a.status === "uploading"
21
29
  );
22
- const uploadingCount = attachments.filter((attachment) => !attachment.attachment && !attachment.error).length;
23
- const emitTyping = () => {
24
- onTypingChange?.(true);
25
- if (typingTimeoutRef.current) {
26
- clearTimeout(typingTimeoutRef.current);
27
- }
28
- typingTimeoutRef.current = setTimeout(() => {
29
- onTypingChange?.(false);
30
- }, 2500);
31
- };
32
- const handleFiles = async (fileList) => {
33
- if (!onUploadAttachment) return;
34
- const files = Array.from(fileList);
35
- for (const file of files) {
36
- const id = `${file.name}:${file.size}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
37
- setAttachments((current) => [
38
- ...current,
39
- {
40
- id,
41
- fileName: file.name,
42
- progress: 0
30
+ const canSend = (text.trim() || hasReadyAttachments) && !hasUploadingAttachments && !isSending;
31
+ useEffect(() => {
32
+ return () => {
33
+ if (typingTimerRef.current) clearTimeout(typingTimerRef.current);
34
+ for (const att of attachments) {
35
+ att.abortController?.abort();
36
+ URL.revokeObjectURL(att.preview);
37
+ if (att.status === "ready" && att.attachment?.file_id) {
38
+ void onDeleteAttachment?.(att.attachment.file_id);
43
39
  }
44
- ]);
45
- const result = await onUploadAttachment(file, (progress) => {
46
- setAttachments(
47
- (current) => current.map(
48
- (attachment) => attachment.id === id ? { ...attachment, progress } : attachment
49
- )
50
- );
51
- });
52
- setAttachments(
53
- (current) => current.map((attachment) => {
54
- if (attachment.id !== id) return attachment;
40
+ }
41
+ };
42
+ }, []);
43
+ const handleTyping = useCallback(() => {
44
+ if (!onTypingChange) return;
45
+ onTypingChange(true);
46
+ if (typingTimerRef.current) clearTimeout(typingTimerRef.current);
47
+ typingTimerRef.current = setTimeout(() => onTypingChange(false), 2e3);
48
+ }, [onTypingChange]);
49
+ const addFiles = useCallback(
50
+ (files) => {
51
+ if (!onUploadAttachment) return;
52
+ const remaining = maxAttachments - attachments.length;
53
+ const toAdd = files.slice(0, remaining);
54
+ for (const file of toAdd) {
55
+ if (onValidateFile) {
56
+ const validation = onValidateFile(file);
57
+ if (!validation.valid) {
58
+ console.warn("File rejected:", validation.error);
59
+ continue;
60
+ }
61
+ }
62
+ const id = crypto.randomUUID();
63
+ const preview = URL.createObjectURL(file);
64
+ const abortController = new AbortController();
65
+ const pending = {
66
+ id,
67
+ file,
68
+ preview,
69
+ status: "uploading",
70
+ progress: 0,
71
+ abortController
72
+ };
73
+ setAttachments((prev) => [...prev, pending]);
74
+ onUploadAttachment(
75
+ file,
76
+ (progress) => {
77
+ setAttachments(
78
+ (prev) => prev.map(
79
+ (a) => a.id === id ? { ...a, progress } : a
80
+ )
81
+ );
82
+ },
83
+ abortController.signal
84
+ ).then((result) => {
55
85
  if (result?.data) {
56
- return {
57
- ...attachment,
58
- progress: 100,
59
- attachment: result.data
60
- };
86
+ setAttachments(
87
+ (prev) => prev.map(
88
+ (a) => a.id === id ? {
89
+ ...a,
90
+ status: "ready",
91
+ progress: 100,
92
+ attachment: result.data
93
+ } : a
94
+ )
95
+ );
96
+ } else {
97
+ setAttachments(
98
+ (prev) => prev.map(
99
+ (a) => a.id === id ? {
100
+ ...a,
101
+ status: "error",
102
+ error: result?.error?.message ?? "Upload failed"
103
+ } : a
104
+ )
105
+ );
61
106
  }
62
- return {
63
- ...attachment,
64
- error: result?.error?.message ?? "Upload failed"
65
- };
66
- })
67
- );
68
- }
69
- };
70
- const handleSubmit = async () => {
71
- const trimmed = content.trim();
72
- if (!trimmed && !readyAttachments.length || isSending || uploadingCount > 0) return;
107
+ }).catch((err) => {
108
+ if (err.name === "AbortError") return;
109
+ setAttachments(
110
+ (prev) => prev.map(
111
+ (a) => a.id === id ? { ...a, status: "error", error: err.message } : a
112
+ )
113
+ );
114
+ });
115
+ }
116
+ },
117
+ [attachments.length, maxAttachments, onUploadAttachment, onValidateFile]
118
+ );
119
+ const removeAttachment = useCallback(
120
+ (id) => {
121
+ setAttachments((prev) => {
122
+ const att = prev.find((a) => a.id === id);
123
+ if (att) {
124
+ att.abortController?.abort();
125
+ URL.revokeObjectURL(att.preview);
126
+ if (att.status === "ready" && att.attachment?.file_id) {
127
+ void onDeleteAttachment?.(att.attachment.file_id);
128
+ }
129
+ }
130
+ return prev.filter((a) => a.id !== id);
131
+ });
132
+ },
133
+ [onDeleteAttachment]
134
+ );
135
+ const handleSend = useCallback(async () => {
136
+ if (!canSend) return;
137
+ const readyAttachments = attachments.filter((a) => a.status === "ready" && a.attachment).map((a) => a.attachment);
73
138
  setIsSending(true);
74
139
  try {
75
- await onSend(trimmed, readyAttachments);
76
- setContent("");
140
+ await onSend(
141
+ text.trim(),
142
+ readyAttachments.length > 0 ? readyAttachments : void 0
143
+ );
144
+ for (const att of attachments) {
145
+ URL.revokeObjectURL(att.preview);
146
+ }
147
+ setText("");
77
148
  setAttachments([]);
78
- onTypingChange?.(false);
149
+ if (onTypingChange) onTypingChange(false);
150
+ if (typingTimerRef.current) clearTimeout(typingTimerRef.current);
151
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
152
+ textareaRef.current?.focus();
79
153
  } finally {
80
154
  setIsSending(false);
81
155
  }
82
- };
156
+ }, [text, attachments, canSend, onSend, onTypingChange]);
157
+ const handleKeyDown = useCallback(
158
+ (e) => {
159
+ if (e.key === "Enter" && !e.shiftKey) {
160
+ e.preventDefault();
161
+ void handleSend();
162
+ }
163
+ },
164
+ [handleSend]
165
+ );
166
+ const handleChange = useCallback(
167
+ (e) => {
168
+ setText(e.target.value);
169
+ handleTyping();
170
+ const ta = e.target;
171
+ ta.style.height = "auto";
172
+ ta.style.height = Math.min(ta.scrollHeight, 120) + "px";
173
+ },
174
+ [handleTyping]
175
+ );
176
+ const handleDragEnter = useCallback(
177
+ (e) => {
178
+ e.preventDefault();
179
+ dragCounterRef.current++;
180
+ if (e.dataTransfer.types.includes("Files") && onUploadAttachment) {
181
+ setIsDragOver(true);
182
+ }
183
+ },
184
+ [onUploadAttachment]
185
+ );
186
+ const handleDragLeave = useCallback((e) => {
187
+ e.preventDefault();
188
+ dragCounterRef.current--;
189
+ if (dragCounterRef.current === 0) {
190
+ setIsDragOver(false);
191
+ }
192
+ }, []);
193
+ const handleDragOver = useCallback((e) => {
194
+ e.preventDefault();
195
+ }, []);
196
+ const handleDrop = useCallback(
197
+ (e) => {
198
+ e.preventDefault();
199
+ setIsDragOver(false);
200
+ dragCounterRef.current = 0;
201
+ const files = Array.from(e.dataTransfer.files);
202
+ if (files.length > 0) addFiles(files);
203
+ },
204
+ [addFiles]
205
+ );
206
+ const handleFileSelect = useCallback(
207
+ (e) => {
208
+ const files = Array.from(e.target.files ?? []);
209
+ if (files.length > 0) addFiles(files);
210
+ e.target.value = "";
211
+ },
212
+ [addFiles]
213
+ );
83
214
  return /* @__PURE__ */ jsxs(
84
215
  "div",
85
216
  {
86
- onDragOver: (event) => {
87
- event.preventDefault();
88
- if (onUploadAttachment) {
89
- setIsDragging(true);
90
- }
91
- },
92
- onDragLeave: () => setIsDragging(false),
93
- onDrop: (event) => {
94
- event.preventDefault();
95
- setIsDragging(false);
96
- void handleFiles(event.dataTransfer.files);
97
- },
98
217
  style: {
218
+ position: "relative",
99
219
  borderTop: "1px solid var(--sm-border-color, #e5e7eb)",
100
- background: isDragging ? "rgba(37, 99, 235, 0.06)" : "var(--sm-surface, #fff)",
101
- padding: 12,
102
- display: "flex",
103
- flexDirection: "column",
104
- gap: 10
220
+ background: "var(--sm-surface, #fff)"
105
221
  },
222
+ onDragEnter: handleDragEnter,
223
+ onDragLeave: handleDragLeave,
224
+ onDragOver: handleDragOver,
225
+ onDrop: handleDrop,
106
226
  children: [
107
- attachments.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: attachments.map((attachment) => /* @__PURE__ */ jsxs(
227
+ isDragOver && /* @__PURE__ */ jsx(
108
228
  "div",
109
229
  {
110
230
  style: {
111
- display: "inline-flex",
231
+ position: "absolute",
232
+ inset: 0,
233
+ zIndex: 50,
234
+ background: "rgba(37, 99, 235, 0.1)",
235
+ backdropFilter: "blur(4px)",
236
+ borderRadius: "0 0 var(--sm-border-radius, 16px) var(--sm-border-radius, 16px)",
237
+ display: "flex",
112
238
  alignItems: "center",
239
+ justifyContent: "center"
240
+ },
241
+ children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
242
+ /* @__PURE__ */ jsxs(
243
+ "svg",
244
+ {
245
+ xmlns: "http://www.w3.org/2000/svg",
246
+ width: "32",
247
+ height: "32",
248
+ viewBox: "0 0 24 24",
249
+ fill: "none",
250
+ stroke: "var(--sm-primary, #2563eb)",
251
+ strokeWidth: "2",
252
+ style: { margin: "0 auto 8px" },
253
+ children: [
254
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
255
+ /* @__PURE__ */ jsx("polyline", { points: "17 8 12 3 7 8" }),
256
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
257
+ ]
258
+ }
259
+ ),
260
+ /* @__PURE__ */ jsx(
261
+ "p",
262
+ {
263
+ style: {
264
+ margin: 0,
265
+ color: "var(--sm-primary, #2563eb)",
266
+ fontSize: 14,
267
+ fontWeight: 500
268
+ },
269
+ children: "Drop to attach"
270
+ }
271
+ )
272
+ ] })
273
+ }
274
+ ),
275
+ attachments.length > 0 && /* @__PURE__ */ jsx(
276
+ "div",
277
+ {
278
+ style: {
279
+ display: "flex",
113
280
  gap: 8,
114
- padding: "6px 10px",
115
- borderRadius: 999,
116
- background: "var(--sm-surface-muted, #f8fafc)",
117
- border: "1px solid var(--sm-border-color, #e5e7eb)",
118
- fontSize: 12
281
+ padding: "8px 12px 4px",
282
+ overflowX: "auto"
283
+ },
284
+ children: attachments.map((att) => /* @__PURE__ */ jsxs(
285
+ "div",
286
+ {
287
+ style: {
288
+ position: "relative",
289
+ display: "flex",
290
+ alignItems: "center",
291
+ gap: 6,
292
+ background: att.status === "error" ? "#fef2f2" : "var(--sm-surface-muted, #f8fafc)",
293
+ border: att.status === "error" ? "1px solid #fecaca" : "1px solid var(--sm-border-color, #e5e7eb)",
294
+ borderRadius: 8,
295
+ padding: "6px 8px",
296
+ fontSize: 12,
297
+ flexShrink: 0,
298
+ maxWidth: 180
299
+ },
300
+ children: [
301
+ att.file.type.startsWith("image/") ? /* @__PURE__ */ jsx(
302
+ "img",
303
+ {
304
+ src: att.preview,
305
+ alt: "",
306
+ style: {
307
+ width: 32,
308
+ height: 32,
309
+ borderRadius: 4,
310
+ objectFit: "cover",
311
+ flexShrink: 0
312
+ }
313
+ }
314
+ ) : /* @__PURE__ */ jsx(
315
+ "div",
316
+ {
317
+ style: {
318
+ width: 32,
319
+ height: 32,
320
+ borderRadius: 4,
321
+ background: "var(--sm-border-color, #e5e7eb)",
322
+ display: "flex",
323
+ alignItems: "center",
324
+ justifyContent: "center",
325
+ flexShrink: 0
326
+ },
327
+ children: /* @__PURE__ */ jsxs(
328
+ "svg",
329
+ {
330
+ xmlns: "http://www.w3.org/2000/svg",
331
+ width: "14",
332
+ height: "14",
333
+ viewBox: "0 0 24 24",
334
+ fill: "none",
335
+ stroke: "currentColor",
336
+ strokeWidth: "2",
337
+ style: { color: "var(--sm-muted-text, #6b7280)" },
338
+ children: [
339
+ /* @__PURE__ */ jsx("polygon", { points: "23 7 16 12 23 17 23 7" }),
340
+ /* @__PURE__ */ jsx("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
341
+ ]
342
+ }
343
+ )
344
+ }
345
+ ),
346
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
347
+ /* @__PURE__ */ jsx(
348
+ "p",
349
+ {
350
+ style: {
351
+ margin: 0,
352
+ overflow: "hidden",
353
+ textOverflow: "ellipsis",
354
+ whiteSpace: "nowrap",
355
+ color: "var(--sm-text-color, #111827)"
356
+ },
357
+ children: att.file.name
358
+ }
359
+ ),
360
+ att.status === "uploading" && /* @__PURE__ */ jsx(
361
+ "div",
362
+ {
363
+ style: {
364
+ width: "100%",
365
+ height: 3,
366
+ background: "var(--sm-border-color, #e5e7eb)",
367
+ borderRadius: 999,
368
+ marginTop: 2,
369
+ overflow: "hidden"
370
+ },
371
+ children: /* @__PURE__ */ jsx(
372
+ "div",
373
+ {
374
+ style: {
375
+ height: "100%",
376
+ background: "var(--sm-primary, #2563eb)",
377
+ borderRadius: 999,
378
+ transition: "width 0.2s ease",
379
+ width: `${att.progress}%`
380
+ }
381
+ }
382
+ )
383
+ }
384
+ ),
385
+ att.status === "error" && /* @__PURE__ */ jsx(
386
+ "p",
387
+ {
388
+ style: {
389
+ margin: 0,
390
+ color: "#dc2626",
391
+ overflow: "hidden",
392
+ textOverflow: "ellipsis",
393
+ whiteSpace: "nowrap"
394
+ },
395
+ children: att.error
396
+ }
397
+ )
398
+ ] }),
399
+ /* @__PURE__ */ jsx(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ onClick: () => removeAttachment(att.id),
404
+ style: {
405
+ flexShrink: 0,
406
+ width: 16,
407
+ height: 16,
408
+ display: "flex",
409
+ alignItems: "center",
410
+ justifyContent: "center",
411
+ borderRadius: 999,
412
+ background: "var(--sm-muted-text, #9ca3af)",
413
+ color: "#fff",
414
+ border: "none",
415
+ cursor: "pointer",
416
+ fontSize: 10,
417
+ lineHeight: 1,
418
+ padding: 0
419
+ },
420
+ children: "\xD7"
421
+ }
422
+ )
423
+ ]
424
+ },
425
+ att.id
426
+ ))
427
+ }
428
+ ),
429
+ /* @__PURE__ */ jsxs(
430
+ "div",
431
+ {
432
+ style: {
433
+ display: "flex",
434
+ alignItems: "flex-end",
435
+ gap: 8,
436
+ padding: "8px 12px"
119
437
  },
120
438
  children: [
121
- /* @__PURE__ */ jsx("span", { children: attachment.fileName }),
122
- /* @__PURE__ */ jsx("span", { style: { color: attachment.error ? "#dc2626" : "var(--sm-muted-text, #6b7280)" }, children: attachment.error ?? `${attachment.progress}%` }),
439
+ onUploadAttachment && /* @__PURE__ */ jsxs(Fragment, { children: [
440
+ /* @__PURE__ */ jsx(
441
+ "button",
442
+ {
443
+ type: "button",
444
+ onClick: () => fileInputRef.current?.click(),
445
+ disabled: disabled || attachments.length >= maxAttachments,
446
+ "aria-label": "Attach file",
447
+ style: {
448
+ flexShrink: 0,
449
+ width: 32,
450
+ height: 32,
451
+ display: "flex",
452
+ alignItems: "center",
453
+ justifyContent: "center",
454
+ borderRadius: 999,
455
+ border: "none",
456
+ background: "transparent",
457
+ cursor: disabled || attachments.length >= maxAttachments ? "not-allowed" : "pointer",
458
+ color: "var(--sm-muted-text, #6b7280)",
459
+ opacity: disabled || attachments.length >= maxAttachments ? 0.4 : 1,
460
+ marginBottom: 2
461
+ },
462
+ children: /* @__PURE__ */ jsx(
463
+ "svg",
464
+ {
465
+ xmlns: "http://www.w3.org/2000/svg",
466
+ width: "18",
467
+ height: "18",
468
+ viewBox: "0 0 24 24",
469
+ fill: "none",
470
+ stroke: "currentColor",
471
+ strokeWidth: "2",
472
+ strokeLinecap: "round",
473
+ strokeLinejoin: "round",
474
+ children: /* @__PURE__ */ jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
475
+ }
476
+ )
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx(
480
+ "input",
481
+ {
482
+ ref: fileInputRef,
483
+ type: "file",
484
+ accept,
485
+ multiple: true,
486
+ style: { display: "none" },
487
+ onChange: handleFileSelect
488
+ }
489
+ )
490
+ ] }),
491
+ /* @__PURE__ */ jsx(
492
+ "textarea",
493
+ {
494
+ ref: textareaRef,
495
+ value: text,
496
+ onChange: handleChange,
497
+ onKeyDown: handleKeyDown,
498
+ placeholder,
499
+ disabled,
500
+ rows: 1,
501
+ style: {
502
+ flex: 1,
503
+ fontSize: 14,
504
+ background: "var(--sm-surface-muted, #f8fafc)",
505
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
506
+ borderRadius: 16,
507
+ padding: "8px 16px",
508
+ fontFamily: "var(--sm-font-family, system-ui, -apple-system, sans-serif)",
509
+ color: "var(--sm-text-color, #111827)",
510
+ resize: "none",
511
+ overflow: "hidden",
512
+ outline: "none",
513
+ minHeight: 36,
514
+ lineHeight: "20px",
515
+ opacity: disabled ? 0.5 : 1,
516
+ cursor: disabled ? "not-allowed" : "text",
517
+ boxSizing: "border-box"
518
+ }
519
+ }
520
+ ),
123
521
  /* @__PURE__ */ jsx(
124
522
  "button",
125
523
  {
524
+ onClick: () => void handleSend(),
525
+ disabled: disabled || !canSend,
126
526
  type: "button",
127
- onClick: () => {
128
- setAttachments((current) => current.filter((item) => item.id !== attachment.id));
129
- },
527
+ "aria-label": "Send message",
130
528
  style: {
529
+ flexShrink: 0,
530
+ width: 32,
531
+ height: 32,
532
+ display: "flex",
533
+ alignItems: "center",
534
+ justifyContent: "center",
535
+ borderRadius: 999,
131
536
  border: "none",
132
- background: "transparent",
133
- cursor: "pointer",
134
- color: "var(--sm-muted-text, #6b7280)"
537
+ background: "var(--sm-primary, #2563eb)",
538
+ color: "#fff",
539
+ cursor: disabled || !canSend ? "not-allowed" : "pointer",
540
+ opacity: disabled || !canSend ? 0.4 : 1,
541
+ marginBottom: 2,
542
+ transition: "opacity 0.15s ease"
135
543
  },
136
- children: "x"
544
+ children: /* @__PURE__ */ jsxs(
545
+ "svg",
546
+ {
547
+ xmlns: "http://www.w3.org/2000/svg",
548
+ width: "16",
549
+ height: "16",
550
+ viewBox: "0 0 24 24",
551
+ fill: "none",
552
+ stroke: "currentColor",
553
+ strokeWidth: "2",
554
+ strokeLinecap: "round",
555
+ strokeLinejoin: "round",
556
+ children: [
557
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
558
+ /* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
559
+ ]
560
+ }
561
+ )
137
562
  }
138
563
  )
139
564
  ]
140
- },
141
- attachment.id
142
- )) }) : null,
143
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, alignItems: "flex-end" }, children: [
144
- /* @__PURE__ */ jsx(
145
- "textarea",
146
- {
147
- value: content,
148
- onChange: (event) => {
149
- setContent(event.target.value);
150
- emitTyping();
151
- },
152
- onKeyDown: (event) => {
153
- if (event.key === "Enter" && !event.shiftKey) {
154
- event.preventDefault();
155
- void handleSubmit();
156
- }
157
- },
158
- rows: 1,
159
- placeholder,
160
- style: {
161
- flex: 1,
162
- minHeight: 44,
163
- maxHeight: 120,
164
- resize: "vertical",
165
- borderRadius: 14,
166
- border: "1px solid var(--sm-border-color, #e5e7eb)",
167
- padding: "12px 14px",
168
- font: "inherit",
169
- color: "var(--sm-text-color, #111827)"
170
- }
171
- }
172
- ),
173
- onUploadAttachment ? /* @__PURE__ */ jsxs(Fragment, { children: [
174
- /* @__PURE__ */ jsx(
175
- "input",
176
- {
177
- ref: fileInputRef,
178
- type: "file",
179
- hidden: true,
180
- multiple: true,
181
- accept: "image/*,video/*,audio/*",
182
- onChange: (event) => {
183
- if (event.target.files) {
184
- void handleFiles(event.target.files);
185
- event.target.value = "";
186
- }
187
- }
188
- }
189
- ),
190
- /* @__PURE__ */ jsx(
191
- "button",
192
- {
193
- type: "button",
194
- onClick: () => fileInputRef.current?.click(),
195
- "aria-label": "Attach files",
196
- style: {
197
- width: 44,
198
- height: 44,
199
- borderRadius: 14,
200
- border: "1px solid var(--sm-border-color, #e5e7eb)",
201
- background: "var(--sm-surface, #fff)",
202
- cursor: "pointer",
203
- color: "var(--sm-text-color, #111827)"
204
- },
205
- children: "+"
206
- }
207
- )
208
- ] }) : null,
209
- /* @__PURE__ */ jsx(
210
- "button",
211
- {
212
- type: "button",
213
- onClick: () => void handleSubmit(),
214
- disabled: isSending || uploadingCount > 0 || !content.trim() && !readyAttachments.length,
215
- style: {
216
- height: 44,
217
- padding: "0 16px",
218
- borderRadius: 14,
219
- border: "none",
220
- background: "var(--sm-primary, #2563eb)",
221
- color: "#fff",
222
- cursor: isSending ? "wait" : "pointer",
223
- opacity: isSending || uploadingCount > 0 ? 0.75 : 1
224
- },
225
- children: "Send"
226
- }
227
- )
228
- ] })
565
+ }
566
+ )
229
567
  ]
230
568
  }
231
569
  );
232
570
  }
233
- var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F62E}", "\u{1F440}"];
571
+ var QUICK_REACTIONS = ["\u2764\uFE0F", "\u{1F602}", "\u{1F44D}", "\u{1F525}", "\u{1F62E}", "\u{1F622}", "\u{1F44F}", "\u{1F64C}"];
234
572
  function EmojiPicker({
235
573
  onSelect,
236
- emojis = DEFAULT_EMOJIS
574
+ onClose,
575
+ anchorRef,
576
+ emojis = QUICK_REACTIONS
237
577
  }) {
238
- return /* @__PURE__ */ jsx(
239
- "div",
240
- {
241
- style: {
242
- display: "inline-flex",
243
- gap: 6,
244
- padding: 6,
245
- borderRadius: 999,
246
- background: "var(--sm-surface, #fff)",
247
- border: "1px solid var(--sm-border-color, #e5e7eb)",
248
- boxShadow: "0 10px 25px rgba(15, 23, 42, 0.12)"
249
- },
250
- children: emojis.map((emoji) => /* @__PURE__ */ jsx(
251
- "button",
252
- {
253
- type: "button",
254
- onClick: () => onSelect(emoji),
255
- "aria-label": `React with ${emoji}`,
256
- style: {
257
- width: 32,
258
- height: 32,
259
- border: "none",
260
- background: "transparent",
261
- borderRadius: 999,
262
- cursor: "pointer",
263
- fontSize: 18
264
- },
265
- children: emoji
266
- },
267
- emoji
268
- ))
578
+ const ref = useRef(null);
579
+ const [position, setPosition] = useState(null);
580
+ useEffect(() => {
581
+ const anchor = anchorRef.current;
582
+ if (!anchor) return;
583
+ const rect = anchor.getBoundingClientRect();
584
+ const pickerWidth = emojis.length * 36 + 16;
585
+ let left = rect.left + rect.width / 2 - pickerWidth / 2;
586
+ if (left < 8) left = 8;
587
+ if (left + pickerWidth > window.innerWidth - 8)
588
+ left = window.innerWidth - 8 - pickerWidth;
589
+ setPosition({ top: rect.top - 8, left });
590
+ }, [anchorRef, emojis.length]);
591
+ useEffect(() => {
592
+ function handleClickOutside(e) {
593
+ if (ref.current && !ref.current.contains(e.target) && anchorRef.current && !anchorRef.current.contains(e.target)) {
594
+ onClose();
595
+ }
269
596
  }
597
+ document.addEventListener("mousedown", handleClickOutside);
598
+ return () => document.removeEventListener("mousedown", handleClickOutside);
599
+ }, [onClose, anchorRef]);
600
+ if (!position) return null;
601
+ return createPortal(
602
+ /* @__PURE__ */ jsx(
603
+ "div",
604
+ {
605
+ ref,
606
+ style: {
607
+ position: "fixed",
608
+ top: position.top,
609
+ left: position.left,
610
+ transform: "translateY(-100%)",
611
+ background: "var(--sm-surface, #fff)",
612
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
613
+ borderRadius: 12,
614
+ boxShadow: "0 10px 25px rgba(15, 23, 42, 0.12)",
615
+ padding: 8,
616
+ display: "flex",
617
+ gap: 4,
618
+ zIndex: 9999
619
+ },
620
+ children: emojis.map((emoji) => /* @__PURE__ */ jsx(
621
+ "button",
622
+ {
623
+ onClick: () => {
624
+ onSelect(emoji);
625
+ onClose();
626
+ },
627
+ type: "button",
628
+ "aria-label": `React with ${emoji}`,
629
+ style: {
630
+ width: 32,
631
+ height: 32,
632
+ display: "flex",
633
+ alignItems: "center",
634
+ justifyContent: "center",
635
+ borderRadius: 8,
636
+ border: "none",
637
+ background: "transparent",
638
+ cursor: "pointer",
639
+ fontSize: 18,
640
+ transition: "background 0.15s ease"
641
+ },
642
+ onMouseEnter: (e) => {
643
+ e.target.style.background = "var(--sm-surface-muted, #f8fafc)";
644
+ },
645
+ onMouseLeave: (e) => {
646
+ e.target.style.background = "transparent";
647
+ },
648
+ children: emoji
649
+ },
650
+ emoji
651
+ ))
652
+ }
653
+ ),
654
+ document.body
270
655
  );
271
656
  }
657
+ function EmojiPickerTrigger({
658
+ onSelect,
659
+ emojis
660
+ }) {
661
+ const [open, setOpen] = useState(false);
662
+ const buttonRef = useRef(null);
663
+ const handleClose = useCallback(() => setOpen(false), []);
664
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
665
+ /* @__PURE__ */ jsx(
666
+ "button",
667
+ {
668
+ ref: buttonRef,
669
+ type: "button",
670
+ onClick: () => setOpen(!open),
671
+ "aria-label": "Add reaction",
672
+ style: {
673
+ padding: 6,
674
+ border: "none",
675
+ background: "transparent",
676
+ cursor: "pointer",
677
+ color: "var(--sm-muted-text, #6b7280)",
678
+ borderRadius: 8,
679
+ display: "flex",
680
+ alignItems: "center",
681
+ justifyContent: "center",
682
+ transition: "color 0.15s ease"
683
+ },
684
+ onMouseEnter: (e) => {
685
+ e.target.style.color = "var(--sm-text-color, #111827)";
686
+ },
687
+ onMouseLeave: (e) => {
688
+ e.target.style.color = "var(--sm-muted-text, #6b7280)";
689
+ },
690
+ children: /* @__PURE__ */ jsxs(
691
+ "svg",
692
+ {
693
+ xmlns: "http://www.w3.org/2000/svg",
694
+ width: "18",
695
+ height: "18",
696
+ viewBox: "0 0 24 24",
697
+ fill: "none",
698
+ stroke: "currentColor",
699
+ strokeWidth: "2",
700
+ strokeLinecap: "round",
701
+ strokeLinejoin: "round",
702
+ children: [
703
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
704
+ /* @__PURE__ */ jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
705
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "9", x2: "9.01", y2: "9" }),
706
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "9", x2: "15.01", y2: "9" })
707
+ ]
708
+ }
709
+ )
710
+ }
711
+ ),
712
+ open && /* @__PURE__ */ jsx(
713
+ EmojiPicker,
714
+ {
715
+ onSelect,
716
+ onClose: handleClose,
717
+ anchorRef: buttonRef,
718
+ emojis
719
+ }
720
+ )
721
+ ] });
722
+ }
723
+ function ReactionBar({
724
+ reactions,
725
+ currentUserId,
726
+ onToggleReaction
727
+ }) {
728
+ if (!reactions || reactions.length === 0) return null;
729
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 4 }, children: reactions.map((r) => {
730
+ const hasReacted = currentUserId ? r.user_ids.includes(currentUserId) : false;
731
+ return /* @__PURE__ */ jsxs(
732
+ "button",
733
+ {
734
+ onClick: () => onToggleReaction(r.emoji),
735
+ type: "button",
736
+ style: {
737
+ display: "inline-flex",
738
+ alignItems: "center",
739
+ gap: 4,
740
+ padding: "2px 8px",
741
+ borderRadius: 999,
742
+ fontSize: 12,
743
+ border: hasReacted ? "1px solid var(--sm-reaction-active-border, rgba(37, 99, 235, 0.4))" : "1px solid var(--sm-border-color, #e5e7eb)",
744
+ background: hasReacted ? "var(--sm-reaction-active-bg, rgba(37, 99, 235, 0.08))" : "var(--sm-surface-muted, #f8fafc)",
745
+ color: hasReacted ? "var(--sm-primary, #2563eb)" : "var(--sm-text-color, #111827)",
746
+ cursor: "pointer",
747
+ lineHeight: "18px"
748
+ },
749
+ children: [
750
+ /* @__PURE__ */ jsx("span", { children: r.emoji }),
751
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: r.count })
752
+ ]
753
+ },
754
+ r.emoji
755
+ );
756
+ }) });
757
+ }
272
758
 
273
759
  // src/react-components/utils.ts
274
760
  var timeFormatter = new Intl.DateTimeFormat(void 0, {
275
761
  hour: "numeric",
276
762
  minute: "2-digit"
277
763
  });
278
- var dateFormatter = new Intl.DateTimeFormat(void 0, {
279
- month: "short",
280
- day: "numeric",
281
- year: "numeric"
282
- });
283
764
  function formatMessageTime(value) {
284
765
  if (!value) return "";
285
766
  return timeFormatter.format(new Date(value));
286
767
  }
287
- function formatDayLabel(value) {
288
- if (!value) return "";
289
- return dateFormatter.format(new Date(value));
290
- }
291
- function isSameDay(left, right) {
292
- if (!left || !right) return false;
293
- const leftDate = new Date(left);
294
- const rightDate = new Date(right);
295
- return leftDate.getFullYear() === rightDate.getFullYear() && leftDate.getMonth() === rightDate.getMonth() && leftDate.getDate() === rightDate.getDate();
768
+ function useAttachmentUrl(fileId, hasUrl, fetcher) {
769
+ const [url, setUrl] = useState(null);
770
+ useEffect(() => {
771
+ if (hasUrl || !fileId || !fetcher) return;
772
+ fetcher(fileId).then((viewUrl) => {
773
+ if (viewUrl) setUrl(viewUrl);
774
+ }).catch(() => {
775
+ });
776
+ }, [fileId, hasUrl, fetcher]);
777
+ return url;
296
778
  }
297
- function renderAttachment(messageId, attachment) {
298
- const key = `${messageId}:${attachment.file_id}`;
299
- const url = attachment.presigned_url;
300
- if (!url) {
301
- return /* @__PURE__ */ jsx(
302
- "div",
303
- {
304
- style: {
305
- padding: "10px 12px",
306
- borderRadius: 12,
307
- background: "rgba(255,255,255,0.16)",
308
- fontSize: 13
309
- },
310
- children: attachment.file_name
311
- },
312
- key
313
- );
314
- }
315
- if (attachment.mime_type.startsWith("image/")) {
316
- return /* @__PURE__ */ jsx(
317
- "img",
318
- {
319
- src: url,
320
- alt: attachment.file_name,
321
- loading: "lazy",
322
- style: {
323
- display: "block",
324
- maxWidth: "100%",
325
- borderRadius: 12,
326
- marginTop: 8
779
+ function AttachmentRenderer({
780
+ att,
781
+ fetcher
782
+ }) {
783
+ const [expanded, setExpanded] = useState(false);
784
+ const fetchedUrl = useAttachmentUrl(att.file_id, !!att.presigned_url, fetcher);
785
+ const viewUrl = att.presigned_url || fetchedUrl;
786
+ const isImage = att.mime_type?.startsWith("image/");
787
+ const isVideo = att.mime_type?.startsWith("video/");
788
+ const isAudio = att.mime_type?.startsWith("audio/");
789
+ if (isImage && viewUrl) {
790
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
791
+ /* @__PURE__ */ jsx(
792
+ "img",
793
+ {
794
+ src: viewUrl,
795
+ alt: att.file_name,
796
+ loading: "lazy",
797
+ onClick: () => setExpanded(true),
798
+ style: {
799
+ display: "block",
800
+ maxHeight: 240,
801
+ maxWidth: "100%",
802
+ objectFit: "contain",
803
+ cursor: "pointer",
804
+ borderRadius: 8
805
+ }
327
806
  }
328
- },
329
- key
330
- );
807
+ ),
808
+ expanded && /* @__PURE__ */ jsx(
809
+ "div",
810
+ {
811
+ onClick: () => setExpanded(false),
812
+ style: {
813
+ position: "fixed",
814
+ inset: 0,
815
+ zIndex: 9999,
816
+ background: "rgba(0, 0, 0, 0.8)",
817
+ display: "flex",
818
+ alignItems: "center",
819
+ justifyContent: "center",
820
+ padding: 16,
821
+ cursor: "pointer"
822
+ },
823
+ children: /* @__PURE__ */ jsx(
824
+ "img",
825
+ {
826
+ src: viewUrl,
827
+ alt: att.file_name,
828
+ style: {
829
+ maxWidth: "100%",
830
+ maxHeight: "100%",
831
+ objectFit: "contain",
832
+ borderRadius: 8
833
+ }
834
+ }
835
+ )
836
+ }
837
+ )
838
+ ] });
331
839
  }
332
- if (attachment.mime_type.startsWith("video/")) {
840
+ if (isVideo && viewUrl) {
333
841
  return /* @__PURE__ */ jsx(
334
842
  "video",
335
843
  {
844
+ src: viewUrl,
336
845
  controls: true,
337
- src: url,
846
+ preload: "metadata",
847
+ poster: att.thumbnail_url,
338
848
  style: {
339
849
  display: "block",
340
- width: "100%",
341
- maxWidth: 320,
342
- borderRadius: 12,
343
- marginTop: 8
850
+ maxHeight: 240,
851
+ maxWidth: "100%",
852
+ borderRadius: 8
344
853
  }
345
- },
346
- key
854
+ }
347
855
  );
348
856
  }
349
- if (attachment.mime_type.startsWith("audio/")) {
857
+ if (isAudio && viewUrl) {
350
858
  return /* @__PURE__ */ jsx(
351
859
  "audio",
352
860
  {
861
+ src: viewUrl,
353
862
  controls: true,
354
- src: url,
863
+ style: { display: "block", width: "100%", marginTop: 4 }
864
+ }
865
+ );
866
+ }
867
+ if (isImage && !viewUrl) {
868
+ return /* @__PURE__ */ jsxs(
869
+ "div",
870
+ {
355
871
  style: {
356
- display: "block",
357
- width: "100%",
358
- marginTop: 8
359
- }
360
- },
361
- key
872
+ display: "flex",
873
+ alignItems: "center",
874
+ gap: 8,
875
+ padding: "8px 12px",
876
+ fontSize: 12,
877
+ color: "var(--sm-muted-text, #6b7280)",
878
+ background: "var(--sm-surface-muted, #f8fafc)",
879
+ borderRadius: 8
880
+ },
881
+ children: [
882
+ /* @__PURE__ */ jsx(
883
+ "div",
884
+ {
885
+ style: {
886
+ width: 16,
887
+ height: 16,
888
+ border: "2px solid var(--sm-border-color, #e5e7eb)",
889
+ borderTopColor: "var(--sm-primary, #2563eb)",
890
+ borderRadius: 999,
891
+ animation: "sm-spin 0.8s linear infinite"
892
+ }
893
+ }
894
+ ),
895
+ "Loading image..."
896
+ ]
897
+ }
362
898
  );
363
899
  }
364
- return /* @__PURE__ */ jsx(
365
- "a",
900
+ return /* @__PURE__ */ jsxs(
901
+ "div",
366
902
  {
367
- href: url,
368
- target: "_blank",
369
- rel: "noreferrer",
370
903
  style: {
371
- display: "inline-block",
372
- marginTop: 8,
373
- color: "inherit",
374
- fontSize: 13
904
+ display: "flex",
905
+ alignItems: "center",
906
+ gap: 8,
907
+ padding: "8px 12px",
908
+ fontSize: 14,
909
+ color: "var(--sm-text-color, #111827)",
910
+ background: "var(--sm-surface-muted, #f8fafc)",
911
+ borderRadius: 8,
912
+ border: "1px solid var(--sm-border-color, #e5e7eb)"
375
913
  },
376
- children: attachment.file_name
377
- },
378
- key
914
+ children: [
915
+ /* @__PURE__ */ jsxs(
916
+ "svg",
917
+ {
918
+ xmlns: "http://www.w3.org/2000/svg",
919
+ width: "16",
920
+ height: "16",
921
+ viewBox: "0 0 24 24",
922
+ fill: "none",
923
+ stroke: "currentColor",
924
+ strokeWidth: "2",
925
+ children: [
926
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
927
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })
928
+ ]
929
+ }
930
+ ),
931
+ viewUrl ? /* @__PURE__ */ jsx(
932
+ "a",
933
+ {
934
+ href: viewUrl,
935
+ target: "_blank",
936
+ rel: "noreferrer",
937
+ style: { color: "inherit", textDecoration: "underline" },
938
+ children: att.file_name
939
+ }
940
+ ) : att.file_name
941
+ ]
942
+ }
379
943
  );
380
944
  }
381
945
  function ChatMessageItem({
382
946
  message,
383
947
  currentUserId,
948
+ conversationId,
949
+ profile,
384
950
  onAddReaction,
385
951
  onRemoveReaction,
952
+ onEdit,
953
+ onDelete,
386
954
  onReport,
955
+ onFetchAttachmentUrl,
956
+ isOwnMessage: isOwnMessageProp,
387
957
  highlight = false
388
958
  }) {
389
- const [showPicker, setShowPicker] = useState(false);
390
- const isOwn = Boolean(currentUserId && message.sender_id === currentUserId);
959
+ const [showActions, setShowActions] = useState(false);
960
+ const [editing, setEditing] = useState(false);
961
+ const [editContent, setEditContent] = useState(message.content);
962
+ const isOwn = isOwnMessageProp !== void 0 ? isOwnMessageProp : Boolean(currentUserId && message.sender_id === currentUserId);
963
+ const displayName = profile?.display_name ?? "User";
964
+ const username = profile?.username;
965
+ const avatarUrl = profile?.avatar_url;
966
+ const initials = displayName.charAt(0).toUpperCase();
391
967
  const canReact = Boolean(onAddReaction || onRemoveReaction);
392
- const reactionEntries = useMemo(() => message.reactions ?? [], [message.reactions]);
968
+ function handleToggleReaction(emoji) {
969
+ const hasReacted = message.reactions?.some(
970
+ (r) => r.emoji === emoji && currentUserId && r.user_ids.includes(currentUserId)
971
+ );
972
+ if (hasReacted) {
973
+ void onRemoveReaction?.(message.id, emoji);
974
+ } else {
975
+ void onAddReaction?.(message.id, emoji);
976
+ }
977
+ }
978
+ function handleSaveEdit() {
979
+ if (editContent.trim() && editContent !== message.content) {
980
+ void onEdit?.(message.id, editContent.trim());
981
+ }
982
+ setEditing(false);
983
+ }
984
+ if (message.message_type === "system") {
985
+ return /* @__PURE__ */ jsx(
986
+ "div",
987
+ {
988
+ style: {
989
+ textAlign: "center",
990
+ fontSize: 12,
991
+ color: "var(--sm-muted-text, #6b7280)",
992
+ padding: "8px 0",
993
+ fontStyle: "italic"
994
+ },
995
+ children: message.content
996
+ }
997
+ );
998
+ }
393
999
  return /* @__PURE__ */ jsxs(
394
1000
  "div",
395
1001
  {
396
1002
  style: {
397
1003
  display: "flex",
398
- flexDirection: "column",
399
- alignItems: isOwn ? "flex-end" : "flex-start",
400
- gap: 6
1004
+ justifyContent: isOwn ? "flex-end" : "flex-start",
1005
+ padding: "3px 16px",
1006
+ position: "relative"
401
1007
  },
1008
+ onMouseEnter: () => setShowActions(true),
1009
+ onMouseLeave: () => setShowActions(false),
402
1010
  children: [
403
- /* @__PURE__ */ jsxs(
404
- "div",
405
- {
406
- style: {
407
- maxWidth: "min(82%, 560px)",
408
- padding: message.attachments?.length ? 10 : "10px 12px",
409
- borderRadius: "var(--sm-border-radius, 16px)",
410
- background: isOwn ? "var(--sm-own-bubble, #2563eb)" : "var(--sm-other-bubble, #f3f4f6)",
411
- color: isOwn ? "var(--sm-own-text, #fff)" : "var(--sm-other-text, #111827)",
412
- boxShadow: highlight ? "0 0 0 2px rgba(37, 99, 235, 0.22)" : "none",
413
- transition: "box-shadow 0.2s ease"
414
- },
415
- children: [
416
- message.content ? /* @__PURE__ */ jsx("div", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 14 }, children: message.content }) : null,
417
- message.attachments?.map((attachment) => renderAttachment(message.id, attachment))
418
- ]
419
- }
420
- ),
421
- /* @__PURE__ */ jsxs(
1011
+ !isOwn && /* @__PURE__ */ jsx(
422
1012
  "div",
423
1013
  {
424
1014
  style: {
1015
+ flexShrink: 0,
1016
+ width: 32,
1017
+ height: 32,
1018
+ borderRadius: 999,
1019
+ background: "var(--sm-surface-muted, #f3f4f6)",
1020
+ overflow: "hidden",
425
1021
  display: "flex",
426
1022
  alignItems: "center",
427
- gap: 8,
1023
+ justifyContent: "center",
1024
+ fontSize: 12,
1025
+ fontWeight: 500,
428
1026
  color: "var(--sm-muted-text, #6b7280)",
429
- fontSize: 12
1027
+ marginRight: 10,
1028
+ marginTop: 2
430
1029
  },
431
- children: [
432
- /* @__PURE__ */ jsx("span", { children: formatMessageTime(message.created_at) }),
433
- message.is_edited ? /* @__PURE__ */ jsx("span", { children: "edited" }) : null,
434
- onReport && !isOwn ? /* @__PURE__ */ jsx(
435
- "button",
436
- {
437
- type: "button",
438
- onClick: () => void onReport(message.id),
439
- style: {
440
- border: "none",
441
- background: "transparent",
442
- color: "inherit",
443
- cursor: "pointer",
444
- fontSize: 12,
445
- padding: 0
446
- },
447
- children: "Report"
448
- }
449
- ) : null,
450
- canReact ? /* @__PURE__ */ jsx(
451
- "button",
452
- {
453
- type: "button",
454
- onClick: () => setShowPicker((value) => !value),
455
- style: {
456
- border: "none",
457
- background: "transparent",
458
- color: "inherit",
459
- cursor: "pointer",
460
- fontSize: 12,
461
- padding: 0
462
- },
463
- children: "React"
464
- }
465
- ) : null
466
- ]
1030
+ children: avatarUrl ? /* @__PURE__ */ jsx(
1031
+ "img",
1032
+ {
1033
+ src: avatarUrl,
1034
+ alt: displayName,
1035
+ style: { width: "100%", height: "100%", objectFit: "cover" }
1036
+ }
1037
+ ) : initials
467
1038
  }
468
1039
  ),
469
- showPicker && canReact ? /* @__PURE__ */ jsx(
470
- EmojiPicker,
471
- {
472
- onSelect: (emoji) => {
473
- setShowPicker(false);
474
- void onAddReaction?.(message.id, emoji);
1040
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", maxWidth: "75%", minWidth: 0 }, children: [
1041
+ showActions && canReact && /* @__PURE__ */ jsxs(
1042
+ "div",
1043
+ {
1044
+ style: {
1045
+ position: "absolute",
1046
+ top: -12,
1047
+ [isOwn ? "right" : "left"]: 4,
1048
+ zIndex: 50,
1049
+ display: "flex",
1050
+ alignItems: "center",
1051
+ gap: 2,
1052
+ background: "var(--sm-surface, #fff)",
1053
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
1054
+ borderRadius: 8,
1055
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
1056
+ padding: "2px 4px"
1057
+ },
1058
+ children: [
1059
+ /* @__PURE__ */ jsx(
1060
+ EmojiPickerTrigger,
1061
+ {
1062
+ onSelect: (emoji) => void onAddReaction?.(message.id, emoji)
1063
+ }
1064
+ ),
1065
+ !isOwn && onReport && /* @__PURE__ */ jsx(
1066
+ "button",
1067
+ {
1068
+ onClick: () => {
1069
+ setShowActions(false);
1070
+ onReport(message.id);
1071
+ },
1072
+ type: "button",
1073
+ "aria-label": "Report",
1074
+ title: "Report message",
1075
+ style: {
1076
+ padding: 6,
1077
+ border: "none",
1078
+ background: "transparent",
1079
+ cursor: "pointer",
1080
+ color: "var(--sm-muted-text, #6b7280)",
1081
+ borderRadius: 8,
1082
+ display: "flex",
1083
+ alignItems: "center",
1084
+ justifyContent: "center"
1085
+ },
1086
+ children: /* @__PURE__ */ jsxs(
1087
+ "svg",
1088
+ {
1089
+ xmlns: "http://www.w3.org/2000/svg",
1090
+ width: "14",
1091
+ height: "14",
1092
+ viewBox: "0 0 24 24",
1093
+ fill: "none",
1094
+ stroke: "currentColor",
1095
+ strokeWidth: "2",
1096
+ children: [
1097
+ /* @__PURE__ */ jsx("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
1098
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "22", x2: "4", y2: "15" })
1099
+ ]
1100
+ }
1101
+ )
1102
+ }
1103
+ ),
1104
+ isOwn && onEdit && /* @__PURE__ */ jsx(
1105
+ "button",
1106
+ {
1107
+ onClick: () => setEditing(true),
1108
+ type: "button",
1109
+ "aria-label": "Edit",
1110
+ style: {
1111
+ padding: 6,
1112
+ border: "none",
1113
+ background: "transparent",
1114
+ cursor: "pointer",
1115
+ color: "var(--sm-muted-text, #6b7280)",
1116
+ borderRadius: 8,
1117
+ display: "flex",
1118
+ alignItems: "center",
1119
+ justifyContent: "center"
1120
+ },
1121
+ children: /* @__PURE__ */ jsxs(
1122
+ "svg",
1123
+ {
1124
+ xmlns: "http://www.w3.org/2000/svg",
1125
+ width: "14",
1126
+ height: "14",
1127
+ viewBox: "0 0 24 24",
1128
+ fill: "none",
1129
+ stroke: "currentColor",
1130
+ strokeWidth: "2",
1131
+ children: [
1132
+ /* @__PURE__ */ jsx("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
1133
+ /* @__PURE__ */ jsx("path", { d: "M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
1134
+ ]
1135
+ }
1136
+ )
1137
+ }
1138
+ ),
1139
+ isOwn && onDelete && /* @__PURE__ */ jsx(
1140
+ "button",
1141
+ {
1142
+ onClick: () => void onDelete(message.id),
1143
+ type: "button",
1144
+ "aria-label": "Delete",
1145
+ style: {
1146
+ padding: 6,
1147
+ border: "none",
1148
+ background: "transparent",
1149
+ cursor: "pointer",
1150
+ color: "var(--sm-muted-text, #6b7280)",
1151
+ borderRadius: 8,
1152
+ display: "flex",
1153
+ alignItems: "center",
1154
+ justifyContent: "center"
1155
+ },
1156
+ children: /* @__PURE__ */ jsxs(
1157
+ "svg",
1158
+ {
1159
+ xmlns: "http://www.w3.org/2000/svg",
1160
+ width: "14",
1161
+ height: "14",
1162
+ viewBox: "0 0 24 24",
1163
+ fill: "none",
1164
+ stroke: "currentColor",
1165
+ strokeWidth: "2",
1166
+ children: [
1167
+ /* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
1168
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
1169
+ ]
1170
+ }
1171
+ )
1172
+ }
1173
+ )
1174
+ ]
475
1175
  }
476
- }
477
- ) : null,
478
- reactionEntries.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: reactionEntries.map((reaction) => {
479
- const reacted = Boolean(currentUserId && reaction.user_ids.includes(currentUserId));
480
- return /* @__PURE__ */ jsxs(
481
- "button",
1176
+ ),
1177
+ /* @__PURE__ */ jsxs(
1178
+ "div",
482
1179
  {
483
- type: "button",
484
- onClick: () => {
485
- if (reacted) {
486
- void onRemoveReaction?.(message.id, reaction.emoji);
487
- return;
488
- }
489
- void onAddReaction?.(message.id, reaction.emoji);
1180
+ style: {
1181
+ borderRadius: "var(--sm-border-radius, 16px)",
1182
+ ...isOwn ? { borderBottomRightRadius: 6 } : { borderBottomLeftRadius: 6 },
1183
+ padding: "8px 14px",
1184
+ background: isOwn ? "var(--sm-own-bubble-bg, var(--sm-own-bubble, var(--sm-primary, #2563eb)))" : "var(--sm-other-bubble-bg, var(--sm-other-bubble, #f3f4f6))",
1185
+ color: isOwn ? "var(--sm-own-bubble-text, var(--sm-own-text, #ffffff))" : "var(--sm-other-bubble-text, var(--sm-other-text, #111827))",
1186
+ fontSize: "var(--sm-font-size, 14px)",
1187
+ fontFamily: "var(--sm-font-family, system-ui, -apple-system, sans-serif)",
1188
+ boxShadow: highlight ? "0 0 0 2px rgba(37, 99, 235, 0.22)" : "none",
1189
+ transition: "box-shadow 0.2s ease"
490
1190
  },
1191
+ children: [
1192
+ !isOwn && /* @__PURE__ */ jsxs(
1193
+ "div",
1194
+ {
1195
+ style: {
1196
+ display: "flex",
1197
+ alignItems: "baseline",
1198
+ gap: 6,
1199
+ marginBottom: 2
1200
+ },
1201
+ children: [
1202
+ /* @__PURE__ */ jsx(
1203
+ "span",
1204
+ {
1205
+ style: {
1206
+ fontSize: 12,
1207
+ fontWeight: 600,
1208
+ color: "var(--sm-other-bubble-text, var(--sm-other-text, #111827))"
1209
+ },
1210
+ children: displayName
1211
+ }
1212
+ ),
1213
+ username && /* @__PURE__ */ jsxs(
1214
+ "span",
1215
+ {
1216
+ style: {
1217
+ fontSize: 12,
1218
+ color: "var(--sm-muted-text, #9ca3af)"
1219
+ },
1220
+ children: [
1221
+ "@",
1222
+ username
1223
+ ]
1224
+ }
1225
+ )
1226
+ ]
1227
+ }
1228
+ ),
1229
+ editing ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
1230
+ /* @__PURE__ */ jsx(
1231
+ "input",
1232
+ {
1233
+ type: "text",
1234
+ value: editContent,
1235
+ onChange: (e) => setEditContent(e.target.value),
1236
+ onKeyDown: (e) => {
1237
+ if (e.key === "Enter") handleSaveEdit();
1238
+ if (e.key === "Escape") setEditing(false);
1239
+ },
1240
+ autoFocus: true,
1241
+ style: {
1242
+ flex: 1,
1243
+ fontSize: 14,
1244
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
1245
+ borderRadius: 6,
1246
+ padding: "4px 8px",
1247
+ outline: "none",
1248
+ color: "var(--sm-text-color, #111827)",
1249
+ background: "var(--sm-surface, #fff)"
1250
+ }
1251
+ }
1252
+ ),
1253
+ /* @__PURE__ */ jsx(
1254
+ "button",
1255
+ {
1256
+ onClick: handleSaveEdit,
1257
+ type: "button",
1258
+ style: {
1259
+ fontSize: 12,
1260
+ fontWeight: 500,
1261
+ border: "none",
1262
+ background: "transparent",
1263
+ cursor: "pointer",
1264
+ color: isOwn ? "rgba(255,255,255,0.7)" : "var(--sm-primary, #2563eb)"
1265
+ },
1266
+ children: "Save"
1267
+ }
1268
+ ),
1269
+ /* @__PURE__ */ jsx(
1270
+ "button",
1271
+ {
1272
+ onClick: () => setEditing(false),
1273
+ type: "button",
1274
+ style: {
1275
+ fontSize: 12,
1276
+ border: "none",
1277
+ background: "transparent",
1278
+ cursor: "pointer",
1279
+ color: isOwn ? "rgba(255,255,255,0.7)" : "var(--sm-muted-text, #6b7280)"
1280
+ },
1281
+ children: "Cancel"
1282
+ }
1283
+ )
1284
+ ] }) : message.content ? /* @__PURE__ */ jsx(
1285
+ "p",
1286
+ {
1287
+ style: {
1288
+ margin: 0,
1289
+ whiteSpace: "pre-wrap",
1290
+ wordBreak: "break-word"
1291
+ },
1292
+ children: message.content
1293
+ }
1294
+ ) : null,
1295
+ /* @__PURE__ */ jsxs(
1296
+ "div",
1297
+ {
1298
+ style: {
1299
+ display: "flex",
1300
+ alignItems: "center",
1301
+ gap: 4,
1302
+ marginTop: 2,
1303
+ justifyContent: isOwn ? "flex-end" : "flex-start"
1304
+ },
1305
+ children: [
1306
+ /* @__PURE__ */ jsx(
1307
+ "span",
1308
+ {
1309
+ style: {
1310
+ fontSize: 10,
1311
+ color: isOwn ? "rgba(255,255,255,0.6)" : "var(--sm-muted-text, #9ca3af)"
1312
+ },
1313
+ children: formatMessageTime(message.created_at)
1314
+ }
1315
+ ),
1316
+ message.is_edited && /* @__PURE__ */ jsx(
1317
+ "span",
1318
+ {
1319
+ style: {
1320
+ fontSize: 10,
1321
+ fontStyle: "italic",
1322
+ color: isOwn ? "rgba(255,255,255,0.6)" : "var(--sm-muted-text, #9ca3af)"
1323
+ },
1324
+ children: "(edited)"
1325
+ }
1326
+ )
1327
+ ]
1328
+ }
1329
+ )
1330
+ ]
1331
+ }
1332
+ ),
1333
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx(
1334
+ "div",
1335
+ {
491
1336
  style: {
492
- border: reacted ? "1px solid rgba(37, 99, 235, 0.4)" : "1px solid var(--sm-border-color, #e5e7eb)",
493
- background: reacted ? "rgba(37, 99, 235, 0.08)" : "var(--sm-surface, #fff)",
494
- color: "var(--sm-text-color, #111827)",
495
- borderRadius: 999,
496
- padding: "4px 8px",
497
- cursor: "pointer",
498
- fontSize: 12
1337
+ marginTop: 4,
1338
+ display: "flex",
1339
+ flexWrap: "wrap",
1340
+ gap: 8,
1341
+ justifyContent: isOwn ? "flex-end" : "flex-start"
499
1342
  },
500
- children: [
501
- reaction.emoji,
502
- " ",
503
- reaction.count
504
- ]
505
- },
506
- `${message.id}:${reaction.emoji}`
507
- );
508
- }) }) : null
1343
+ children: message.attachments.map((att) => /* @__PURE__ */ jsx(
1344
+ "div",
1345
+ {
1346
+ style: {
1347
+ borderRadius: 8,
1348
+ overflow: "hidden",
1349
+ maxWidth: 320
1350
+ },
1351
+ children: /* @__PURE__ */ jsx(
1352
+ AttachmentRenderer,
1353
+ {
1354
+ att,
1355
+ fetcher: onFetchAttachmentUrl
1356
+ }
1357
+ )
1358
+ },
1359
+ att.file_id
1360
+ ))
1361
+ }
1362
+ ),
1363
+ /* @__PURE__ */ jsx(
1364
+ ReactionBar,
1365
+ {
1366
+ reactions: message.reactions ?? [],
1367
+ currentUserId,
1368
+ onToggleReaction: handleToggleReaction
1369
+ }
1370
+ )
1371
+ ] })
509
1372
  ]
510
1373
  }
511
1374
  );
512
1375
  }
513
- function getUnreadIndex(messages, unreadSince) {
514
- if (!unreadSince) return -1;
515
- return messages.findIndex((message) => new Date(message.created_at).getTime() > new Date(unreadSince).getTime());
1376
+ function getDateLabel(dateStr) {
1377
+ const date = new Date(dateStr);
1378
+ const now = /* @__PURE__ */ new Date();
1379
+ const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
1380
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1381
+ const diffDays = Math.round(
1382
+ (today.getTime() - dateDay.getTime()) / (1e3 * 60 * 60 * 24)
1383
+ );
1384
+ if (diffDays === 0) return "Today";
1385
+ if (diffDays === 1) return "Yesterday";
1386
+ return date.toLocaleDateString(void 0, {
1387
+ month: "short",
1388
+ day: "numeric",
1389
+ year: date.getFullYear() !== now.getFullYear() ? "numeric" : void 0
1390
+ });
1391
+ }
1392
+ function isSameDay(a, b) {
1393
+ return new Date(a).toDateString() === new Date(b).toDateString();
516
1394
  }
517
1395
  function ChatMessageList({
518
1396
  messages,
519
1397
  currentUserId,
520
- unreadSince,
521
- scrollToUnreadOnMount = true,
1398
+ conversationId,
1399
+ profiles,
1400
+ hasMore = false,
1401
+ isLoading = false,
1402
+ onLoadMore,
522
1403
  onAddReaction,
523
1404
  onRemoveReaction,
1405
+ onEdit,
1406
+ onDelete,
524
1407
  onReport,
1408
+ onFetchAttachmentUrl,
1409
+ firstUnreadMessageId,
1410
+ unreadSince,
1411
+ isNearBottom: isNearBottomProp,
1412
+ onReachBottom,
525
1413
  emptyState
526
1414
  }) {
527
1415
  const containerRef = useRef(null);
528
- const unreadMarkerRef = useRef(null);
529
- const lastMessageCountRef = useRef(messages.length);
530
- const didScrollToUnreadRef = useRef(false);
531
- const [showJumpToLatest, setShowJumpToLatest] = useState(false);
532
- const unreadIndex = useMemo(() => getUnreadIndex(messages, unreadSince), [messages, unreadSince]);
1416
+ const bottomRef = useRef(null);
1417
+ const unreadDividerRef = useRef(null);
1418
+ const prevLengthRef = useRef(messages.length);
1419
+ const isNearBottomRef = useRef(true);
1420
+ const [showNewMessagesPill, setShowNewMessagesPill] = useState(false);
1421
+ const resolvedFirstUnreadId = firstUnreadMessageId ?? (unreadSince ? messages.find(
1422
+ (m) => new Date(m.created_at).getTime() > new Date(unreadSince).getTime()
1423
+ )?.id : void 0);
533
1424
  useEffect(() => {
534
- const container = containerRef.current;
535
- if (!container) return;
536
- const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
537
- const shouldStickToBottom = distanceFromBottom < 80;
538
- if (messages.length > lastMessageCountRef.current && shouldStickToBottom) {
539
- container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
540
- setShowJumpToLatest(false);
541
- } else if (messages.length > lastMessageCountRef.current) {
542
- setShowJumpToLatest(true);
1425
+ if (isNearBottomProp !== void 0) {
1426
+ isNearBottomRef.current = isNearBottomProp;
1427
+ }
1428
+ }, [isNearBottomProp]);
1429
+ const handleScroll = useCallback(() => {
1430
+ const el = containerRef.current;
1431
+ if (!el) return;
1432
+ const threshold = 100;
1433
+ const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
1434
+ isNearBottomRef.current = nearBottom;
1435
+ if (nearBottom) {
1436
+ setShowNewMessagesPill(false);
1437
+ }
1438
+ if (unreadDividerRef.current && el) {
1439
+ const dividerRect = unreadDividerRef.current.getBoundingClientRect();
1440
+ const containerRect = el.getBoundingClientRect();
1441
+ if (dividerRect.bottom < containerRect.bottom) {
1442
+ onReachBottom?.();
1443
+ }
1444
+ } else if (nearBottom) {
1445
+ onReachBottom?.();
1446
+ }
1447
+ }, [onReachBottom]);
1448
+ useEffect(() => {
1449
+ if (messages.length > prevLengthRef.current) {
1450
+ if (isNearBottomRef.current) {
1451
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1452
+ } else {
1453
+ setShowNewMessagesPill(true);
1454
+ }
543
1455
  }
544
- lastMessageCountRef.current = messages.length;
545
- }, [messages]);
1456
+ prevLengthRef.current = messages.length;
1457
+ }, [messages.length]);
546
1458
  useEffect(() => {
547
- if (!scrollToUnreadOnMount || unreadIndex < 0 || didScrollToUnreadRef.current) {
548
- return;
1459
+ if (!isLoading && messages.length > 0) {
1460
+ if (resolvedFirstUnreadId && unreadDividerRef.current) {
1461
+ unreadDividerRef.current.scrollIntoView({
1462
+ block: "start",
1463
+ behavior: "instant"
1464
+ });
1465
+ } else {
1466
+ bottomRef.current?.scrollIntoView({
1467
+ behavior: "instant"
1468
+ });
1469
+ }
549
1470
  }
550
- unreadMarkerRef.current?.scrollIntoView({ block: "center" });
551
- didScrollToUnreadRef.current = true;
552
- }, [scrollToUnreadOnMount, unreadIndex, messages.length]);
553
- if (!messages.length) {
1471
+ }, [isLoading, resolvedFirstUnreadId]);
1472
+ useEffect(() => {
1473
+ const container = containerRef.current;
1474
+ const bottom = bottomRef.current;
1475
+ if (!container || !bottom) return;
1476
+ const observer = new IntersectionObserver(
1477
+ (entries) => {
1478
+ if (entries[0]?.isIntersecting) {
1479
+ onReachBottom?.();
1480
+ }
1481
+ },
1482
+ { root: container, threshold: 0.1 }
1483
+ );
1484
+ observer.observe(bottom);
1485
+ return () => observer.disconnect();
1486
+ }, [onReachBottom]);
1487
+ const scrollToBottom = useCallback(() => {
1488
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1489
+ setShowNewMessagesPill(false);
1490
+ }, []);
1491
+ if (isLoading) {
1492
+ return /* @__PURE__ */ jsx(
1493
+ "div",
1494
+ {
1495
+ style: {
1496
+ flex: 1,
1497
+ display: "flex",
1498
+ alignItems: "center",
1499
+ justifyContent: "center",
1500
+ color: "var(--sm-muted-text, #6b7280)",
1501
+ fontSize: 14,
1502
+ padding: 24
1503
+ },
1504
+ children: /* @__PURE__ */ jsx(
1505
+ "div",
1506
+ {
1507
+ style: {
1508
+ width: 20,
1509
+ height: 20,
1510
+ border: "2px solid var(--sm-border-color, #e5e7eb)",
1511
+ borderTopColor: "var(--sm-primary, #2563eb)",
1512
+ borderRadius: 999,
1513
+ animation: "sm-spin 0.8s linear infinite"
1514
+ }
1515
+ }
1516
+ )
1517
+ }
1518
+ );
1519
+ }
1520
+ if (messages.length === 0) {
554
1521
  return /* @__PURE__ */ jsx(
555
1522
  "div",
556
1523
  {
@@ -568,103 +1535,200 @@ function ChatMessageList({
568
1535
  );
569
1536
  }
570
1537
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, minHeight: 0 }, children: [
571
- /* @__PURE__ */ jsx(
1538
+ /* @__PURE__ */ jsx("style", { children: `@keyframes sm-spin { to { transform: rotate(360deg); } }` }),
1539
+ /* @__PURE__ */ jsxs(
572
1540
  "div",
573
1541
  {
574
1542
  ref: containerRef,
575
- onScroll: (event) => {
576
- const element = event.currentTarget;
577
- const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
578
- setShowJumpToLatest(distanceFromBottom > 120);
579
- },
1543
+ onScroll: handleScroll,
580
1544
  style: {
581
1545
  height: "100%",
582
1546
  overflowY: "auto",
583
- padding: 16,
584
- display: "flex",
585
- flexDirection: "column",
586
- gap: 12,
1547
+ position: "relative",
587
1548
  background: "var(--sm-surface-muted, #f8fafc)"
588
1549
  },
589
- children: messages.map((message, index) => {
590
- const previousMessage = messages[index - 1];
591
- const showDateDivider = !previousMessage || !isSameDay(previousMessage.created_at, message.created_at);
592
- const showUnreadDivider = unreadIndex === index;
593
- return /* @__PURE__ */ jsxs(React3.Fragment, { children: [
594
- showDateDivider ? /* @__PURE__ */ jsx(
595
- "div",
596
- {
597
- style: {
598
- alignSelf: "center",
599
- fontSize: 12,
600
- color: "var(--sm-muted-text, #6b7280)",
601
- padding: "4px 10px",
602
- borderRadius: 999,
603
- background: "rgba(148, 163, 184, 0.12)"
604
- },
605
- children: formatDayLabel(message.created_at)
606
- }
607
- ) : null,
608
- showUnreadDivider ? /* @__PURE__ */ jsxs(
609
- "div",
610
- {
611
- ref: unreadMarkerRef,
612
- style: {
613
- display: "flex",
614
- alignItems: "center",
615
- gap: 10,
616
- color: "var(--sm-primary, #2563eb)",
617
- fontSize: 12,
618
- fontWeight: 600
619
- },
620
- children: [
621
- /* @__PURE__ */ jsx("div", { style: { flex: 1, height: 1, background: "rgba(37, 99, 235, 0.28)" } }),
622
- /* @__PURE__ */ jsx("span", { children: "New messages" }),
623
- /* @__PURE__ */ jsx("div", { style: { flex: 1, height: 1, background: "rgba(37, 99, 235, 0.28)" } })
624
- ]
625
- }
626
- ) : null,
627
- /* @__PURE__ */ jsx(
628
- ChatMessageItem,
629
- {
630
- message,
631
- currentUserId,
632
- onAddReaction,
633
- onRemoveReaction,
634
- onReport,
635
- highlight: showUnreadDivider
636
- }
637
- )
638
- ] }, message.id);
639
- })
1550
+ children: [
1551
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
1552
+ "div",
1553
+ {
1554
+ style: {
1555
+ display: "flex",
1556
+ justifyContent: "center",
1557
+ padding: "12px 0"
1558
+ },
1559
+ children: /* @__PURE__ */ jsx(
1560
+ "button",
1561
+ {
1562
+ onClick: onLoadMore,
1563
+ type: "button",
1564
+ style: {
1565
+ fontSize: 12,
1566
+ color: "var(--sm-muted-text, #6b7280)",
1567
+ fontWeight: 500,
1568
+ padding: "4px 12px",
1569
+ borderRadius: 999,
1570
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
1571
+ background: "var(--sm-surface, #fff)",
1572
+ cursor: "pointer"
1573
+ },
1574
+ children: "Load earlier messages"
1575
+ }
1576
+ )
1577
+ }
1578
+ ),
1579
+ messages.map((msg, i) => {
1580
+ const prevMsg = i > 0 ? messages[i - 1] : null;
1581
+ const showDateSeparator = !prevMsg || !isSameDay(msg.created_at, prevMsg.created_at);
1582
+ const showUnreadDivider = resolvedFirstUnreadId === msg.id;
1583
+ const isOwn = msg.sender_id === currentUserId;
1584
+ return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
1585
+ showUnreadDivider && /* @__PURE__ */ jsxs(
1586
+ "div",
1587
+ {
1588
+ ref: unreadDividerRef,
1589
+ style: {
1590
+ display: "flex",
1591
+ alignItems: "center",
1592
+ gap: 12,
1593
+ padding: "8px 16px",
1594
+ margin: "4px 0"
1595
+ },
1596
+ children: [
1597
+ /* @__PURE__ */ jsx(
1598
+ "div",
1599
+ {
1600
+ style: {
1601
+ flex: 1,
1602
+ height: 1,
1603
+ background: "var(--sm-unread-divider-color, rgba(37, 99, 235, 0.4))"
1604
+ }
1605
+ }
1606
+ ),
1607
+ /* @__PURE__ */ jsx(
1608
+ "span",
1609
+ {
1610
+ style: {
1611
+ fontSize: 12,
1612
+ color: "var(--sm-primary, #2563eb)",
1613
+ fontWeight: 500,
1614
+ background: "var(--sm-unread-divider-bg, rgba(37, 99, 235, 0.06))",
1615
+ padding: "2px 8px",
1616
+ borderRadius: 999
1617
+ },
1618
+ children: "New messages"
1619
+ }
1620
+ ),
1621
+ /* @__PURE__ */ jsx(
1622
+ "div",
1623
+ {
1624
+ style: {
1625
+ flex: 1,
1626
+ height: 1,
1627
+ background: "var(--sm-unread-divider-color, rgba(37, 99, 235, 0.4))"
1628
+ }
1629
+ }
1630
+ )
1631
+ ]
1632
+ }
1633
+ ),
1634
+ showDateSeparator && /* @__PURE__ */ jsxs(
1635
+ "div",
1636
+ {
1637
+ style: {
1638
+ display: "flex",
1639
+ alignItems: "center",
1640
+ gap: 12,
1641
+ padding: "8px 16px",
1642
+ margin: "4px 0"
1643
+ },
1644
+ children: [
1645
+ /* @__PURE__ */ jsx(
1646
+ "div",
1647
+ {
1648
+ style: {
1649
+ flex: 1,
1650
+ height: 1,
1651
+ background: "var(--sm-border-color, #e5e7eb)"
1652
+ }
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ jsx(
1656
+ "span",
1657
+ {
1658
+ style: {
1659
+ fontSize: 12,
1660
+ color: "var(--sm-muted-text, #6b7280)",
1661
+ fontWeight: 500
1662
+ },
1663
+ children: getDateLabel(msg.created_at)
1664
+ }
1665
+ ),
1666
+ /* @__PURE__ */ jsx(
1667
+ "div",
1668
+ {
1669
+ style: {
1670
+ flex: 1,
1671
+ height: 1,
1672
+ background: "var(--sm-border-color, #e5e7eb)"
1673
+ }
1674
+ }
1675
+ )
1676
+ ]
1677
+ }
1678
+ ),
1679
+ /* @__PURE__ */ jsx(
1680
+ ChatMessageItem,
1681
+ {
1682
+ message: msg,
1683
+ currentUserId,
1684
+ conversationId,
1685
+ profile: profiles?.get(msg.sender_id),
1686
+ onAddReaction,
1687
+ onRemoveReaction,
1688
+ onEdit,
1689
+ onDelete,
1690
+ onReport,
1691
+ onFetchAttachmentUrl,
1692
+ isOwnMessage: isOwn,
1693
+ highlight: showUnreadDivider
1694
+ }
1695
+ )
1696
+ ] }, msg.id);
1697
+ }),
1698
+ /* @__PURE__ */ jsx("div", { ref: bottomRef })
1699
+ ]
640
1700
  }
641
1701
  ),
642
- showJumpToLatest ? /* @__PURE__ */ jsx(
1702
+ showNewMessagesPill && /* @__PURE__ */ jsxs(
643
1703
  "button",
644
1704
  {
1705
+ onClick: scrollToBottom,
645
1706
  type: "button",
646
- onClick: () => {
647
- containerRef.current?.scrollTo({
648
- top: containerRef.current.scrollHeight,
649
- behavior: "smooth"
650
- });
651
- setShowJumpToLatest(false);
652
- },
653
1707
  style: {
654
1708
  position: "absolute",
655
- right: 16,
656
- bottom: 16,
1709
+ bottom: 12,
1710
+ left: "50%",
1711
+ transform: "translateX(-50%)",
657
1712
  border: "none",
658
1713
  borderRadius: 999,
659
1714
  background: "var(--sm-primary, #2563eb)",
660
1715
  color: "#fff",
661
- padding: "10px 14px",
1716
+ padding: "6px 16px",
662
1717
  cursor: "pointer",
663
- boxShadow: "0 12px 28px rgba(37, 99, 235, 0.28)"
1718
+ boxShadow: "0 4px 12px rgba(37, 99, 235, 0.3)",
1719
+ fontSize: 12,
1720
+ fontWeight: 500,
1721
+ zIndex: 10,
1722
+ display: "flex",
1723
+ alignItems: "center",
1724
+ gap: 4
664
1725
  },
665
- children: "New messages"
1726
+ children: [
1727
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 14 }, children: "\u2193" }),
1728
+ " New messages"
1729
+ ]
666
1730
  }
667
- ) : null
1731
+ )
668
1732
  ] });
669
1733
  }
670
1734
 
@@ -696,8 +1760,10 @@ function ChatThread({
696
1760
  conversationId,
697
1761
  theme,
698
1762
  currentUserId,
1763
+ profiles,
699
1764
  title = "Chat",
700
- subtitle
1765
+ subtitle,
1766
+ onFetchAttachmentUrl
701
1767
  }) {
702
1768
  const client = useChatClient();
703
1769
  const resolvedUserId = currentUserId ?? client.userId;
@@ -706,8 +1772,12 @@ function ChatThread({
706
1772
  readStatuses,
707
1773
  isLoading,
708
1774
  error,
1775
+ hasMore,
709
1776
  sendMessage,
1777
+ loadMore,
710
1778
  markRead,
1779
+ editMessage,
1780
+ deleteMessage,
711
1781
  addReaction,
712
1782
  removeReaction,
713
1783
  reportMessage,
@@ -812,10 +1882,19 @@ function ChatThread({
812
1882
  {
813
1883
  messages,
814
1884
  currentUserId: resolvedUserId,
815
- unreadSince: ownReadStatus,
1885
+ conversationId,
1886
+ profiles,
1887
+ hasMore,
1888
+ isLoading,
1889
+ onLoadMore: loadMore,
816
1890
  onAddReaction: (messageId, emoji) => void addReaction(messageId, emoji),
817
1891
  onRemoveReaction: (messageId, emoji) => void removeReaction(messageId, emoji),
1892
+ onEdit: (messageId, content) => void editMessage(messageId, content),
1893
+ onDelete: (messageId) => void deleteMessage(messageId),
818
1894
  onReport: (messageId) => void reportMessage(messageId, "other"),
1895
+ onFetchAttachmentUrl,
1896
+ unreadSince: ownReadStatus,
1897
+ onReachBottom: () => void markRead(),
819
1898
  emptyState: isLoading ? "Loading messages..." : "Start the conversation"
820
1899
  }
821
1900
  ),
@@ -837,7 +1916,7 @@ function ChatThread({
837
1916
  onSend: async (content, attachments) => {
838
1917
  await sendMessage(content, {
839
1918
  attachments,
840
- message_type: inferMessageType(content, attachments)
1919
+ message_type: inferMessageType(content, attachments ?? [])
841
1920
  });
842
1921
  },
843
1922
  onTypingChange: (isTyping) => {
@@ -1014,6 +2093,346 @@ function ConversationList({
1014
2093
  }
1015
2094
  );
1016
2095
  }
2096
+ var REPORT_REASONS = [
2097
+ { value: "spam", label: "Spam" },
2098
+ { value: "harassment", label: "Harassment" },
2099
+ { value: "hate", label: "Hate speech" },
2100
+ { value: "violence", label: "Violence" },
2101
+ { value: "other", label: "Other" }
2102
+ ];
2103
+ function ReportDialog({
2104
+ messageId,
2105
+ onSubmit,
2106
+ onClose
2107
+ }) {
2108
+ const [reason, setReason] = useState("spam");
2109
+ const [description, setDescription] = useState("");
2110
+ const [submitting, setSubmitting] = useState(false);
2111
+ const [error, setError] = useState(null);
2112
+ const [submitted, setSubmitted] = useState(false);
2113
+ async function handleSubmit() {
2114
+ setSubmitting(true);
2115
+ setError(null);
2116
+ try {
2117
+ await onSubmit({
2118
+ messageId,
2119
+ reason,
2120
+ description: description.trim() || void 0
2121
+ });
2122
+ setSubmitted(true);
2123
+ setTimeout(onClose, 1500);
2124
+ } catch (e) {
2125
+ setError(e instanceof Error ? e.message : "Failed to submit report");
2126
+ } finally {
2127
+ setSubmitting(false);
2128
+ }
2129
+ }
2130
+ return /* @__PURE__ */ jsx(
2131
+ "div",
2132
+ {
2133
+ style: {
2134
+ position: "fixed",
2135
+ inset: 0,
2136
+ zIndex: 9999,
2137
+ display: "flex",
2138
+ alignItems: "center",
2139
+ justifyContent: "center",
2140
+ background: "rgba(0, 0, 0, 0.5)",
2141
+ backdropFilter: "blur(4px)"
2142
+ },
2143
+ onClick: onClose,
2144
+ children: /* @__PURE__ */ jsxs(
2145
+ "div",
2146
+ {
2147
+ style: {
2148
+ background: "var(--sm-surface, #fff)",
2149
+ borderRadius: 16,
2150
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
2151
+ width: "100%",
2152
+ maxWidth: 448,
2153
+ margin: "0 16px",
2154
+ overflow: "hidden",
2155
+ color: "var(--sm-text-color, #111827)",
2156
+ fontFamily: "var(--sm-font-family, system-ui, -apple-system, sans-serif)"
2157
+ },
2158
+ onClick: (e) => e.stopPropagation(),
2159
+ children: [
2160
+ /* @__PURE__ */ jsxs(
2161
+ "div",
2162
+ {
2163
+ style: {
2164
+ padding: "16px 24px",
2165
+ borderBottom: "1px solid var(--sm-border-color, #e5e7eb)",
2166
+ display: "flex",
2167
+ alignItems: "center",
2168
+ justifyContent: "space-between"
2169
+ },
2170
+ children: [
2171
+ /* @__PURE__ */ jsx(
2172
+ "h3",
2173
+ {
2174
+ style: {
2175
+ margin: 0,
2176
+ fontSize: 16,
2177
+ fontWeight: 600,
2178
+ color: "var(--sm-text-color, #111827)"
2179
+ },
2180
+ children: "Report Message"
2181
+ }
2182
+ ),
2183
+ /* @__PURE__ */ jsx(
2184
+ "button",
2185
+ {
2186
+ onClick: onClose,
2187
+ type: "button",
2188
+ "aria-label": "Close",
2189
+ style: {
2190
+ border: "none",
2191
+ background: "transparent",
2192
+ padding: 4,
2193
+ cursor: "pointer",
2194
+ color: "var(--sm-muted-text, #6b7280)",
2195
+ borderRadius: 8,
2196
+ display: "flex",
2197
+ alignItems: "center",
2198
+ justifyContent: "center"
2199
+ },
2200
+ children: /* @__PURE__ */ jsxs(
2201
+ "svg",
2202
+ {
2203
+ xmlns: "http://www.w3.org/2000/svg",
2204
+ width: "18",
2205
+ height: "18",
2206
+ viewBox: "0 0 24 24",
2207
+ fill: "none",
2208
+ stroke: "currentColor",
2209
+ strokeWidth: "2",
2210
+ children: [
2211
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
2212
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
2213
+ ]
2214
+ }
2215
+ )
2216
+ }
2217
+ )
2218
+ ]
2219
+ }
2220
+ ),
2221
+ /* @__PURE__ */ jsx("div", { style: { padding: "16px 24px" }, children: submitted ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "16px 0" }, children: [
2222
+ /* @__PURE__ */ jsx(
2223
+ "div",
2224
+ {
2225
+ style: {
2226
+ width: 40,
2227
+ height: 40,
2228
+ borderRadius: 999,
2229
+ background: "#dcfce7",
2230
+ display: "flex",
2231
+ alignItems: "center",
2232
+ justifyContent: "center",
2233
+ margin: "0 auto 12px"
2234
+ },
2235
+ children: /* @__PURE__ */ jsx(
2236
+ "svg",
2237
+ {
2238
+ xmlns: "http://www.w3.org/2000/svg",
2239
+ width: "20",
2240
+ height: "20",
2241
+ viewBox: "0 0 24 24",
2242
+ fill: "none",
2243
+ stroke: "#16a34a",
2244
+ strokeWidth: "2",
2245
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
2246
+ }
2247
+ )
2248
+ }
2249
+ ),
2250
+ /* @__PURE__ */ jsx(
2251
+ "p",
2252
+ {
2253
+ style: {
2254
+ margin: 0,
2255
+ fontSize: 14,
2256
+ fontWeight: 500,
2257
+ color: "var(--sm-text-color, #111827)"
2258
+ },
2259
+ children: "Report submitted"
2260
+ }
2261
+ ),
2262
+ /* @__PURE__ */ jsx(
2263
+ "p",
2264
+ {
2265
+ style: {
2266
+ margin: "4px 0 0",
2267
+ fontSize: 12,
2268
+ color: "var(--sm-muted-text, #6b7280)"
2269
+ },
2270
+ children: "We will review this message shortly."
2271
+ }
2272
+ )
2273
+ ] }) : /* @__PURE__ */ jsxs(
2274
+ "div",
2275
+ {
2276
+ style: { display: "flex", flexDirection: "column", gap: 16 },
2277
+ children: [
2278
+ error && /* @__PURE__ */ jsx(
2279
+ "div",
2280
+ {
2281
+ style: {
2282
+ padding: "8px 12px",
2283
+ background: "#fef2f2",
2284
+ color: "#dc2626",
2285
+ fontSize: 14,
2286
+ borderRadius: 8
2287
+ },
2288
+ children: error
2289
+ }
2290
+ ),
2291
+ /* @__PURE__ */ jsxs("div", { children: [
2292
+ /* @__PURE__ */ jsx(
2293
+ "label",
2294
+ {
2295
+ style: {
2296
+ display: "block",
2297
+ fontSize: 14,
2298
+ fontWeight: 500,
2299
+ color: "var(--sm-text-color, #111827)",
2300
+ marginBottom: 6
2301
+ },
2302
+ children: "Reason"
2303
+ }
2304
+ ),
2305
+ /* @__PURE__ */ jsx(
2306
+ "select",
2307
+ {
2308
+ value: reason,
2309
+ onChange: (e) => setReason(e.target.value),
2310
+ style: {
2311
+ width: "100%",
2312
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
2313
+ borderRadius: 8,
2314
+ padding: "8px 12px",
2315
+ fontSize: 14,
2316
+ background: "var(--sm-surface, #fff)",
2317
+ color: "var(--sm-text-color, #111827)",
2318
+ fontFamily: "inherit",
2319
+ outline: "none"
2320
+ },
2321
+ children: REPORT_REASONS.map((r) => /* @__PURE__ */ jsx("option", { value: r.value, children: r.label }, r.value))
2322
+ }
2323
+ )
2324
+ ] }),
2325
+ /* @__PURE__ */ jsxs("div", { children: [
2326
+ /* @__PURE__ */ jsxs(
2327
+ "label",
2328
+ {
2329
+ style: {
2330
+ display: "block",
2331
+ fontSize: 14,
2332
+ fontWeight: 500,
2333
+ color: "var(--sm-text-color, #111827)",
2334
+ marginBottom: 6
2335
+ },
2336
+ children: [
2337
+ "Description",
2338
+ " ",
2339
+ /* @__PURE__ */ jsx(
2340
+ "span",
2341
+ {
2342
+ style: {
2343
+ fontWeight: 400,
2344
+ color: "var(--sm-muted-text, #6b7280)"
2345
+ },
2346
+ children: "(optional)"
2347
+ }
2348
+ )
2349
+ ]
2350
+ }
2351
+ ),
2352
+ /* @__PURE__ */ jsx(
2353
+ "textarea",
2354
+ {
2355
+ value: description,
2356
+ onChange: (e) => setDescription(e.target.value),
2357
+ placeholder: "Provide additional details...",
2358
+ rows: 3,
2359
+ maxLength: 1e3,
2360
+ style: {
2361
+ width: "100%",
2362
+ border: "1px solid var(--sm-border-color, #e5e7eb)",
2363
+ borderRadius: 8,
2364
+ padding: "8px 12px",
2365
+ fontSize: 14,
2366
+ fontFamily: "inherit",
2367
+ color: "var(--sm-text-color, #111827)",
2368
+ resize: "none",
2369
+ outline: "none",
2370
+ boxSizing: "border-box"
2371
+ }
2372
+ }
2373
+ )
2374
+ ] })
2375
+ ]
2376
+ }
2377
+ ) }),
2378
+ !submitted && /* @__PURE__ */ jsxs(
2379
+ "div",
2380
+ {
2381
+ style: {
2382
+ padding: "16px 24px",
2383
+ borderTop: "1px solid var(--sm-border-color, #e5e7eb)",
2384
+ display: "flex",
2385
+ justifyContent: "flex-end",
2386
+ gap: 8
2387
+ },
2388
+ children: [
2389
+ /* @__PURE__ */ jsx(
2390
+ "button",
2391
+ {
2392
+ onClick: onClose,
2393
+ type: "button",
2394
+ style: {
2395
+ padding: "8px 16px",
2396
+ fontSize: 14,
2397
+ fontWeight: 500,
2398
+ color: "var(--sm-text-color, #111827)",
2399
+ background: "transparent",
2400
+ border: "none",
2401
+ borderRadius: 8,
2402
+ cursor: "pointer"
2403
+ },
2404
+ children: "Cancel"
2405
+ }
2406
+ ),
2407
+ /* @__PURE__ */ jsx(
2408
+ "button",
2409
+ {
2410
+ onClick: () => void handleSubmit(),
2411
+ disabled: submitting,
2412
+ type: "button",
2413
+ style: {
2414
+ padding: "8px 16px",
2415
+ fontSize: 14,
2416
+ fontWeight: 500,
2417
+ background: "var(--sm-primary, #2563eb)",
2418
+ color: "#fff",
2419
+ border: "none",
2420
+ borderRadius: 8,
2421
+ cursor: submitting ? "wait" : "pointer",
2422
+ opacity: submitting ? 0.5 : 1
2423
+ },
2424
+ children: submitting ? "Submitting..." : "Submit Report"
2425
+ }
2426
+ )
2427
+ ]
2428
+ }
2429
+ )
2430
+ ]
2431
+ }
2432
+ )
2433
+ }
2434
+ );
2435
+ }
1017
2436
  var ChatContext = createContext(null);
1018
2437
  function useChatContext() {
1019
2438
  const ctx = useContext(ChatContext);
@@ -1449,4 +2868,4 @@ function useUnreadCount() {
1449
2868
  return { totalUnread };
1450
2869
  }
1451
2870
 
1452
- export { ChatInput, ChatMessageItem, ChatMessageList, ChatProvider, ChatThread, ConversationList, EmojiPicker, useChat, useChatClient, useChatConfig, useConnection, useConversations, usePresence, useTyping, useUnreadCount };
2871
+ export { ChatInput, ChatMessageItem, ChatMessageList, ChatProvider, ChatThread, ConversationList, EmojiPicker, EmojiPickerTrigger, ReactionBar, ReportDialog, useChat, useChatClient, useChatConfig, useConnection, useConversations, usePresence, useTyping, useUnreadCount };