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