@mieweb/ui 0.9.0-dev.4 → 0.9.0-dev.6

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 (50) hide show
  1. package/dist/{chunk-DG6LIEUJ.js → chunk-22CYDCXA.js} +13 -1183
  2. package/dist/chunk-22CYDCXA.js.map +1 -0
  3. package/dist/chunk-3SD7PDCV.js +1878 -0
  4. package/dist/chunk-3SD7PDCV.js.map +1 -0
  5. package/dist/{chunk-USZ3VEPE.js → chunk-NORSXAXL.js} +853 -779
  6. package/dist/chunk-NORSXAXL.js.map +1 -0
  7. package/dist/{chunk-Z3RYJWSV.cjs → chunk-PY7XAG4D.cjs} +3 -4
  8. package/dist/chunk-PY7XAG4D.cjs.map +1 -0
  9. package/dist/chunk-T6PTKWM6.cjs +1912 -0
  10. package/dist/chunk-T6PTKWM6.cjs.map +1 -0
  11. package/dist/{chunk-DH7I5HMK.cjs → chunk-WIAQQNFU.cjs} +12 -1184
  12. package/dist/chunk-WIAQQNFU.cjs.map +1 -0
  13. package/dist/{chunk-XEZCHQHB.js → chunk-X5YM3KXY.js} +3 -4
  14. package/dist/chunk-X5YM3KXY.js.map +1 -0
  15. package/dist/{chunk-3MHG4HYB.cjs → chunk-XCNDZNY7.cjs} +855 -780
  16. package/dist/chunk-XCNDZNY7.cjs.map +1 -0
  17. package/dist/components/SuperChat/index.cjs +8 -8
  18. package/dist/components/SuperChat/index.d.cts +4 -4
  19. package/dist/components/SuperChat/index.d.ts +4 -4
  20. package/dist/components/SuperChat/index.js +2 -2
  21. package/dist/components/SuperChat/plugins/index.cjs +10 -3
  22. package/dist/components/SuperChat/plugins/index.cjs.map +1 -1
  23. package/dist/components/SuperChat/plugins/index.d.cts +3 -3
  24. package/dist/components/SuperChat/plugins/index.d.ts +3 -3
  25. package/dist/components/SuperChat/plugins/index.js +9 -2
  26. package/dist/components/SuperChat/plugins/index.js.map +1 -1
  27. package/dist/index.cjs +171 -114
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +471 -410
  30. package/dist/index.d.ts +471 -410
  31. package/dist/index.js +95 -41
  32. package/dist/index.js.map +1 -1
  33. package/dist/styles.css +1 -1
  34. package/dist/tailwind-preset.cjs +4 -4
  35. package/dist/tailwind-preset.js +1 -1
  36. package/dist/{types-DZDroUW8.d.cts → types-B97JNRNQ.d.cts} +1 -1
  37. package/dist/{types-B2RLmdVd.d.cts → types-CnPEwzlp.d.cts} +6 -2
  38. package/dist/{types-B2RLmdVd.d.ts → types-CnPEwzlp.d.ts} +6 -2
  39. package/dist/{types-9vK_YdEb.d.ts → types-gZXrfLj6.d.ts} +1 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-3MHG4HYB.cjs.map +0 -1
  42. package/dist/chunk-5VXRKOPR.cjs +0 -735
  43. package/dist/chunk-5VXRKOPR.cjs.map +0 -1
  44. package/dist/chunk-DG6LIEUJ.js.map +0 -1
  45. package/dist/chunk-DH7I5HMK.cjs.map +0 -1
  46. package/dist/chunk-IF3X2WZP.js +0 -705
  47. package/dist/chunk-IF3X2WZP.js.map +0 -1
  48. package/dist/chunk-USZ3VEPE.js.map +0 -1
  49. package/dist/chunk-XEZCHQHB.js.map +0 -1
  50. package/dist/chunk-Z3RYJWSV.cjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunk5VXRKOPR_cjs = require('./chunk-5VXRKOPR.cjs');
3
+ var chunkT6PTKWM6_cjs = require('./chunk-T6PTKWM6.cjs');
4
4
  var chunkS5RVSXDV_cjs = require('./chunk-S5RVSXDV.cjs');
5
- var React6 = require('react');
5
+ var React = require('react');
6
6
  var classVarianceAuthority = require('class-variance-authority');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
8
 
@@ -24,513 +24,287 @@ function _interopNamespace(e) {
24
24
  return Object.freeze(n);
25
25
  }
26
26
 
27
- var React6__namespace = /*#__PURE__*/_interopNamespace(React6);
27
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
28
28
 
