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