29
- function isFileAccepted(file, accepted) {
30
- if (!accepted || accepted.length === 0) return true;
31
- const mime = file.type;
32
- const name = file.name.toLowerCase();
33
- return accepted.some((token) => {
34
- if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
35
- if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
36
- if (token.includes("/")) return mime === token;
37
- return false;
38
- });
39
- }
40
- function CharacterCounter({
41
- current,
42
- max,
43
- showWarningAt = 0.9,
44
- className
45
- }) {
46
- const percentage = current / max;
47
- const isWarning = percentage >= showWarningAt && percentage < 1;
48
- const isOver = current > max;
49
- return /* @__PURE__ */ jsxRuntime.jsxs(
50
- "span",
51
- {
52
- className: chunkS5RVSXDV_cjs.cn(
53
- "text-xs tabular-nums",
54
- isOver ? "font-medium text-red-700 dark:text-red-400" : isWarning ? "text-amber-700 dark:text-amber-400" : "text-neutral-600 dark:text-neutral-400",
55
- className
56
- ),
57
- "aria-live": "polite",
58
- "aria-label": `${current} of ${max} characters`,
59
- children: [
60
- current,
61
- "/",
62
- max
63
- ]
64
- }
65
- );
66
- }
67
- CharacterCounter.displayName = "CharacterCounter";
68
- var sendButtonVariants = classVarianceAuthority.cva(
69
- [
70
- "inline-flex items-center justify-center",
71
- "rounded-full p-3 self-start",
72
- "transition-all duration-200",
73
- "focus:outline-none focus:ring-2 focus:ring-primary-500",
74
- "disabled:opacity-50 disabled:cursor-not-allowed"
75
- ],
76
- {
77
- variants: {
78
- variant: {
79
- primary: [
80
- "bg-primary-800 text-white",
81
- "hover:bg-primary-900",
82
- "active:scale-95"
83
- ],
84
- subtle: [
85
- "bg-transparent text-primary-800",
86
- "hover:bg-primary-50 dark:hover:bg-primary-900/20"
87
- ]
88
- },
89
- canSend: {
90
- true: "",
91
- false: "opacity-50 cursor-not-allowed"
29
+ function useMessages(options) {
30
+ const {
31
+ initialMessages = [],
32
+ currentUser,
33
+ onSend,
34
+ onRetry,
35
+ onLoadMore
36
+ } = options;
37
+ const [messages, setMessages] = React__namespace.useState(initialMessages);
38
+ const [isSending, setIsSending] = React__namespace.useState(false);
39
+ const [isLoadingMore, setIsLoadingMore] = React__namespace.useState(false);
40
+ React__namespace.useEffect(() => {
41
+ setMessages(initialMessages);
42
+ }, [initialMessages]);
43
+ const addMessage = React__namespace.useCallback((message) => {
44
+ setMessages((prev) => {
45
+ if (prev.some((m) => m.id === message.id)) {
46
+ return prev;
92
47
  }
48
+ return [...prev, message];
49
+ });
50
+ }, []);
51
+ const updateMessage = React__namespace.useCallback(
52
+ (messageId, updates) => {
53
+ setMessages(
54
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
55
+ );
93
56
  },
94
- defaultVariants: {
95
- variant: "primary",
96
- canSend: false
97
- }
98
- }
99
- );
100
- var SendButton = React6__namespace.forwardRef(
101
- ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
102
- return /* @__PURE__ */ jsxRuntime.jsx(
103
- "button",
104
- {
105
- ref,
106
- type: "submit",
107
- disabled: disabled || !canSend || isLoading,
108
- "data-slot": "composer-send-button",
109
- className: chunkS5RVSXDV_cjs.cn(sendButtonVariants({ variant, canSend }), className),
110
- "aria-label": isLoading ? "Sending message" : "Send message",
111
- ...props,
112
- children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs(
113
- "svg",
114
- {
115
- "aria-hidden": "true",
116
- className: "h-5 w-5 animate-spin",
117
- fill: "none",
118
- viewBox: "0 0 24 24",
119
- children: [
120
- /* @__PURE__ */ jsxRuntime.jsx(
121
- "circle",
122
- {
123
- className: "opacity-25",
124
- cx: "12",
125
- cy: "12",
126
- r: "10",
127
- stroke: "currentColor",
128
- strokeWidth: "4"
129
- }
130
- ),
131
- /* @__PURE__ */ jsxRuntime.jsx(
132
- "path",
133
- {
134
- className: "opacity-75",
135
- fill: "currentColor",
136
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
137
- }
138
- )
139
- ]
140
- }
141
- ) : /* @__PURE__ */ jsxRuntime.jsx(
142
- "svg",
143
- {
144
- "aria-hidden": "true",
145
- className: "h-5 w-5",
146
- fill: "none",
147
- viewBox: "0 0 24 24",
148
- stroke: "currentColor",
149
- children: /* @__PURE__ */ jsxRuntime.jsx(
150
- "path",
151
- {
152
- strokeLinecap: "round",
153
- strokeLinejoin: "round",
154
- strokeWidth: 2,
155
- d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
156
- }
157
- )
158
- }
159
- )
160
- }
161
- );
162
- }
163
- );
164
- SendButton.displayName = "SendButton";
165
- var MessageComposer = React6__namespace.forwardRef(
166
- ({
167
- onSend,
168
- onTypingStart,
169
- onTypingStop,
170
- value: controlledValue,
171
- onValueChange,
172
- placeholder = "Type a message...",
173
- maxLength = 1600,
174
- showCharacterCount = false,
175
- disabled = false,
176
- isSending = false,
177
- showAttachmentPicker = true,
178
- showCameraButton = false,
179
- acceptedFileTypes = ["image/*", "video/*", ".pdf", ".doc", ".docx"],
180
- maxFileSize = 25 * 1024 * 1024,
181
- maxAttachments = 10,
182
- onError,
183
- autoFocus = false,
184
- replyTo = null,
185
- onCancelReply,
186
- variant = "default",
187
- inputTrailing,
188
- mentionOptions,
189
- className
190
- }, ref) => {
191
- const textareaRef = React6__namespace.useRef(null);
192
- const [internalContent, setInternalContent] = React6__namespace.useState("");
193
- const isControlled = controlledValue !== void 0;
194
- const content = isControlled ? controlledValue : internalContent;
195
- const setContent = React6__namespace.useCallback(
196
- (val) => {
197
- if (isControlled) {
198
- const newVal = typeof val === "function" ? val(controlledValue) : val;
199
- onValueChange?.(newVal);
57
+ []
58
+ );
59
+ const removeMessage = React__namespace.useCallback((messageId) => {
60
+ setMessages((prev) => prev.filter((m) => m.id !== messageId));
61
+ }, []);
62
+ const updateStatus = React__namespace.useCallback(
63
+ (messageId, status) => {
64
+ updateMessage(messageId, { status });
65
+ },
66
+ [updateMessage]
67
+ );
68
+ const markAsRead = React__namespace.useCallback(
69
+ (messageId) => {
70
+ updateStatus(messageId, "read");
71
+ },
72
+ [updateStatus]
73
+ );
74
+ const sendMessage = React__namespace.useCallback(
75
+ async (newMessage) => {
76
+ const optimisticId = `optimistic-${Date.now()}`;
77
+ const optimisticMessage = {
78
+ id: optimisticId,
79
+ type: "text",
80
+ content: newMessage.content,
81
+ sender: currentUser,
82
+ timestamp: /* @__PURE__ */ new Date(),
83
+ status: "sending",
84
+ attachments: []
85
+ // Would handle attachment uploads here
86
+ };
87
+ addMessage(optimisticMessage);
88
+ setIsSending(true);
89
+ try {
90
+ if (onSend) {
91
+ const sentMessage = await onSend(newMessage);
92
+ setMessages(
93
+ (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
94
+ );
200
95
  } else {
201
- setInternalContent(val);
96
+ updateStatus(optimisticId, "sent");
202
97
  }
203
- },
204
- [isControlled, controlledValue, onValueChange]
205
- );
206
- const [attachments, setAttachments] = React6__namespace.useState(
207
- []
208
- );
209
- const [isTyping, setIsTyping] = React6__namespace.useState(false);
210
- const typingTimeoutRef = React6__namespace.useRef(null);
211
- React6__namespace.useImperativeHandle(ref, () => textareaRef.current);
212
- React6__namespace.useEffect(() => {
213
- const textarea = textareaRef.current;
214
- if (textarea) {
215
- textarea.style.height = "auto";
216
- textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
217
- }
218
- }, [content]);
219
- React6__namespace.useEffect(() => {
220
- if (content.length > 0 && !isTyping) {
221
- setIsTyping(true);
222
- onTypingStart?.();
223
- }
224
- if (typingTimeoutRef.current) {
225
- clearTimeout(typingTimeoutRef.current);
98
+ } catch {
99
+ updateStatus(optimisticId, "failed");
100
+ } finally {
101
+ setIsSending(false);
226
102
  }
227
- typingTimeoutRef.current = setTimeout(() => {
228
- if (isTyping) {
229
- setIsTyping(false);
230
- onTypingStop?.();
103
+ },
104
+ [currentUser, onSend, addMessage, updateStatus]
105
+ );
106
+ const retryMessage = React__namespace.useCallback(
107
+ async (messageId) => {
108
+ updateStatus(messageId, "sending");
109
+ try {
110
+ if (onRetry) {
111
+ await onRetry(messageId);
112
+ updateStatus(messageId, "sent");
231
113
  }
232
- }, 2e3);
233
- return () => {
234
- if (typingTimeoutRef.current) {
235
- clearTimeout(typingTimeoutRef.current);
236
- }
237
- };
238
- }, [content, isTyping, onTypingStart, onTypingStop]);
239
- React6__namespace.useEffect(() => {
240
- if (autoFocus) {
241
- textareaRef.current?.focus();
242
- }
243
- }, [autoFocus]);
244
- const replyToId = replyTo?.id;
245
- React6__namespace.useEffect(() => {
246
- if (replyToId !== void 0) {
247
- textareaRef.current?.focus();
248
- }
249
- }, [replyToId]);
250
- const mention = chunk5VXRKOPR_cjs.useMentionAutocomplete({
251
- options: mentionOptions,
252
- value: content,
253
- setValue: setContent,
254
- textareaRef
255
- });
256
- const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
257
- const handleSubmit = async (event) => {
258
- event.preventDefault();
259
- if (!canSend) return;
260
- const message = {
261
- content: content.trim(),
262
- attachments: attachments.map((a) => a.file),
263
- replyToId: replyTo?.id
264
- };
265
- setContent("");
266
- setAttachments([]);
267
- setIsTyping(false);
268
- onTypingStop?.();
269
- try {
270
- await onSend(message);
271
114
  } catch {
272
- setContent(message.content);
273
- onError?.("Failed to send message");
274
- }
275
- };
276
- const handleKeyDown = (event) => {
277
- if (mention.handleKeyDown(event)) return;
278
- if (event.key === "Enter" && !event.shiftKey) {
279
- event.preventDefault();
280
- if (canSend) {
281
- handleSubmit(event);
282
- }
115
+ updateStatus(messageId, "failed");
283
116
  }
284
- };
285
- const handleFilesSelected = (files) => {
286
- const remainingSlots = maxAttachments - attachments.length;
287
- const filesToAdd = files.slice(0, remainingSlots);
288
- if (files.length > remainingSlots) {
289
- onError?.(`Maximum ${maxAttachments} attachments allowed`);
117
+ },
118
+ [onRetry, updateStatus]
119
+ );
120
+ const loadMore = React__namespace.useCallback(async () => {
121
+ if (isLoadingMore || !onLoadMore) return;
122
+ setIsLoadingMore(true);
123
+ try {
124
+ const olderMessages = await onLoadMore();
125
+ setMessages((prev) => [...olderMessages, ...prev]);
126
+ } finally {
127
+ setIsLoadingMore(false);
128
+ }
129
+ }, [isLoadingMore, onLoadMore]);
130
+ return {
131
+ messages,
132
+ addMessage,
133
+ updateMessage,
134
+ removeMessage,
135
+ sendMessage,
136
+ retryMessage,
137
+ loadMore,
138
+ isSending,
139
+ isLoadingMore,
140
+ markAsRead,
141
+ updateStatus
142
+ };
143
+ }
144
+ function useTypingIndicator(options = {}) {
145
+ const {
146
+ typingParticipants: initialParticipants = [],
147
+ debounceTime = 2e3,
148
+ onTypingStart,
149
+ onTypingStop
150
+ } = options;
151
+ const [participants, setParticipants] = React__namespace.useState(initialParticipants);
152
+ const [isLocalTyping, setIsLocalTyping] = React__namespace.useState(false);
153
+ const typingTimeoutRef = React__namespace.useRef(null);
154
+ React__namespace.useEffect(() => {
155
+ setParticipants(initialParticipants);
156
+ }, [initialParticipants]);
157
+ const startTyping = React__namespace.useCallback(() => {
158
+ if (!isLocalTyping) {
159
+ setIsLocalTyping(true);
160
+ onTypingStart?.();
161
+ }
162
+ if (typingTimeoutRef.current) {
163
+ clearTimeout(typingTimeoutRef.current);
164
+ }
165
+ typingTimeoutRef.current = setTimeout(() => {
166
+ setIsLocalTyping(false);
167
+ onTypingStop?.();
168
+ }, debounceTime);
169
+ }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
170
+ const stopTyping = React__namespace.useCallback(() => {
171
+ if (typingTimeoutRef.current) {
172
+ clearTimeout(typingTimeoutRef.current);
173
+ }
174
+ setIsLocalTyping(false);
175
+ onTypingStop?.();
176
+ }, [onTypingStop]);
177
+ React__namespace.useEffect(() => {
178
+ return () => {
179
+ if (typingTimeoutRef.current) {
180
+ clearTimeout(typingTimeoutRef.current);
290
181
  }
291
- const newAttachments = filesToAdd.map((file) => {
292
- const type = chunk5VXRKOPR_cjs.getFileType(file.type);
293
- let previewUrl;
294
- if (type === "image" || type === "video") {
295
- previewUrl = URL.createObjectURL(file);
296
- }
297
- return {
298
- id: chunk5VXRKOPR_cjs.generateAttachmentId(),
299
- file,
300
- previewUrl,
301
- type,
302
- state: "pending"
303
- };
304
- });
305
- setAttachments((prev) => [...prev, ...newAttachments]);
306
- };
307
- const handlePaste = (event) => {
308
- if (!showAttachmentPicker || disabled) return;
309
- const pasted = Array.from(event.clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null).filter((f) => isFileAccepted(f, acceptedFileTypes));
310
- if (pasted.length === 0) return;
311
- event.preventDefault();
312
- handleFilesSelected(pasted);
313
182
  };
314
- const handleRemoveAttachment = (attachmentId) => {
315
- setAttachments((prev) => {
316
- const attachment = prev.find((a) => a.id === attachmentId);
317
- if (attachment?.previewUrl) {
318
- URL.revokeObjectURL(attachment.previewUrl);
319
- }
320
- return prev.filter((a) => a.id !== attachmentId);
321
- });
183
+ }, []);
184
+ const typingState = React__namespace.useMemo(
185
+ () => ({
186
+ participants,
187
+ lastUpdated: /* @__PURE__ */ new Date()
188
+ }),
189
+ [participants]
190
+ );
191
+ return {
192
+ typingState,
193
+ startTyping,
194
+ stopTyping,
195
+ setTypingParticipants: setParticipants
196
+ };
197
+ }
198
+ function useMessageScroll(options) {
199
+ const { messages, currentUserId, threshold = 100 } = options;
200
+ const scrollContainerRef = React__namespace.useRef(null);
201
+ const bottomRef = React__namespace.useRef(null);
202
+ const [isScrolledUp, setIsScrolledUp] = React__namespace.useState(false);
203
+ const prevMessageCountRef = React__namespace.useRef(messages.length);
204
+ React__namespace.useEffect(() => {
205
+ const container = scrollContainerRef.current;
206
+ if (!container) return;
207
+ const handleScroll = () => {
208
+ const { scrollTop, scrollHeight, clientHeight } = container;
209
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
210
+ setIsScrolledUp(!isAtBottom);
322
211
  };
323
- React6__namespace.useEffect(() => {
324
- const currentAttachments = attachments;
325
- return () => {
326
- currentAttachments.forEach((a) => {
327
- if (a.previewUrl) {
328
- URL.revokeObjectURL(a.previewUrl);
212
+ container.addEventListener("scroll", handleScroll);
213
+ return () => container.removeEventListener("scroll", handleScroll);
214
+ }, [threshold]);
215
+ const scrollToBottom = React__namespace.useCallback((smooth = true) => {
216
+ bottomRef.current?.scrollIntoView({
217
+ behavior: smooth ? "smooth" : "auto"
218
+ });
219
+ }, []);
220
+ React__namespace.useEffect(() => {
221
+ const messageCountChanged = messages.length !== prevMessageCountRef.current;
222
+ prevMessageCountRef.current = messages.length;
223
+ if (!messageCountChanged) return;
224
+ const lastMessage = messages[messages.length - 1];
225
+ const isOutgoing = lastMessage?.sender.id === currentUserId;
226
+ if (isOutgoing || !isScrolledUp) {
227
+ scrollToBottom(true);
228
+ }
229
+ }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
230
+ return {
231
+ scrollContainerRef,
232
+ bottomRef,
233
+ isScrolledUp,
234
+ scrollToBottom
235
+ };
236
+ }
237
+ function useReadReceipts(options) {
238
+ const { currentUserId, onMarkRead, threshold = 0.5 } = options;
239
+ const observerRef = React__namespace.useRef(null);
240
+ const observedMessagesRef = React__namespace.useRef(/* @__PURE__ */ new Set());
241
+ React__namespace.useEffect(() => {
242
+ observerRef.current = new IntersectionObserver(
243
+ (entries) => {
244
+ entries.forEach((entry) => {
245
+ if (entry.isIntersecting) {
246
+ const messageId = entry.target.getAttribute("data-message-id");
247
+ if (messageId && !observedMessagesRef.current.has(messageId)) {
248
+ observedMessagesRef.current.add(messageId);
249
+ onMarkRead?.(messageId);
250
+ }
329
251
  }
330
252
  });
331
- };
332
- }, [attachments]);
333
- return /* @__PURE__ */ jsxRuntime.jsx(
334
- chunk5VXRKOPR_cjs.DragDropZone,
335
- {
336
- onFilesDropped: handleFilesSelected,
337
- acceptedTypes: acceptedFileTypes,
338
- maxFileSize,
339
- maxFiles: maxAttachments - attachments.length,
340
- disabled: disabled || attachments.length >= maxAttachments,
341
- onError,
342
- className: chunkS5RVSXDV_cjs.cn("w-full", className),
343
- children: /* @__PURE__ */ jsxRuntime.jsxs(
344
- "form",
345
- {
346
- onSubmit: handleSubmit,
347
- "data-slot": "message-composer",
348
- className: "w-full",
349
- children: [
350
- replyTo && /* @__PURE__ */ jsxRuntime.jsxs(
351
- "div",
352
- {
353
- "data-slot": "composer-reply-preview",
354
- className: chunkS5RVSXDV_cjs.cn(
355
- "flex items-center gap-2 px-4 py-2",
356
- "bg-neutral-50 dark:bg-neutral-800/50",
357
- "border-primary-500 border-s-4"
358
- ),
359
- children: [
360
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
361
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
362
- "Replying to ",
363
- replyTo.senderName
364
- ] }),
365
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
366
- ] }),
367
- /* @__PURE__ */ jsxRuntime.jsx(
368
- "button",
369
- {
370
- type: "button",
371
- onClick: onCancelReply,
372
- className: chunkS5RVSXDV_cjs.cn(
373
- "shrink-0 rounded p-1",
374
- "text-neutral-500 hover:text-neutral-700",
375
- "dark:text-neutral-400 dark:hover:text-neutral-200",
376
- "focus:ring-primary-500 focus:ring-2 focus:outline-none"
377
- ),
378
- "aria-label": "Cancel reply",
379
- children: /* @__PURE__ */ jsxRuntime.jsx(
380
- "svg",
381
- {
382
- "aria-hidden": "true",
383
- className: "h-4 w-4",
384
- fill: "none",
385
- viewBox: "0 0 24 24",
386
- stroke: "currentColor",
387
- children: /* @__PURE__ */ jsxRuntime.jsx(
388
- "path",
389
- {
390
- strokeLinecap: "round",
391
- strokeLinejoin: "round",
392
- strokeWidth: 2,
393
- d: "M6 18L18 6M6 6l12 12"
394
- }
395
- )
396
- }
397
- )
398
- }
399
- )
400
- ]
401
- }
402
- ),
403
- attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
404
- "div",
405
- {
406
- "data-slot": "composer-attachments",
407
- className: chunkS5RVSXDV_cjs.cn(
408
- "flex flex-wrap gap-2 p-3",
409
- "border-t border-neutral-200 dark:border-neutral-700"
410
- ),
411
- children: attachments.map((attachment) => /* @__PURE__ */ jsxRuntime.jsx(
412
- chunk5VXRKOPR_cjs.AttachmentPreviewItem,
413
- {
414
- attachment,
415
- onRemove: () => handleRemoveAttachment(attachment.id)
416
- },
417
- attachment.id
418
- ))
419
- }
420
- ),
421
- /* @__PURE__ */ jsxRuntime.jsxs(
422
- "div",
423
- {
424
- "data-slot": "composer-input-area",
425
- className: chunkS5RVSXDV_cjs.cn(
426
- "flex items-center gap-2 p-3",
427
- "bg-white dark:bg-neutral-900",
428
- variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
429
- ),
430
- children: [
431
- showAttachmentPicker && /* @__PURE__ */ jsxRuntime.jsx(
432
- chunk5VXRKOPR_cjs.AttachmentPicker,
433
- {
434
- onFilesSelected: handleFilesSelected,
435
- acceptedTypes: acceptedFileTypes,
436
- maxFileSize,
437
- maxFiles: maxAttachments - attachments.length,
438
- disabled: disabled || attachments.length >= maxAttachments,
439
- onError
440
- }
441
- ),
442
- showCameraButton && /* @__PURE__ */ jsxRuntime.jsx(
443
- chunk5VXRKOPR_cjs.CameraButton,
444
- {
445
- onCapture: (file) => handleFilesSelected([file]),
446
- disabled: disabled || attachments.length >= maxAttachments
447
- }
448
- ),
449
- /* @__PURE__ */ jsxRuntime.jsxs(
450
- "div",
451
- {
452
- "data-slot": "composer-input-wrapper",
453
- className: "relative flex-1",
454
- ref: mention.anchorRef,
455
- children: [
456
- /* @__PURE__ */ jsxRuntime.jsx(chunk5VXRKOPR_cjs.MentionMenu, { mention }),
457
- /* @__PURE__ */ jsxRuntime.jsx(
458
- "textarea",
459
- {
460
- ref: textareaRef,
461
- "data-slot": "composer-input",
462
- value: content,
463
- onChange: (e) => {
464
- setContent(e.target.value);
465
- mention.sync(
466
- e.target.value,
467
- e.target.selectionStart ?? e.target.value.length
468
- );
469
- },
470
- onClick: (e) => {
471
- const el = e.currentTarget;
472
- mention.sync(el.value, el.selectionStart ?? el.value.length);
473
- },
474
- onKeyDown: handleKeyDown,
475
- onPaste: handlePaste,
476
- placeholder,
477
- disabled: disabled || isSending,
478
- rows: 1,
479
- className: chunkS5RVSXDV_cjs.cn(
480
- "w-full resize-none rounded-2xl py-2.5",
481
- inputTrailing ? "ps-4 pe-10" : "px-4",
482
- "bg-neutral-100 dark:bg-neutral-800",
483
- "text-neutral-900 dark:text-neutral-100",
484
- "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
485
- "focus:ring-primary-500 focus:ring-2 focus:outline-none",
486
- "disabled:cursor-not-allowed disabled:opacity-50",
487
- "transition-colors",
488
- "max-h-[150px]"
489
- ),
490
- "aria-label": "Message",
491
- "aria-describedby": showCharacterCount ? "char-count" : void 0,
492
- ...mention.inputProps
493
- }
494
- ),
495
- inputTrailing && /* @__PURE__ */ jsxRuntime.jsx(
496
- "div",
497
- {
498
- "data-slot": "composer-input-trailing",
499
- className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
500
- children: inputTrailing
501
- }
502
- ),
503
- showCharacterCount && /* @__PURE__ */ jsxRuntime.jsx(
504
- "div",
505
- {
506
- "data-slot": "composer-char-count",
507
- id: "char-count",
508
- className: "absolute end-3 bottom-1.5",
509
- children: /* @__PURE__ */ jsxRuntime.jsx(CharacterCounter, { current: content.length, max: maxLength })
510
- }
511
- )
512
- ]
513
- }
514
- ),
515
- /* @__PURE__ */ jsxRuntime.jsx(
516
- SendButton,
517
- {
518
- canSend,
519
- isLoading: isSending,
520
- disabled
521
- }
522
- )
523
- ]
524
- }
525
- )
526
- ]
527
- }
528
- )
529
- }
253
+ },
254
+ { threshold }
530
255
  );
531
- }
532
- );
533
- MessageComposer.displayName = "MessageComposer";
256
+ return () => {
257
+ observerRef.current?.disconnect();
258
+ };
259
+ }, [onMarkRead, threshold]);
260
+ const observeMessage = React__namespace.useCallback(
261
+ (element, message) => {
262
+ if (!element || !observerRef.current) return;
263
+ if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
264
+ element.setAttribute("data-message-id", message.id);
265
+ observerRef.current.observe(element);
266
+ }
267
+ },
268
+ [currentUserId]
269
+ );
270
+ return { observeMessage };
271
+ }
272
+ function useTypingEmulation({
273
+ value,
274
+ onTypingStart,
275
+ onTypingStop
276
+ }) {
277
+ const [isTyping, setIsTyping] = React__namespace.useState(false);
278
+ const typingTimeoutRef = React__namespace.useRef(
279
+ null
280
+ );
281
+ React__namespace.useEffect(() => {
282
+ if (!onTypingStart && !onTypingStop) return;
283
+ if (value.length > 0 && !isTyping) {
284
+ setIsTyping(true);
285
+ onTypingStart?.();
286
+ }
287
+ if (typingTimeoutRef.current) {
288
+ clearTimeout(typingTimeoutRef.current);
289
+ }
290
+ typingTimeoutRef.current = setTimeout(() => {
291
+ if (isTyping) {
292
+ setIsTyping(false);
293
+ onTypingStop?.();
294
+ }
295
+ }, 2e3);
296
+ return () => {
297
+ if (typingTimeoutRef.current) {
298
+ clearTimeout(typingTimeoutRef.current);
299
+ }
300
+ };
301
+ }, [value, isTyping, onTypingStart, onTypingStop]);
302
+ const stopTyping = React__namespace.useCallback(() => {
303
+ setIsTyping(false);
304
+ onTypingStop?.();
305
+ }, [onTypingStop]);
306
+ return { stopTyping };
307
+ }
534
308
  var statusIconVariants = classVarianceAuthority.cva(
535
309
  "inline-flex items-center gap-0.5 text-current",
536
310
  {
@@ -911,7 +685,7 @@ function defaultFormatTimestamp(timestamp) {
911
685
  minute: "2-digit"
912
686
  });
913
687
  }
914
- var MessageBubble = React6__namespace.forwardRef(
688
+ var MessageBubble = React__namespace.forwardRef(
915
689
  ({
916
690
  className,
917
691
  message,
@@ -1386,7 +1160,7 @@ function LoadMoreButton({
1386
1160
  );
1387
1161
  }
1388
1162
  LoadMoreButton.displayName = "LoadMoreButton";
1389
- var MessageList = React6__namespace.forwardRef(
1163
+ var MessageList = React__namespace.forwardRef(
1390
1164
  ({
1391
1165
  messages,
1392
1166
  currentUser,
@@ -1405,19 +1179,19 @@ var MessageList = React6__namespace.forwardRef(
1405
1179
  className,
1406
1180
  autoScroll = "onNewMessage"
1407
1181
  }, ref) => {
1408
- const scrollContainerRef = React6__namespace.useRef(null);
1409
- const bottomRef = React6__namespace.useRef(null);
1410
- const [isUserScrolled, setIsUserScrolled] = React6__namespace.useState(false);
1411
- const prevMessageCountRef = React6__namespace.useRef(messages.length);
1412
- React6__namespace.useImperativeHandle(ref, () => scrollContainerRef.current);
1413
- const handleScroll = React6__namespace.useCallback(() => {
1182
+ const scrollContainerRef = React__namespace.useRef(null);
1183
+ const bottomRef = React__namespace.useRef(null);
1184
+ const [isUserScrolled, setIsUserScrolled] = React__namespace.useState(false);
1185
+ const prevMessageCountRef = React__namespace.useRef(messages.length);
1186
+ React__namespace.useImperativeHandle(ref, () => scrollContainerRef.current);
1187
+ const handleScroll = React__namespace.useCallback(() => {
1414
1188
  const container = scrollContainerRef.current;
1415
1189
  if (!container) return;
1416
1190
  const { scrollTop, scrollHeight, clientHeight } = container;
1417
1191
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
1418
1192
  setIsUserScrolled(!isAtBottom);
1419
1193
  }, []);
1420
- React6__namespace.useEffect(() => {
1194
+ React__namespace.useEffect(() => {
1421
1195
  const container = scrollContainerRef.current;
1422
1196
  const bottom = bottomRef.current;
1423
1197
  if (!container || !bottom) return;
@@ -1433,7 +1207,7 @@ var MessageList = React6__namespace.forwardRef(
1433
1207
  }
1434
1208
  }
1435
1209
  }, [messages, currentUser.id, autoScroll, isUserScrolled]);
1436
- React6__namespace.useEffect(() => {
1210
+ React__namespace.useEffect(() => {
1437
1211
  const bottom = bottomRef.current;
1438
1212
  if (bottom && !isLoading) {
1439
1213
  bottom.scrollIntoView();
@@ -1486,7 +1260,7 @@ var MessageList = React6__namespace.forwardRef(
1486
1260
  {
1487
1261
  "data-slot": "message-list-content",
1488
1262
  className: "flex flex-col gap-1 p-4",
1489
- children: messageGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(React6__namespace.Fragment, { children: [
1263
+ children: messageGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Fragment, { children: [
1490
1264
  groupByDate && group.label && /* @__PURE__ */ jsxRuntime.jsx(DateSeparator, { label: group.label }),
1491
1265
  group.messages.map((message, index) => {
1492
1266
  const prevMessage = group.messages[index - 1];
@@ -1558,14 +1332,519 @@ var MessageList = React6__namespace.forwardRef(
1558
1332
  )
1559
1333
  }
1560
1334
  )
1561
- }
1562
- )
1563
- ]
1335
+ }
1336
+ )
1337
+ ]
1338
+ }
1339
+ );
1340
+ }
1341
+ );
1342
+ MessageList.displayName = "MessageList";
1343
+ function isFileAccepted(file, accepted) {
1344
+ if (!accepted || accepted.length === 0) return true;
1345
+ const mime = file.type;
1346
+ const name = file.name.toLowerCase();
1347
+ return accepted.some((token) => {
1348
+ if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
1349
+ if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
1350
+ if (token.includes("/")) return mime === token;
1351
+ return false;
1352
+ });
1353
+ }
1354
+ function CharacterCounter({
1355
+ current,
1356
+ max,
1357
+ showWarningAt = 0.9,
1358
+ className
1359
+ }) {
1360
+ const percentage = current / max;
1361
+ const isWarning = percentage >= showWarningAt && percentage < 1;
1362
+ const isOver = current > max;
1363
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1364
+ "span",
1365
+ {
1366
+ className: chunkS5RVSXDV_cjs.cn(
1367
+ "text-xs tabular-nums",
1368
+ isOver ? "font-medium text-red-700 dark:text-red-400" : isWarning ? "text-amber-700 dark:text-amber-400" : "text-neutral-600 dark:text-neutral-400",
1369
+ className
1370
+ ),
1371
+ "aria-live": "polite",
1372
+ "aria-label": `${current} of ${max} characters`,
1373
+ children: [
1374
+ current,
1375
+ "/",
1376
+ max
1377
+ ]
1378
+ }
1379
+ );
1380
+ }
1381
+ CharacterCounter.displayName = "CharacterCounter";
1382
+ var sendButtonVariants = classVarianceAuthority.cva(
1383
+ [
1384
+ "inline-flex items-center justify-center",
1385
+ "rounded-full p-3 self-start",
1386
+ "transition-all duration-200",
1387
+ "focus:outline-none focus:ring-2 focus:ring-primary-500",
1388
+ "disabled:opacity-50 disabled:cursor-not-allowed"
1389
+ ],
1390
+ {
1391
+ variants: {
1392
+ variant: {
1393
+ primary: [
1394
+ "bg-primary-800 text-white",
1395
+ "hover:bg-primary-900",
1396
+ "active:scale-95"
1397
+ ],
1398
+ subtle: [
1399
+ "bg-transparent text-primary-800",
1400
+ "hover:bg-primary-50 dark:hover:bg-primary-900/20"
1401
+ ]
1402
+ },
1403
+ canSend: {
1404
+ true: "",
1405
+ false: "opacity-50 cursor-not-allowed"
1406
+ }
1407
+ },
1408
+ defaultVariants: {
1409
+ variant: "primary",
1410
+ canSend: false
1411
+ }
1412
+ }
1413
+ );
1414
+ var SendButton = React__namespace.forwardRef(
1415
+ ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
1416
+ return /* @__PURE__ */ jsxRuntime.jsx(
1417
+ "button",
1418
+ {
1419
+ ref,
1420
+ type: "submit",
1421
+ disabled: disabled || !canSend || isLoading,
1422
+ "data-slot": "composer-send-button",
1423
+ className: chunkS5RVSXDV_cjs.cn(sendButtonVariants({ variant, canSend }), className),
1424
+ "aria-label": isLoading ? "Sending message" : "Send message",
1425
+ ...props,
1426
+ children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs(
1427
+ "svg",
1428
+ {
1429
+ "aria-hidden": "true",
1430
+ className: "h-5 w-5 animate-spin",
1431
+ fill: "none",
1432
+ viewBox: "0 0 24 24",
1433
+ children: [
1434
+ /* @__PURE__ */ jsxRuntime.jsx(
1435
+ "circle",
1436
+ {
1437
+ className: "opacity-25",
1438
+ cx: "12",
1439
+ cy: "12",
1440
+ r: "10",
1441
+ stroke: "currentColor",
1442
+ strokeWidth: "4"
1443
+ }
1444
+ ),
1445
+ /* @__PURE__ */ jsxRuntime.jsx(
1446
+ "path",
1447
+ {
1448
+ className: "opacity-75",
1449
+ fill: "currentColor",
1450
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1451
+ }
1452
+ )
1453
+ ]
1454
+ }
1455
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1456
+ "svg",
1457
+ {
1458
+ "aria-hidden": "true",
1459
+ className: "h-5 w-5",
1460
+ fill: "none",
1461
+ viewBox: "0 0 24 24",
1462
+ stroke: "currentColor",
1463
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1464
+ "path",
1465
+ {
1466
+ strokeLinecap: "round",
1467
+ strokeLinejoin: "round",
1468
+ strokeWidth: 2,
1469
+ d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
1470
+ }
1471
+ )
1472
+ }
1473
+ )
1474
+ }
1475
+ );
1476
+ }
1477
+ );
1478
+ SendButton.displayName = "SendButton";
1479
+ var MessageComposer = React__namespace.forwardRef(
1480
+ ({
1481
+ onSend,
1482
+ onTypingStart,
1483
+ onTypingStop,
1484
+ value: controlledValue,
1485
+ onValueChange,
1486
+ placeholder = "Type a message...",
1487
+ maxLength = 1600,
1488
+ showCharacterCount = false,
1489
+ disabled = false,
1490
+ isSending = false,
1491
+ showAttachmentPicker = true,
1492
+ showCameraButton = false,
1493
+ acceptedFileTypes = chunkT6PTKWM6_cjs.DEFAULT_ACCEPTED_FILE_TYPES,
1494
+ maxFileSize = chunkT6PTKWM6_cjs.DEFAULT_MAX_FILE_SIZE,
1495
+ maxAttachments = 10,
1496
+ onError,
1497
+ autoFocus = false,
1498
+ replyTo = null,
1499
+ onCancelReply,
1500
+ variant = "default",
1501
+ inputTrailing,
1502
+ mentionOptions,
1503
+ className
1504
+ }, ref) => {
1505
+ const textareaRef = React__namespace.useRef(null);
1506
+ const [internalContent, setInternalContent] = React__namespace.useState("");
1507
+ const isControlled = controlledValue !== void 0;
1508
+ const content = isControlled ? controlledValue : internalContent;
1509
+ const setContent = React__namespace.useCallback(
1510
+ (val) => {
1511
+ if (isControlled) {
1512
+ const newVal = typeof val === "function" ? val(controlledValue) : val;
1513
+ onValueChange?.(newVal);
1514
+ } else {
1515
+ setInternalContent(val);
1516
+ }
1517
+ },
1518
+ [isControlled, controlledValue, onValueChange]
1519
+ );
1520
+ const [attachments, setAttachments] = React__namespace.useState(
1521
+ []
1522
+ );
1523
+ const [isTyping, setIsTyping] = React__namespace.useState(false);
1524
+ const typingTimeoutRef = React__namespace.useRef(null);
1525
+ React__namespace.useImperativeHandle(ref, () => textareaRef.current);
1526
+ React__namespace.useEffect(() => {
1527
+ const textarea = textareaRef.current;
1528
+ if (textarea) {
1529
+ textarea.style.height = "auto";
1530
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
1531
+ }
1532
+ }, [content]);
1533
+ React__namespace.useEffect(() => {
1534
+ if (content.length > 0 && !isTyping) {
1535
+ setIsTyping(true);
1536
+ onTypingStart?.();
1537
+ }
1538
+ if (typingTimeoutRef.current) {
1539
+ clearTimeout(typingTimeoutRef.current);
1540
+ }
1541
+ typingTimeoutRef.current = setTimeout(() => {
1542
+ if (isTyping) {
1543
+ setIsTyping(false);
1544
+ onTypingStop?.();
1545
+ }
1546
+ }, 2e3);
1547
+ return () => {
1548
+ if (typingTimeoutRef.current) {
1549
+ clearTimeout(typingTimeoutRef.current);
1550
+ }
1551
+ };
1552
+ }, [content, isTyping, onTypingStart, onTypingStop]);
1553
+ React__namespace.useEffect(() => {
1554
+ if (autoFocus) {
1555
+ textareaRef.current?.focus();
1556
+ }
1557
+ }, [autoFocus]);
1558
+ const replyToId = replyTo?.id;
1559
+ React__namespace.useEffect(() => {
1560
+ if (replyToId !== void 0) {
1561
+ textareaRef.current?.focus();
1562
+ }
1563
+ }, [replyToId]);
1564
+ const mention = chunkT6PTKWM6_cjs.useMentionAutocomplete({
1565
+ options: mentionOptions,
1566
+ value: content,
1567
+ setValue: setContent,
1568
+ textareaRef
1569
+ });
1570
+ const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
1571
+ const handleSubmit = async (event) => {
1572
+ event.preventDefault();
1573
+ if (!canSend) return;
1574
+ const message = {
1575
+ content: content.trim(),
1576
+ attachments: attachments.map((a) => a.file),
1577
+ replyToId: replyTo?.id
1578
+ };
1579
+ setContent("");
1580
+ setAttachments([]);
1581
+ setIsTyping(false);
1582
+ onTypingStop?.();
1583
+ try {
1584
+ await onSend(message);
1585
+ } catch {
1586
+ setContent(message.content);
1587
+ onError?.("Failed to send message");
1588
+ }
1589
+ };
1590
+ const handleKeyDown = (event) => {
1591
+ if (mention.handleKeyDown(event)) return;
1592
+ if (event.key === "Enter" && !event.shiftKey) {
1593
+ event.preventDefault();
1594
+ if (canSend) {
1595
+ handleSubmit(event);
1596
+ }
1597
+ }
1598
+ };
1599
+ const handleFilesSelected = (files) => {
1600
+ const remainingSlots = maxAttachments - attachments.length;
1601
+ const filesToAdd = files.slice(0, remainingSlots);
1602
+ if (files.length > remainingSlots) {
1603
+ onError?.(`Maximum ${maxAttachments} attachments allowed`);
1604
+ }
1605
+ const newAttachments = filesToAdd.map((file) => {
1606
+ const type = chunkT6PTKWM6_cjs.getFileType(file.type);
1607
+ let previewUrl;
1608
+ if (type === "image" || type === "video") {
1609
+ previewUrl = URL.createObjectURL(file);
1610
+ }
1611
+ return {
1612
+ id: chunkT6PTKWM6_cjs.generateAttachmentId(),
1613
+ file,
1614
+ previewUrl,
1615
+ type,
1616
+ state: "pending"
1617
+ };
1618
+ });
1619
+ setAttachments((prev) => [...prev, ...newAttachments]);
1620
+ };
1621
+ const handlePaste = (event) => {
1622
+ if (!showAttachmentPicker || disabled) return;
1623
+ const pasted = Array.from(event.clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null).filter((f) => isFileAccepted(f, acceptedFileTypes));
1624
+ if (pasted.length === 0) return;
1625
+ event.preventDefault();
1626
+ handleFilesSelected(pasted);
1627
+ };
1628
+ const handleRemoveAttachment = (attachmentId) => {
1629
+ setAttachments((prev) => {
1630
+ const attachment = prev.find((a) => a.id === attachmentId);
1631
+ if (attachment?.previewUrl) {
1632
+ URL.revokeObjectURL(attachment.previewUrl);
1633
+ }
1634
+ return prev.filter((a) => a.id !== attachmentId);
1635
+ });
1636
+ };
1637
+ React__namespace.useEffect(() => {
1638
+ const currentAttachments = attachments;
1639
+ return () => {
1640
+ currentAttachments.forEach((a) => {
1641
+ if (a.previewUrl) {
1642
+ URL.revokeObjectURL(a.previewUrl);
1643
+ }
1644
+ });
1645
+ };
1646
+ }, [attachments]);
1647
+ return /* @__PURE__ */ jsxRuntime.jsx(
1648
+ chunkT6PTKWM6_cjs.DragDropZone,
1649
+ {
1650
+ onFilesDropped: handleFilesSelected,
1651
+ acceptedTypes: acceptedFileTypes,
1652
+ maxFileSize,
1653
+ maxFiles: maxAttachments - attachments.length,
1654
+ disabled: disabled || attachments.length >= maxAttachments,
1655
+ onError,
1656
+ className: chunkS5RVSXDV_cjs.cn("w-full", className),
1657
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1658
+ "form",
1659
+ {
1660
+ onSubmit: handleSubmit,
1661
+ "data-slot": "message-composer",
1662
+ className: "w-full",
1663
+ children: [
1664
+ replyTo && /* @__PURE__ */ jsxRuntime.jsxs(
1665
+ "div",
1666
+ {
1667
+ "data-slot": "composer-reply-preview",
1668
+ className: chunkS5RVSXDV_cjs.cn(
1669
+ "flex items-center gap-2 px-4 py-2",
1670
+ "bg-neutral-50 dark:bg-neutral-800/50",
1671
+ "border-primary-500 border-s-4"
1672
+ ),
1673
+ children: [
1674
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1675
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
1676
+ "Replying to ",
1677
+ replyTo.senderName
1678
+ ] }),
1679
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
1680
+ ] }),
1681
+ /* @__PURE__ */ jsxRuntime.jsx(
1682
+ "button",
1683
+ {
1684
+ type: "button",
1685
+ onClick: onCancelReply,
1686
+ className: chunkS5RVSXDV_cjs.cn(
1687
+ "shrink-0 rounded p-1",
1688
+ "text-neutral-500 hover:text-neutral-700",
1689
+ "dark:text-neutral-400 dark:hover:text-neutral-200",
1690
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none"
1691
+ ),
1692
+ "aria-label": "Cancel reply",
1693
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1694
+ "svg",
1695
+ {
1696
+ "aria-hidden": "true",
1697
+ className: "h-4 w-4",
1698
+ fill: "none",
1699
+ viewBox: "0 0 24 24",
1700
+ stroke: "currentColor",
1701
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1702
+ "path",
1703
+ {
1704
+ strokeLinecap: "round",
1705
+ strokeLinejoin: "round",
1706
+ strokeWidth: 2,
1707
+ d: "M6 18L18 6M6 6l12 12"
1708
+ }
1709
+ )
1710
+ }
1711
+ )
1712
+ }
1713
+ )
1714
+ ]
1715
+ }
1716
+ ),
1717
+ attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1718
+ "div",
1719
+ {
1720
+ "data-slot": "composer-attachments",
1721
+ className: chunkS5RVSXDV_cjs.cn(
1722
+ "flex flex-wrap gap-2 p-3",
1723
+ "border-t border-neutral-200 dark:border-neutral-700"
1724
+ ),
1725
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxRuntime.jsx(
1726
+ chunkT6PTKWM6_cjs.AttachmentPreviewItem,
1727
+ {
1728
+ attachment,
1729
+ onRemove: () => handleRemoveAttachment(attachment.id)
1730
+ },
1731
+ attachment.id
1732
+ ))
1733
+ }
1734
+ ),
1735
+ /* @__PURE__ */ jsxRuntime.jsxs(
1736
+ "div",
1737
+ {
1738
+ "data-slot": "composer-input-area",
1739
+ className: chunkS5RVSXDV_cjs.cn(
1740
+ "flex items-center gap-2 p-3",
1741
+ "bg-white dark:bg-neutral-900",
1742
+ variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
1743
+ ),
1744
+ children: [
1745
+ showAttachmentPicker && /* @__PURE__ */ jsxRuntime.jsx(
1746
+ chunkT6PTKWM6_cjs.AttachmentPicker,
1747
+ {
1748
+ onFilesSelected: handleFilesSelected,
1749
+ acceptedTypes: acceptedFileTypes,
1750
+ maxFileSize,
1751
+ maxFiles: maxAttachments - attachments.length,
1752
+ disabled: disabled || attachments.length >= maxAttachments,
1753
+ onError
1754
+ }
1755
+ ),
1756
+ showCameraButton && /* @__PURE__ */ jsxRuntime.jsx(
1757
+ chunkT6PTKWM6_cjs.CameraButton,
1758
+ {
1759
+ onCapture: (file) => handleFilesSelected([file]),
1760
+ disabled: disabled || attachments.length >= maxAttachments
1761
+ }
1762
+ ),
1763
+ /* @__PURE__ */ jsxRuntime.jsxs(
1764
+ "div",
1765
+ {
1766
+ "data-slot": "composer-input-wrapper",
1767
+ className: "relative flex-1",
1768
+ ref: mention.anchorRef,
1769
+ children: [
1770
+ /* @__PURE__ */ jsxRuntime.jsx(chunkT6PTKWM6_cjs.MentionMenu, { mention }),
1771
+ /* @__PURE__ */ jsxRuntime.jsx(
1772
+ "textarea",
1773
+ {
1774
+ ref: textareaRef,
1775
+ "data-slot": "composer-input",
1776
+ value: content,
1777
+ onChange: (e) => {
1778
+ setContent(e.target.value);
1779
+ mention.sync(
1780
+ e.target.value,
1781
+ e.target.selectionStart ?? e.target.value.length
1782
+ );
1783
+ },
1784
+ onClick: (e) => {
1785
+ const el = e.currentTarget;
1786
+ mention.sync(el.value, el.selectionStart ?? el.value.length);
1787
+ },
1788
+ onKeyDown: handleKeyDown,
1789
+ onPaste: handlePaste,
1790
+ placeholder,
1791
+ disabled: disabled || isSending,
1792
+ rows: 1,
1793
+ className: chunkS5RVSXDV_cjs.cn(
1794
+ "w-full resize-none rounded-2xl py-2.5",
1795
+ inputTrailing ? "ps-4 pe-10" : "px-4",
1796
+ "bg-neutral-100 dark:bg-neutral-800",
1797
+ "text-neutral-900 dark:text-neutral-100",
1798
+ "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
1799
+ "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1800
+ "disabled:cursor-not-allowed disabled:opacity-50",
1801
+ "transition-colors",
1802
+ "max-h-[150px]"
1803
+ ),
1804
+ "aria-label": "Message",
1805
+ "aria-describedby": showCharacterCount ? "char-count" : void 0,
1806
+ ...mention.inputProps
1807
+ }
1808
+ ),
1809
+ inputTrailing && /* @__PURE__ */ jsxRuntime.jsx(
1810
+ "div",
1811
+ {
1812
+ "data-slot": "composer-input-trailing",
1813
+ className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
1814
+ children: inputTrailing
1815
+ }
1816
+ ),
1817
+ showCharacterCount && /* @__PURE__ */ jsxRuntime.jsx(
1818
+ "div",
1819
+ {
1820
+ "data-slot": "composer-char-count",
1821
+ id: "char-count",
1822
+ className: "absolute end-3 bottom-1.5",
1823
+ children: /* @__PURE__ */ jsxRuntime.jsx(CharacterCounter, { current: content.length, max: maxLength })
1824
+ }
1825
+ )
1826
+ ]
1827
+ }
1828
+ ),
1829
+ /* @__PURE__ */ jsxRuntime.jsx(
1830
+ SendButton,
1831
+ {
1832
+ canSend,
1833
+ isLoading: isSending,
1834
+ disabled
1835
+ }
1836
+ )
1837
+ ]
1838
+ }
1839
+ )
1840
+ ]
1841
+ }
1842
+ )
1564
1843
  }
1565
1844
  );
1566
1845
  }
1567
1846
  );
1568
- MessageList.displayName = "MessageList";
1847
+ MessageComposer.displayName = "MessageComposer";
1569
1848
  var headerVariants = classVarianceAuthority.cva(
1570
1849
  [
1571
1850
  "flex items-center gap-3 px-4 py-3",
@@ -1625,7 +1904,7 @@ function formatLastSeen(date) {
1625
1904
  if (diffDays < 7) return `${diffDays}d ago`;
1626
1905
  return date.toLocaleDateString();
1627
1906
  }
1628
- var ConversationHeader = React6__namespace.forwardRef(
1907
+ var ConversationHeader = React__namespace.forwardRef(
1629
1908
  ({
1630
1909
  className,
1631
1910
  size,
@@ -1747,7 +2026,7 @@ var ConversationHeader = React6__namespace.forwardRef(
1747
2026
  }
1748
2027
  );
1749
2028
  ConversationHeader.displayName = "ConversationHeader";
1750
- var ConversationListItem = React6__namespace.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
2029
+ var ConversationListItem = React__namespace.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
1751
2030
  const participant = conversation.participants.find((p) => !p.isCurrentUser);
1752
2031
  const title = getConversationTitle(conversation, participant);
1753
2032
  const avatarUrl = conversation.avatarUrl || participant?.avatarUrl;
@@ -1955,7 +2234,7 @@ function ConversationListSkeleton({
1955
2234
  }
1956
2235
  ConversationListSkeleton.displayName = "ConversationListSkeleton";
1957
2236
  function LightboxModal({ attachment, onClose }) {
1958
- React6__namespace.useEffect(() => {
2237
+ React__namespace.useEffect(() => {
1959
2238
  const handleKeyDown = (event) => {
1960
2239
  if (event.key === "Escape") {
1961
2240
  onClose();
@@ -2052,7 +2331,7 @@ function LightboxModal({ attachment, onClose }) {
2052
2331
  );
2053
2332
  }
2054
2333
  LightboxModal.displayName = "LightboxModal";
2055
- var MessageThread = React6__namespace.forwardRef(
2334
+ var MessageThread = React__namespace.forwardRef(
2056
2335
  ({
2057
2336
  conversation,
2058
2337
  messages,
@@ -2083,8 +2362,20 @@ var MessageThread = React6__namespace.forwardRef(
2083
2362
  onError,
2084
2363
  className
2085
2364
  }, ref) => {
2086
- const [lightboxAttachment, setLightboxAttachment] = React6__namespace.useState(null);
2087
- const [replyTo, setReplyTo] = React6__namespace.useState(null);
2365
+ const [lightboxAttachment, setLightboxAttachment] = React__namespace.useState(null);
2366
+ const [replyTo, setReplyTo] = React__namespace.useState(null);
2367
+ const composerRef = React__namespace.useRef(null);
2368
+ const [draft, setDraft] = React__namespace.useState("");
2369
+ const draftEpochRef = React__namespace.useRef(0);
2370
+ const handleDraftChange = React__namespace.useCallback((value) => {
2371
+ draftEpochRef.current += 1;
2372
+ setDraft(value);
2373
+ }, []);
2374
+ const { stopTyping } = useTypingEmulation({
2375
+ value: draft,
2376
+ onTypingStart: eventHandlers.onTypingStart,
2377
+ onTypingStop: eventHandlers.onTypingStop
2378
+ });
2088
2379
  const participant = conversation?.type === "direct" ? conversation.participants.find((p) => p.id !== currentUser.id) : void 0;
2089
2380
  const handleAttachmentClick = (attachment, message) => {
2090
2381
  if (attachment.type === "image" || attachment.type === "video") {
@@ -2093,12 +2384,21 @@ var MessageThread = React6__namespace.forwardRef(
2093
2384
  eventHandlers.onAttachmentClick?.(attachment, message);
2094
2385
  };
2095
2386
  const handleSendMessage = async (newMessage) => {
2387
+ stopTyping();
2096
2388
  const messageWithReply = {
2097
2389
  ...newMessage,
2098
2390
  replyToId: replyTo?.id || newMessage.replyToId
2099
2391
  };
2100
2392
  setReplyTo(null);
2101
- await eventHandlers.onSendMessage?.(messageWithReply);
2393
+ const epoch = draftEpochRef.current;
2394
+ try {
2395
+ await Promise.resolve(eventHandlers.onSendMessage?.(messageWithReply));
2396
+ } catch (error) {
2397
+ if (draftEpochRef.current === epoch) {
2398
+ setDraft(newMessage.content);
2399
+ }
2400
+ throw error;
2401
+ }
2102
2402
  };
2103
2403
  return /* @__PURE__ */ jsxRuntime.jsxs(
2104
2404
  "div",
@@ -2122,11 +2422,13 @@ var MessageThread = React6__namespace.forwardRef(
2122
2422
  }
2123
2423
  ),
2124
2424
  /* @__PURE__ */ jsxRuntime.jsx(
2125
- chunk5VXRKOPR_cjs.DragDropZone,
2425
+ chunkT6PTKWM6_cjs.DragDropZone,
2126
2426
  {
2127
- onFilesDropped: () => {
2128
- onError?.("Drop files on the composer to attach them");
2427
+ onFilesDropped: (files) => {
2428
+ composerRef.current?.addFiles(files);
2129
2429
  },
2430
+ maxFiles: maxAttachments,
2431
+ onError,
2130
2432
  disabled: !showAttachmentPicker,
2131
2433
  className: "flex-1 overflow-hidden",
2132
2434
  children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -2152,23 +2454,38 @@ var MessageThread = React6__namespace.forwardRef(
2152
2454
  }
2153
2455
  ),
2154
2456
  /* @__PURE__ */ jsxRuntime.jsx(
2155
- MessageComposer,
2457
+ "div",
2156
2458
  {
2157
- onSend: handleSendMessage,
2158
- onTypingStart: eventHandlers.onTypingStart,
2159
- onTypingStop: eventHandlers.onTypingStop,
2160
- placeholder,
2161
- maxLength: maxMessageLength,
2162
- showCharacterCount,
2163
- isSending,
2164
- showAttachmentPicker,
2165
- showCameraButton,
2166
- acceptedFileTypes,
2167
- maxFileSize,
2168
- maxAttachments,
2169
- onError,
2170
- replyTo,
2171
- onCancelReply: () => setReplyTo(null)
2459
+ "data-slot": "message-thread-composer",
2460
+ className: "shrink-0 border-t border-neutral-200 p-3 dark:border-neutral-700",
2461
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2462
+ chunkT6PTKWM6_cjs.ChatComposer,
2463
+ {
2464
+ ref: composerRef,
2465
+ value: draft,
2466
+ onValueChange: handleDraftChange,
2467
+ onSend: handleSendMessage,
2468
+ placeholder,
2469
+ maxLength: maxMessageLength,
2470
+ showCharacterCount,
2471
+ isSending,
2472
+ allowAttachments: showAttachmentPicker,
2473
+ acceptedFileTypes: acceptedFileTypes ?? chunkT6PTKWM6_cjs.DEFAULT_ACCEPTED_FILE_TYPES,
2474
+ maxFileSize: maxFileSize ?? chunkT6PTKWM6_cjs.DEFAULT_MAX_FILE_SIZE,
2475
+ maxAttachments,
2476
+ onError,
2477
+ replyTo,
2478
+ onCancelReply: () => setReplyTo(null),
2479
+ inputLabel: "Message",
2480
+ micSlot: showCameraButton ? /* @__PURE__ */ jsxRuntime.jsx(
2481
+ chunkT6PTKWM6_cjs.CameraButton,
2482
+ {
2483
+ onCapture: (file) => composerRef.current?.addFiles([file]),
2484
+ disabled: isSending
2485
+ }
2486
+ ) : void 0
2487
+ }
2488
+ )
2172
2489
  }
2173
2490
  ),
2174
2491
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2248,249 +2565,6 @@ function MessagingSplitView({
2248
2565
  );
2249
2566
  }
2250
2567
  MessagingSplitView.displayName = "MessagingSplitView";
2251
- function useMessages(options) {
2252
- const {
2253
- initialMessages = [],
2254
- currentUser,
2255
- onSend,
2256
- onRetry,
2257
- onLoadMore
2258
- } = options;
2259
- const [messages, setMessages] = React6__namespace.useState(initialMessages);
2260
- const [isSending, setIsSending] = React6__namespace.useState(false);
2261
- const [isLoadingMore, setIsLoadingMore] = React6__namespace.useState(false);
2262
- React6__namespace.useEffect(() => {
2263
- setMessages(initialMessages);
2264
- }, [initialMessages]);
2265
- const addMessage = React6__namespace.useCallback((message) => {
2266
- setMessages((prev) => {
2267
- if (prev.some((m) => m.id === message.id)) {
2268
- return prev;
2269
- }
2270
- return [...prev, message];
2271
- });
2272
- }, []);
2273
- const updateMessage = React6__namespace.useCallback(
2274
- (messageId, updates) => {
2275
- setMessages(
2276
- (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
2277
- );
2278
- },
2279
- []
2280
- );
2281
- const removeMessage = React6__namespace.useCallback((messageId) => {
2282
- setMessages((prev) => prev.filter((m) => m.id !== messageId));
2283
- }, []);
2284
- const updateStatus = React6__namespace.useCallback(
2285
- (messageId, status) => {
2286
- updateMessage(messageId, { status });
2287
- },
2288
- [updateMessage]
2289
- );
2290
- const markAsRead = React6__namespace.useCallback(
2291
- (messageId) => {
2292
- updateStatus(messageId, "read");
2293
- },
2294
- [updateStatus]
2295
- );
2296
- const sendMessage = React6__namespace.useCallback(
2297
- async (newMessage) => {
2298
- const optimisticId = `optimistic-${Date.now()}`;
2299
- const optimisticMessage = {
2300
- id: optimisticId,
2301
- type: "text",
2302
- content: newMessage.content,
2303
- sender: currentUser,
2304
- timestamp: /* @__PURE__ */ new Date(),
2305
- status: "sending",
2306
- attachments: []
2307
- // Would handle attachment uploads here
2308
- };
2309
- addMessage(optimisticMessage);
2310
- setIsSending(true);
2311
- try {
2312
- if (onSend) {
2313
- const sentMessage = await onSend(newMessage);
2314
- setMessages(
2315
- (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
2316
- );
2317
- } else {
2318
- updateStatus(optimisticId, "sent");
2319
- }
2320
- } catch {
2321
- updateStatus(optimisticId, "failed");
2322
- } finally {
2323
- setIsSending(false);
2324
- }
2325
- },
2326
- [currentUser, onSend, addMessage, updateStatus]
2327
- );
2328
- const retryMessage = React6__namespace.useCallback(
2329
- async (messageId) => {
2330
- updateStatus(messageId, "sending");
2331
- try {
2332
- if (onRetry) {
2333
- await onRetry(messageId);
2334
- updateStatus(messageId, "sent");
2335
- }
2336
- } catch {
2337
- updateStatus(messageId, "failed");
2338
- }
2339
- },
2340
- [onRetry, updateStatus]
2341
- );
2342
- const loadMore = React6__namespace.useCallback(async () => {
2343
- if (isLoadingMore || !onLoadMore) return;
2344
- setIsLoadingMore(true);
2345
- try {
2346
- const olderMessages = await onLoadMore();
2347
- setMessages((prev) => [...olderMessages, ...prev]);
2348
- } finally {
2349
- setIsLoadingMore(false);
2350
- }
2351
- }, [isLoadingMore, onLoadMore]);
2352
- return {
2353
- messages,
2354
- addMessage,
2355
- updateMessage,
2356
- removeMessage,
2357
- sendMessage,
2358
- retryMessage,
2359
- loadMore,
2360
- isSending,
2361
- isLoadingMore,
2362
- markAsRead,
2363
- updateStatus
2364
- };
2365
- }
2366
- function useTypingIndicator(options = {}) {
2367
- const {
2368
- typingParticipants: initialParticipants = [],
2369
- debounceTime = 2e3,
2370
- onTypingStart,
2371
- onTypingStop
2372
- } = options;
2373
- const [participants, setParticipants] = React6__namespace.useState(initialParticipants);
2374
- const [isLocalTyping, setIsLocalTyping] = React6__namespace.useState(false);
2375
- const typingTimeoutRef = React6__namespace.useRef(null);
2376
- React6__namespace.useEffect(() => {
2377
- setParticipants(initialParticipants);
2378
- }, [initialParticipants]);
2379
- const startTyping = React6__namespace.useCallback(() => {
2380
- if (!isLocalTyping) {
2381
- setIsLocalTyping(true);
2382
- onTypingStart?.();
2383
- }
2384
- if (typingTimeoutRef.current) {
2385
- clearTimeout(typingTimeoutRef.current);
2386
- }
2387
- typingTimeoutRef.current = setTimeout(() => {
2388
- setIsLocalTyping(false);
2389
- onTypingStop?.();
2390
- }, debounceTime);
2391
- }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
2392
- const stopTyping = React6__namespace.useCallback(() => {
2393
- if (typingTimeoutRef.current) {
2394
- clearTimeout(typingTimeoutRef.current);
2395
- }
2396
- setIsLocalTyping(false);
2397
- onTypingStop?.();
2398
- }, [onTypingStop]);
2399
- React6__namespace.useEffect(() => {
2400
- return () => {
2401
- if (typingTimeoutRef.current) {
2402
- clearTimeout(typingTimeoutRef.current);
2403
- }
2404
- };
2405
- }, []);
2406
- const typingState = React6__namespace.useMemo(
2407
- () => ({
2408
- participants,
2409
- lastUpdated: /* @__PURE__ */ new Date()
2410
- }),
2411
- [participants]
2412
- );
2413
- return {
2414
- typingState,
2415
- startTyping,
2416
- stopTyping,
2417
- setTypingParticipants: setParticipants
2418
- };
2419
- }
2420
- function useMessageScroll(options) {
2421
- const { messages, currentUserId, threshold = 100 } = options;
2422
- const scrollContainerRef = React6__namespace.useRef(null);
2423
- const bottomRef = React6__namespace.useRef(null);
2424
- const [isScrolledUp, setIsScrolledUp] = React6__namespace.useState(false);
2425
- const prevMessageCountRef = React6__namespace.useRef(messages.length);
2426
- React6__namespace.useEffect(() => {
2427
- const container = scrollContainerRef.current;
2428
- if (!container) return;
2429
- const handleScroll = () => {
2430
- const { scrollTop, scrollHeight, clientHeight } = container;
2431
- const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
2432
- setIsScrolledUp(!isAtBottom);
2433
- };
2434
- container.addEventListener("scroll", handleScroll);
2435
- return () => container.removeEventListener("scroll", handleScroll);
2436
- }, [threshold]);
2437
- const scrollToBottom = React6__namespace.useCallback((smooth = true) => {
2438
- bottomRef.current?.scrollIntoView({
2439
- behavior: smooth ? "smooth" : "auto"
2440
- });
2441
- }, []);
2442
- React6__namespace.useEffect(() => {
2443
- const messageCountChanged = messages.length !== prevMessageCountRef.current;
2444
- prevMessageCountRef.current = messages.length;
2445
- if (!messageCountChanged) return;
2446
- const lastMessage = messages[messages.length - 1];
2447
- const isOutgoing = lastMessage?.sender.id === currentUserId;
2448
- if (isOutgoing || !isScrolledUp) {
2449
- scrollToBottom(true);
2450
- }
2451
- }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
2452
- return {
2453
- scrollContainerRef,
2454
- bottomRef,
2455
- isScrolledUp,
2456
- scrollToBottom
2457
- };
2458
- }
2459
- function useReadReceipts(options) {
2460
- const { currentUserId, onMarkRead, threshold = 0.5 } = options;
2461
- const observerRef = React6__namespace.useRef(null);
2462
- const observedMessagesRef = React6__namespace.useRef(/* @__PURE__ */ new Set());
2463
- React6__namespace.useEffect(() => {
2464
- observerRef.current = new IntersectionObserver(
2465
- (entries) => {
2466
- entries.forEach((entry) => {
2467
- if (entry.isIntersecting) {
2468
- const messageId = entry.target.getAttribute("data-message-id");
2469
- if (messageId && !observedMessagesRef.current.has(messageId)) {
2470
- observedMessagesRef.current.add(messageId);
2471
- onMarkRead?.(messageId);
2472
- }
2473
- }
2474
- });
2475
- },
2476
- { threshold }
2477
- );
2478
- return () => {
2479
- observerRef.current?.disconnect();
2480
- };
2481
- }, [onMarkRead, threshold]);
2482
- const observeMessage = React6__namespace.useCallback(
2483
- (element, message) => {
2484
- if (!element || !observerRef.current) return;
2485
- if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
2486
- element.setAttribute("data-message-id", message.id);
2487
- observerRef.current.observe(element);
2488
- }
2489
- },
2490
- [currentUserId]
2491
- );
2492
- return { observeMessage };
2493
- }
2494
2568
 
2495
2569
  exports.AttachmentPreview = AttachmentPreview;
2496
2570
  exports.CharacterCounter = CharacterCounter;
@@ -2524,6 +2598,7 @@ exports.sendButtonVariants = sendButtonVariants;
2524
2598
  exports.useMessageScroll = useMessageScroll;
2525
2599
  exports.useMessages = useMessages;
2526
2600
  exports.useReadReceipts = useReadReceipts;
2601
+ exports.useTypingEmulation = useTypingEmulation;
2527
2602
  exports.useTypingIndicator = useTypingIndicator;
2528
- //# sourceMappingURL=chunk-3MHG4HYB.cjs.map
2529
- //# sourceMappingURL=chunk-3MHG4HYB.cjs.map
2603
+ //# sourceMappingURL=chunk-XCNDZNY7.cjs.map
2604
+ //# sourceMappingURL=chunk-XCNDZNY7.cjs.map