@mieweb/ui 0.9.0-dev.5 → 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 (33) hide show
  1. package/dist/{chunk-QWJGEDHV.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-PL6TFCYR.js → chunk-NORSXAXL.js} +374 -300
  6. package/dist/chunk-NORSXAXL.js.map +1 -0
  7. package/dist/chunk-T6PTKWM6.cjs +1912 -0
  8. package/dist/chunk-T6PTKWM6.cjs.map +1 -0
  9. package/dist/{chunk-XJUIZA4G.cjs → chunk-WIAQQNFU.cjs} +12 -1184
  10. package/dist/chunk-WIAQQNFU.cjs.map +1 -0
  11. package/dist/{chunk-ZXRVVNT5.cjs → chunk-XCNDZNY7.cjs} +384 -309
  12. package/dist/chunk-XCNDZNY7.cjs.map +1 -0
  13. package/dist/components/SuperChat/index.cjs +8 -8
  14. package/dist/components/SuperChat/index.js +2 -2
  15. package/dist/components/SuperChat/plugins/index.cjs +10 -3
  16. package/dist/components/SuperChat/plugins/index.cjs.map +1 -1
  17. package/dist/components/SuperChat/plugins/index.js +9 -2
  18. package/dist/components/SuperChat/plugins/index.js.map +1 -1
  19. package/dist/index.cjs +92 -120
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +38 -4
  22. package/dist/index.d.ts +38 -4
  23. package/dist/index.js +17 -48
  24. package/dist/index.js.map +1 -1
  25. package/package.json +1 -1
  26. package/dist/chunk-H3GRO43Z.cjs +0 -735
  27. package/dist/chunk-H3GRO43Z.cjs.map +0 -1
  28. package/dist/chunk-MD3ZY4UO.js +0 -705
  29. package/dist/chunk-MD3ZY4UO.js.map +0 -1
  30. package/dist/chunk-PL6TFCYR.js.map +0 -1
  31. package/dist/chunk-QWJGEDHV.js.map +0 -1
  32. package/dist/chunk-XJUIZA4G.cjs.map +0 -1
  33. package/dist/chunk-ZXRVVNT5.cjs.map +0 -1
@@ -1,9 +1,288 @@
1
- import { useMentionAutocomplete, DragDropZone, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, getFileType, generateAttachmentId } from './chunk-MD3ZY4UO.js';
1
+ import { useMentionAutocomplete, DragDropZone, DEFAULT_MAX_FILE_SIZE, DEFAULT_ACCEPTED_FILE_TYPES, AttachmentPreviewItem, AttachmentPicker, CameraButton, MentionMenu, ChatComposer, getFileType, generateAttachmentId } from './chunk-3SD7PDCV.js';
2
2
  import { cn } from './chunk-TQONQSJO.js';
3
- import * as React6 from 'react';
3
+ import * as React from 'react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
 
7
+ function useMessages(options) {
8
+ const {
9
+ initialMessages = [],
10
+ currentUser,
11
+ onSend,
12
+ onRetry,
13
+ onLoadMore
14
+ } = options;
15
+ const [messages, setMessages] = React.useState(initialMessages);
16
+ const [isSending, setIsSending] = React.useState(false);
17
+ const [isLoadingMore, setIsLoadingMore] = React.useState(false);
18
+ React.useEffect(() => {
19
+ setMessages(initialMessages);
20
+ }, [initialMessages]);
21
+ const addMessage = React.useCallback((message) => {
22
+ setMessages((prev) => {
23
+ if (prev.some((m) => m.id === message.id)) {
24
+ return prev;
25
+ }
26
+ return [...prev, message];
27
+ });
28
+ }, []);
29
+ const updateMessage = React.useCallback(
30
+ (messageId, updates) => {
31
+ setMessages(
32
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
33
+ );
34
+ },
35
+ []
36
+ );
37
+ const removeMessage = React.useCallback((messageId) => {
38
+ setMessages((prev) => prev.filter((m) => m.id !== messageId));
39
+ }, []);
40
+ const updateStatus = React.useCallback(
41
+ (messageId, status) => {
42
+ updateMessage(messageId, { status });
43
+ },
44
+ [updateMessage]
45
+ );
46
+ const markAsRead = React.useCallback(
47
+ (messageId) => {
48
+ updateStatus(messageId, "read");
49
+ },
50
+ [updateStatus]
51
+ );
52
+ const sendMessage = React.useCallback(
53
+ async (newMessage) => {
54
+ const optimisticId = `optimistic-${Date.now()}`;
55
+ const optimisticMessage = {
56
+ id: optimisticId,
57
+ type: "text",
58
+ content: newMessage.content,
59
+ sender: currentUser,
60
+ timestamp: /* @__PURE__ */ new Date(),
61
+ status: "sending",
62
+ attachments: []
63
+ // Would handle attachment uploads here
64
+ };
65
+ addMessage(optimisticMessage);
66
+ setIsSending(true);
67
+ try {
68
+ if (onSend) {
69
+ const sentMessage = await onSend(newMessage);
70
+ setMessages(
71
+ (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
72
+ );
73
+ } else {
74
+ updateStatus(optimisticId, "sent");
75
+ }
76
+ } catch {
77
+ updateStatus(optimisticId, "failed");
78
+ } finally {
79
+ setIsSending(false);
80
+ }
81
+ },
82
+ [currentUser, onSend, addMessage, updateStatus]
83
+ );
84
+ const retryMessage = React.useCallback(
85
+ async (messageId) => {
86
+ updateStatus(messageId, "sending");
87
+ try {
88
+ if (onRetry) {
89
+ await onRetry(messageId);
90
+ updateStatus(messageId, "sent");
91
+ }
92
+ } catch {
93
+ updateStatus(messageId, "failed");
94
+ }
95
+ },
96
+ [onRetry, updateStatus]
97
+ );
98
+ const loadMore = React.useCallback(async () => {
99
+ if (isLoadingMore || !onLoadMore) return;
100
+ setIsLoadingMore(true);
101
+ try {
102
+ const olderMessages = await onLoadMore();
103
+ setMessages((prev) => [...olderMessages, ...prev]);
104
+ } finally {
105
+ setIsLoadingMore(false);
106
+ }
107
+ }, [isLoadingMore, onLoadMore]);
108
+ return {
109
+ messages,
110
+ addMessage,
111
+ updateMessage,
112
+ removeMessage,
113
+ sendMessage,
114
+ retryMessage,
115
+ loadMore,
116
+ isSending,
117
+ isLoadingMore,
118
+ markAsRead,
119
+ updateStatus
120
+ };
121
+ }
122
+ function useTypingIndicator(options = {}) {
123
+ const {
124
+ typingParticipants: initialParticipants = [],
125
+ debounceTime = 2e3,
126
+ onTypingStart,
127
+ onTypingStop
128
+ } = options;
129
+ const [participants, setParticipants] = React.useState(initialParticipants);
130
+ const [isLocalTyping, setIsLocalTyping] = React.useState(false);
131
+ const typingTimeoutRef = React.useRef(null);
132
+ React.useEffect(() => {
133
+ setParticipants(initialParticipants);
134
+ }, [initialParticipants]);
135
+ const startTyping = React.useCallback(() => {
136
+ if (!isLocalTyping) {
137
+ setIsLocalTyping(true);
138
+ onTypingStart?.();
139
+ }
140
+ if (typingTimeoutRef.current) {
141
+ clearTimeout(typingTimeoutRef.current);
142
+ }
143
+ typingTimeoutRef.current = setTimeout(() => {
144
+ setIsLocalTyping(false);
145
+ onTypingStop?.();
146
+ }, debounceTime);
147
+ }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
148
+ const stopTyping = React.useCallback(() => {
149
+ if (typingTimeoutRef.current) {
150
+ clearTimeout(typingTimeoutRef.current);
151
+ }
152
+ setIsLocalTyping(false);
153
+ onTypingStop?.();
154
+ }, [onTypingStop]);
155
+ React.useEffect(() => {
156
+ return () => {
157
+ if (typingTimeoutRef.current) {
158
+ clearTimeout(typingTimeoutRef.current);
159
+ }
160
+ };
161
+ }, []);
162
+ const typingState = React.useMemo(
163
+ () => ({
164
+ participants,
165
+ lastUpdated: /* @__PURE__ */ new Date()
166
+ }),
167
+ [participants]
168
+ );
169
+ return {
170
+ typingState,
171
+ startTyping,
172
+ stopTyping,
173
+ setTypingParticipants: setParticipants
174
+ };
175
+ }
176
+ function useMessageScroll(options) {
177
+ const { messages, currentUserId, threshold = 100 } = options;
178
+ const scrollContainerRef = React.useRef(null);
179
+ const bottomRef = React.useRef(null);
180
+ const [isScrolledUp, setIsScrolledUp] = React.useState(false);
181
+ const prevMessageCountRef = React.useRef(messages.length);
182
+ React.useEffect(() => {
183
+ const container = scrollContainerRef.current;
184
+ if (!container) return;
185
+ const handleScroll = () => {
186
+ const { scrollTop, scrollHeight, clientHeight } = container;
187
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
188
+ setIsScrolledUp(!isAtBottom);
189
+ };
190
+ container.addEventListener("scroll", handleScroll);
191
+ return () => container.removeEventListener("scroll", handleScroll);
192
+ }, [threshold]);
193
+ const scrollToBottom = React.useCallback((smooth = true) => {
194
+ bottomRef.current?.scrollIntoView({
195
+ behavior: smooth ? "smooth" : "auto"
196
+ });
197
+ }, []);
198
+ React.useEffect(() => {
199
+ const messageCountChanged = messages.length !== prevMessageCountRef.current;
200
+ prevMessageCountRef.current = messages.length;
201
+ if (!messageCountChanged) return;
202
+ const lastMessage = messages[messages.length - 1];
203
+ const isOutgoing = lastMessage?.sender.id === currentUserId;
204
+ if (isOutgoing || !isScrolledUp) {
205
+ scrollToBottom(true);
206
+ }
207
+ }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
208
+ return {
209
+ scrollContainerRef,
210
+ bottomRef,
211
+ isScrolledUp,
212
+ scrollToBottom
213
+ };
214
+ }
215
+ function useReadReceipts(options) {
216
+ const { currentUserId, onMarkRead, threshold = 0.5 } = options;
217
+ const observerRef = React.useRef(null);
218
+ const observedMessagesRef = React.useRef(/* @__PURE__ */ new Set());
219
+ React.useEffect(() => {
220
+ observerRef.current = new IntersectionObserver(
221
+ (entries) => {
222
+ entries.forEach((entry) => {
223
+ if (entry.isIntersecting) {
224
+ const messageId = entry.target.getAttribute("data-message-id");
225
+ if (messageId && !observedMessagesRef.current.has(messageId)) {
226
+ observedMessagesRef.current.add(messageId);
227
+ onMarkRead?.(messageId);
228
+ }
229
+ }
230
+ });
231
+ },
232
+ { threshold }
233
+ );
234
+ return () => {
235
+ observerRef.current?.disconnect();
236
+ };
237
+ }, [onMarkRead, threshold]);
238
+ const observeMessage = React.useCallback(
239
+ (element, message) => {
240
+ if (!element || !observerRef.current) return;
241
+ if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
242
+ element.setAttribute("data-message-id", message.id);
243
+ observerRef.current.observe(element);
244
+ }
245
+ },
246
+ [currentUserId]
247
+ );
248
+ return { observeMessage };
249
+ }
250
+ function useTypingEmulation({
251
+ value,
252
+ onTypingStart,
253
+ onTypingStop
254
+ }) {
255
+ const [isTyping, setIsTyping] = React.useState(false);
256
+ const typingTimeoutRef = React.useRef(
257
+ null
258
+ );
259
+ React.useEffect(() => {
260
+ if (!onTypingStart && !onTypingStop) return;
261
+ if (value.length > 0 && !isTyping) {
262
+ setIsTyping(true);
263
+ onTypingStart?.();
264
+ }
265
+ if (typingTimeoutRef.current) {
266
+ clearTimeout(typingTimeoutRef.current);
267
+ }
268
+ typingTimeoutRef.current = setTimeout(() => {
269
+ if (isTyping) {
270
+ setIsTyping(false);
271
+ onTypingStop?.();
272
+ }
273
+ }, 2e3);
274
+ return () => {
275
+ if (typingTimeoutRef.current) {
276
+ clearTimeout(typingTimeoutRef.current);
277
+ }
278
+ };
279
+ }, [value, isTyping, onTypingStart, onTypingStop]);
280
+ const stopTyping = React.useCallback(() => {
281
+ setIsTyping(false);
282
+ onTypingStop?.();
283
+ }, [onTypingStop]);
284
+ return { stopTyping };
285
+ }
7
286
  var statusIconVariants = cva(
8
287
  "inline-flex items-center gap-0.5 text-current",
9
288
  {
@@ -384,7 +663,7 @@ function defaultFormatTimestamp(timestamp) {
384
663
  minute: "2-digit"
385
664
  });
386
665
  }
387
- var MessageBubble = React6.forwardRef(
666
+ var MessageBubble = React.forwardRef(
388
667
  ({
389
668
  className,
390
669
  message,
@@ -859,7 +1138,7 @@ function LoadMoreButton({
859
1138
  );
860
1139
  }
861
1140
  LoadMoreButton.displayName = "LoadMoreButton";
862
- var MessageList = React6.forwardRef(
1141
+ var MessageList = React.forwardRef(
863
1142
  ({
864
1143
  messages,
865
1144
  currentUser,
@@ -878,19 +1157,19 @@ var MessageList = React6.forwardRef(
878
1157
  className,
879
1158
  autoScroll = "onNewMessage"
880
1159
  }, ref) => {
881
- const scrollContainerRef = React6.useRef(null);
882
- const bottomRef = React6.useRef(null);
883
- const [isUserScrolled, setIsUserScrolled] = React6.useState(false);
884
- const prevMessageCountRef = React6.useRef(messages.length);
885
- React6.useImperativeHandle(ref, () => scrollContainerRef.current);
886
- const handleScroll = React6.useCallback(() => {
1160
+ const scrollContainerRef = React.useRef(null);
1161
+ const bottomRef = React.useRef(null);
1162
+ const [isUserScrolled, setIsUserScrolled] = React.useState(false);
1163
+ const prevMessageCountRef = React.useRef(messages.length);
1164
+ React.useImperativeHandle(ref, () => scrollContainerRef.current);
1165
+ const handleScroll = React.useCallback(() => {
887
1166
  const container = scrollContainerRef.current;
888
1167
  if (!container) return;
889
1168
  const { scrollTop, scrollHeight, clientHeight } = container;
890
1169
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
891
1170
  setIsUserScrolled(!isAtBottom);
892
1171
  }, []);
893
- React6.useEffect(() => {
1172
+ React.useEffect(() => {
894
1173
  const container = scrollContainerRef.current;
895
1174
  const bottom = bottomRef.current;
896
1175
  if (!container || !bottom) return;
@@ -906,7 +1185,7 @@ var MessageList = React6.forwardRef(
906
1185
  }
907
1186
  }
908
1187
  }, [messages, currentUser.id, autoScroll, isUserScrolled]);
909
- React6.useEffect(() => {
1188
+ React.useEffect(() => {
910
1189
  const bottom = bottomRef.current;
911
1190
  if (bottom && !isLoading) {
912
1191
  bottom.scrollIntoView();
@@ -959,7 +1238,7 @@ var MessageList = React6.forwardRef(
959
1238
  {
960
1239
  "data-slot": "message-list-content",
961
1240
  className: "flex flex-col gap-1 p-4",
962
- children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
1241
+ children: messageGroups.map((group) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
963
1242
  groupByDate && group.label && /* @__PURE__ */ jsx(DateSeparator, { label: group.label }),
964
1243
  group.messages.map((message, index) => {
965
1244
  const prevMessage = group.messages[index - 1];
@@ -1110,7 +1389,7 @@ var sendButtonVariants = cva(
1110
1389
  }
1111
1390
  }
1112
1391
  );
1113
- var SendButton = React6.forwardRef(
1392
+ var SendButton = React.forwardRef(
1114
1393
  ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
1115
1394
  return /* @__PURE__ */ jsx(
1116
1395
  "button",
@@ -1175,7 +1454,7 @@ var SendButton = React6.forwardRef(
1175
1454
  }
1176
1455
  );
1177
1456
  SendButton.displayName = "SendButton";
1178
- var MessageComposer = React6.forwardRef(
1457
+ var MessageComposer = React.forwardRef(
1179
1458
  ({
1180
1459
  onSend,
1181
1460
  onTypingStart,
@@ -1189,8 +1468,8 @@ var MessageComposer = React6.forwardRef(
1189
1468
  isSending = false,
1190
1469
  showAttachmentPicker = true,
1191
1470
  showCameraButton = false,
1192
- acceptedFileTypes = ["image/*", "video/*", ".pdf", ".doc", ".docx"],
1193
- maxFileSize = 25 * 1024 * 1024,
1471
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
1472
+ maxFileSize = DEFAULT_MAX_FILE_SIZE,
1194
1473
  maxAttachments = 10,
1195
1474
  onError,
1196
1475
  autoFocus = false,
@@ -1201,11 +1480,11 @@ var MessageComposer = React6.forwardRef(
1201
1480
  mentionOptions,
1202
1481
  className
1203
1482
  }, ref) => {
1204
- const textareaRef = React6.useRef(null);
1205
- const [internalContent, setInternalContent] = React6.useState("");
1483
+ const textareaRef = React.useRef(null);
1484
+ const [internalContent, setInternalContent] = React.useState("");
1206
1485
  const isControlled = controlledValue !== void 0;
1207
1486
  const content = isControlled ? controlledValue : internalContent;
1208
- const setContent = React6.useCallback(
1487
+ const setContent = React.useCallback(
1209
1488
  (val) => {
1210
1489
  if (isControlled) {
1211
1490
  const newVal = typeof val === "function" ? val(controlledValue) : val;
@@ -1216,20 +1495,20 @@ var MessageComposer = React6.forwardRef(
1216
1495
  },
1217
1496
  [isControlled, controlledValue, onValueChange]
1218
1497
  );
1219
- const [attachments, setAttachments] = React6.useState(
1498
+ const [attachments, setAttachments] = React.useState(
1220
1499
  []
1221
1500
  );
1222
- const [isTyping, setIsTyping] = React6.useState(false);
1223
- const typingTimeoutRef = React6.useRef(null);
1224
- React6.useImperativeHandle(ref, () => textareaRef.current);
1225
- React6.useEffect(() => {
1501
+ const [isTyping, setIsTyping] = React.useState(false);
1502
+ const typingTimeoutRef = React.useRef(null);
1503
+ React.useImperativeHandle(ref, () => textareaRef.current);
1504
+ React.useEffect(() => {
1226
1505
  const textarea = textareaRef.current;
1227
1506
  if (textarea) {
1228
1507
  textarea.style.height = "auto";
1229
1508
  textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
1230
1509
  }
1231
1510
  }, [content]);
1232
- React6.useEffect(() => {
1511
+ React.useEffect(() => {
1233
1512
  if (content.length > 0 && !isTyping) {
1234
1513
  setIsTyping(true);
1235
1514
  onTypingStart?.();
@@ -1249,13 +1528,13 @@ var MessageComposer = React6.forwardRef(
1249
1528
  }
1250
1529
  };
1251
1530
  }, [content, isTyping, onTypingStart, onTypingStop]);
1252
- React6.useEffect(() => {
1531
+ React.useEffect(() => {
1253
1532
  if (autoFocus) {
1254
1533
  textareaRef.current?.focus();
1255
1534
  }
1256
1535
  }, [autoFocus]);
1257
1536
  const replyToId = replyTo?.id;
1258
- React6.useEffect(() => {
1537
+ React.useEffect(() => {
1259
1538
  if (replyToId !== void 0) {
1260
1539
  textareaRef.current?.focus();
1261
1540
  }
@@ -1333,7 +1612,7 @@ var MessageComposer = React6.forwardRef(
1333
1612
  return prev.filter((a) => a.id !== attachmentId);
1334
1613
  });
1335
1614
  };
1336
- React6.useEffect(() => {
1615
+ React.useEffect(() => {
1337
1616
  const currentAttachments = attachments;
1338
1617
  return () => {
1339
1618
  currentAttachments.forEach((a) => {
@@ -1603,7 +1882,7 @@ function formatLastSeen(date) {
1603
1882
  if (diffDays < 7) return `${diffDays}d ago`;
1604
1883
  return date.toLocaleDateString();
1605
1884
  }
1606
- var ConversationHeader = React6.forwardRef(
1885
+ var ConversationHeader = React.forwardRef(
1607
1886
  ({
1608
1887
  className,
1609
1888
  size,
@@ -1725,7 +2004,7 @@ var ConversationHeader = React6.forwardRef(
1725
2004
  }
1726
2005
  );
1727
2006
  ConversationHeader.displayName = "ConversationHeader";
1728
- var ConversationListItem = React6.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
2007
+ var ConversationListItem = React.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
1729
2008
  const participant = conversation.participants.find((p) => !p.isCurrentUser);
1730
2009
  const title = getConversationTitle(conversation, participant);
1731
2010
  const avatarUrl = conversation.avatarUrl || participant?.avatarUrl;
@@ -1933,7 +2212,7 @@ function ConversationListSkeleton({
1933
2212
  }
1934
2213
  ConversationListSkeleton.displayName = "ConversationListSkeleton";
1935
2214
  function LightboxModal({ attachment, onClose }) {
1936
- React6.useEffect(() => {
2215
+ React.useEffect(() => {
1937
2216
  const handleKeyDown = (event) => {
1938
2217
  if (event.key === "Escape") {
1939
2218
  onClose();
@@ -2030,7 +2309,7 @@ function LightboxModal({ attachment, onClose }) {
2030
2309
  );
2031
2310
  }
2032
2311
  LightboxModal.displayName = "LightboxModal";
2033
- var MessageThread = React6.forwardRef(
2312
+ var MessageThread = React.forwardRef(
2034
2313
  ({
2035
2314
  conversation,
2036
2315
  messages,
@@ -2061,8 +2340,20 @@ var MessageThread = React6.forwardRef(
2061
2340
  onError,
2062
2341
  className
2063
2342
  }, ref) => {
2064
- const [lightboxAttachment, setLightboxAttachment] = React6.useState(null);
2065
- const [replyTo, setReplyTo] = React6.useState(null);
2343
+ const [lightboxAttachment, setLightboxAttachment] = React.useState(null);
2344
+ const [replyTo, setReplyTo] = React.useState(null);
2345
+ const composerRef = React.useRef(null);
2346
+ const [draft, setDraft] = React.useState("");
2347
+ const draftEpochRef = React.useRef(0);
2348
+ const handleDraftChange = React.useCallback((value) => {
2349
+ draftEpochRef.current += 1;
2350
+ setDraft(value);
2351
+ }, []);
2352
+ const { stopTyping } = useTypingEmulation({
2353
+ value: draft,
2354
+ onTypingStart: eventHandlers.onTypingStart,
2355
+ onTypingStop: eventHandlers.onTypingStop
2356
+ });
2066
2357
  const participant = conversation?.type === "direct" ? conversation.participants.find((p) => p.id !== currentUser.id) : void 0;
2067
2358
  const handleAttachmentClick = (attachment, message) => {
2068
2359
  if (attachment.type === "image" || attachment.type === "video") {
@@ -2071,12 +2362,21 @@ var MessageThread = React6.forwardRef(
2071
2362
  eventHandlers.onAttachmentClick?.(attachment, message);
2072
2363
  };
2073
2364
  const handleSendMessage = async (newMessage) => {
2365
+ stopTyping();
2074
2366
  const messageWithReply = {
2075
2367
  ...newMessage,
2076
2368
  replyToId: replyTo?.id || newMessage.replyToId
2077
2369
  };
2078
2370
  setReplyTo(null);
2079
- await eventHandlers.onSendMessage?.(messageWithReply);
2371
+ const epoch = draftEpochRef.current;
2372
+ try {
2373
+ await Promise.resolve(eventHandlers.onSendMessage?.(messageWithReply));
2374
+ } catch (error) {
2375
+ if (draftEpochRef.current === epoch) {
2376
+ setDraft(newMessage.content);
2377
+ }
2378
+ throw error;
2379
+ }
2080
2380
  };
2081
2381
  return /* @__PURE__ */ jsxs(
2082
2382
  "div",
@@ -2102,9 +2402,11 @@ var MessageThread = React6.forwardRef(
2102
2402
  /* @__PURE__ */ jsx(
2103
2403
  DragDropZone,
2104
2404
  {
2105
- onFilesDropped: () => {
2106
- onError?.("Drop files on the composer to attach them");
2405
+ onFilesDropped: (files) => {
2406
+ composerRef.current?.addFiles(files);
2107
2407
  },
2408
+ maxFiles: maxAttachments,
2409
+ onError,
2108
2410
  disabled: !showAttachmentPicker,
2109
2411
  className: "flex-1 overflow-hidden",
2110
2412
  children: /* @__PURE__ */ jsx(
@@ -2130,23 +2432,38 @@ var MessageThread = React6.forwardRef(
2130
2432
  }
2131
2433
  ),
2132
2434
  /* @__PURE__ */ jsx(
2133
- MessageComposer,
2435
+ "div",
2134
2436
  {
2135
- onSend: handleSendMessage,
2136
- onTypingStart: eventHandlers.onTypingStart,
2137
- onTypingStop: eventHandlers.onTypingStop,
2138
- placeholder,
2139
- maxLength: maxMessageLength,
2140
- showCharacterCount,
2141
- isSending,
2142
- showAttachmentPicker,
2143
- showCameraButton,
2144
- acceptedFileTypes,
2145
- maxFileSize,
2146
- maxAttachments,
2147
- onError,
2148
- replyTo,
2149
- onCancelReply: () => setReplyTo(null)
2437
+ "data-slot": "message-thread-composer",
2438
+ className: "shrink-0 border-t border-neutral-200 p-3 dark:border-neutral-700",
2439
+ children: /* @__PURE__ */ jsx(
2440
+ ChatComposer,
2441
+ {
2442
+ ref: composerRef,
2443
+ value: draft,
2444
+ onValueChange: handleDraftChange,
2445
+ onSend: handleSendMessage,
2446
+ placeholder,
2447
+ maxLength: maxMessageLength,
2448
+ showCharacterCount,
2449
+ isSending,
2450
+ allowAttachments: showAttachmentPicker,
2451
+ acceptedFileTypes: acceptedFileTypes ?? DEFAULT_ACCEPTED_FILE_TYPES,
2452
+ maxFileSize: maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
2453
+ maxAttachments,
2454
+ onError,
2455
+ replyTo,
2456
+ onCancelReply: () => setReplyTo(null),
2457
+ inputLabel: "Message",
2458
+ micSlot: showCameraButton ? /* @__PURE__ */ jsx(
2459
+ CameraButton,
2460
+ {
2461
+ onCapture: (file) => composerRef.current?.addFiles([file]),
2462
+ disabled: isSending
2463
+ }
2464
+ ) : void 0
2465
+ }
2466
+ )
2150
2467
  }
2151
2468
  ),
2152
2469
  /* @__PURE__ */ jsx(
@@ -2226,250 +2543,7 @@ function MessagingSplitView({
2226
2543
  );
2227
2544
  }
2228
2545
  MessagingSplitView.displayName = "MessagingSplitView";
2229
- function useMessages(options) {
2230
- const {
2231
- initialMessages = [],
2232
- currentUser,
2233
- onSend,
2234
- onRetry,
2235
- onLoadMore
2236
- } = options;
2237
- const [messages, setMessages] = React6.useState(initialMessages);
2238
- const [isSending, setIsSending] = React6.useState(false);
2239
- const [isLoadingMore, setIsLoadingMore] = React6.useState(false);
2240
- React6.useEffect(() => {
2241
- setMessages(initialMessages);
2242
- }, [initialMessages]);
2243
- const addMessage = React6.useCallback((message) => {
2244
- setMessages((prev) => {
2245
- if (prev.some((m) => m.id === message.id)) {
2246
- return prev;
2247
- }
2248
- return [...prev, message];
2249
- });
2250
- }, []);
2251
- const updateMessage = React6.useCallback(
2252
- (messageId, updates) => {
2253
- setMessages(
2254
- (prev) => prev.map((m) => m.id === messageId ? { ...m, ...updates } : m)
2255
- );
2256
- },
2257
- []
2258
- );
2259
- const removeMessage = React6.useCallback((messageId) => {
2260
- setMessages((prev) => prev.filter((m) => m.id !== messageId));
2261
- }, []);
2262
- const updateStatus = React6.useCallback(
2263
- (messageId, status) => {
2264
- updateMessage(messageId, { status });
2265
- },
2266
- [updateMessage]
2267
- );
2268
- const markAsRead = React6.useCallback(
2269
- (messageId) => {
2270
- updateStatus(messageId, "read");
2271
- },
2272
- [updateStatus]
2273
- );
2274
- const sendMessage = React6.useCallback(
2275
- async (newMessage) => {
2276
- const optimisticId = `optimistic-${Date.now()}`;
2277
- const optimisticMessage = {
2278
- id: optimisticId,
2279
- type: "text",
2280
- content: newMessage.content,
2281
- sender: currentUser,
2282
- timestamp: /* @__PURE__ */ new Date(),
2283
- status: "sending",
2284
- attachments: []
2285
- // Would handle attachment uploads here
2286
- };
2287
- addMessage(optimisticMessage);
2288
- setIsSending(true);
2289
- try {
2290
- if (onSend) {
2291
- const sentMessage = await onSend(newMessage);
2292
- setMessages(
2293
- (prev) => prev.map((m) => m.id === optimisticId ? sentMessage : m)
2294
- );
2295
- } else {
2296
- updateStatus(optimisticId, "sent");
2297
- }
2298
- } catch {
2299
- updateStatus(optimisticId, "failed");
2300
- } finally {
2301
- setIsSending(false);
2302
- }
2303
- },
2304
- [currentUser, onSend, addMessage, updateStatus]
2305
- );
2306
- const retryMessage = React6.useCallback(
2307
- async (messageId) => {
2308
- updateStatus(messageId, "sending");
2309
- try {
2310
- if (onRetry) {
2311
- await onRetry(messageId);
2312
- updateStatus(messageId, "sent");
2313
- }
2314
- } catch {
2315
- updateStatus(messageId, "failed");
2316
- }
2317
- },
2318
- [onRetry, updateStatus]
2319
- );
2320
- const loadMore = React6.useCallback(async () => {
2321
- if (isLoadingMore || !onLoadMore) return;
2322
- setIsLoadingMore(true);
2323
- try {
2324
- const olderMessages = await onLoadMore();
2325
- setMessages((prev) => [...olderMessages, ...prev]);
2326
- } finally {
2327
- setIsLoadingMore(false);
2328
- }
2329
- }, [isLoadingMore, onLoadMore]);
2330
- return {
2331
- messages,
2332
- addMessage,
2333
- updateMessage,
2334
- removeMessage,
2335
- sendMessage,
2336
- retryMessage,
2337
- loadMore,
2338
- isSending,
2339
- isLoadingMore,
2340
- markAsRead,
2341
- updateStatus
2342
- };
2343
- }
2344
- function useTypingIndicator(options = {}) {
2345
- const {
2346
- typingParticipants: initialParticipants = [],
2347
- debounceTime = 2e3,
2348
- onTypingStart,
2349
- onTypingStop
2350
- } = options;
2351
- const [participants, setParticipants] = React6.useState(initialParticipants);
2352
- const [isLocalTyping, setIsLocalTyping] = React6.useState(false);
2353
- const typingTimeoutRef = React6.useRef(null);
2354
- React6.useEffect(() => {
2355
- setParticipants(initialParticipants);
2356
- }, [initialParticipants]);
2357
- const startTyping = React6.useCallback(() => {
2358
- if (!isLocalTyping) {
2359
- setIsLocalTyping(true);
2360
- onTypingStart?.();
2361
- }
2362
- if (typingTimeoutRef.current) {
2363
- clearTimeout(typingTimeoutRef.current);
2364
- }
2365
- typingTimeoutRef.current = setTimeout(() => {
2366
- setIsLocalTyping(false);
2367
- onTypingStop?.();
2368
- }, debounceTime);
2369
- }, [isLocalTyping, debounceTime, onTypingStart, onTypingStop]);
2370
- const stopTyping = React6.useCallback(() => {
2371
- if (typingTimeoutRef.current) {
2372
- clearTimeout(typingTimeoutRef.current);
2373
- }
2374
- setIsLocalTyping(false);
2375
- onTypingStop?.();
2376
- }, [onTypingStop]);
2377
- React6.useEffect(() => {
2378
- return () => {
2379
- if (typingTimeoutRef.current) {
2380
- clearTimeout(typingTimeoutRef.current);
2381
- }
2382
- };
2383
- }, []);
2384
- const typingState = React6.useMemo(
2385
- () => ({
2386
- participants,
2387
- lastUpdated: /* @__PURE__ */ new Date()
2388
- }),
2389
- [participants]
2390
- );
2391
- return {
2392
- typingState,
2393
- startTyping,
2394
- stopTyping,
2395
- setTypingParticipants: setParticipants
2396
- };
2397
- }
2398
- function useMessageScroll(options) {
2399
- const { messages, currentUserId, threshold = 100 } = options;
2400
- const scrollContainerRef = React6.useRef(null);
2401
- const bottomRef = React6.useRef(null);
2402
- const [isScrolledUp, setIsScrolledUp] = React6.useState(false);
2403
- const prevMessageCountRef = React6.useRef(messages.length);
2404
- React6.useEffect(() => {
2405
- const container = scrollContainerRef.current;
2406
- if (!container) return;
2407
- const handleScroll = () => {
2408
- const { scrollTop, scrollHeight, clientHeight } = container;
2409
- const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold;
2410
- setIsScrolledUp(!isAtBottom);
2411
- };
2412
- container.addEventListener("scroll", handleScroll);
2413
- return () => container.removeEventListener("scroll", handleScroll);
2414
- }, [threshold]);
2415
- const scrollToBottom = React6.useCallback((smooth = true) => {
2416
- bottomRef.current?.scrollIntoView({
2417
- behavior: smooth ? "smooth" : "auto"
2418
- });
2419
- }, []);
2420
- React6.useEffect(() => {
2421
- const messageCountChanged = messages.length !== prevMessageCountRef.current;
2422
- prevMessageCountRef.current = messages.length;
2423
- if (!messageCountChanged) return;
2424
- const lastMessage = messages[messages.length - 1];
2425
- const isOutgoing = lastMessage?.sender.id === currentUserId;
2426
- if (isOutgoing || !isScrolledUp) {
2427
- scrollToBottom(true);
2428
- }
2429
- }, [messages, currentUserId, isScrolledUp, scrollToBottom]);
2430
- return {
2431
- scrollContainerRef,
2432
- bottomRef,
2433
- isScrolledUp,
2434
- scrollToBottom
2435
- };
2436
- }
2437
- function useReadReceipts(options) {
2438
- const { currentUserId, onMarkRead, threshold = 0.5 } = options;
2439
- const observerRef = React6.useRef(null);
2440
- const observedMessagesRef = React6.useRef(/* @__PURE__ */ new Set());
2441
- React6.useEffect(() => {
2442
- observerRef.current = new IntersectionObserver(
2443
- (entries) => {
2444
- entries.forEach((entry) => {
2445
- if (entry.isIntersecting) {
2446
- const messageId = entry.target.getAttribute("data-message-id");
2447
- if (messageId && !observedMessagesRef.current.has(messageId)) {
2448
- observedMessagesRef.current.add(messageId);
2449
- onMarkRead?.(messageId);
2450
- }
2451
- }
2452
- });
2453
- },
2454
- { threshold }
2455
- );
2456
- return () => {
2457
- observerRef.current?.disconnect();
2458
- };
2459
- }, [onMarkRead, threshold]);
2460
- const observeMessage = React6.useCallback(
2461
- (element, message) => {
2462
- if (!element || !observerRef.current) return;
2463
- if (message.sender.id !== currentUserId && message.status !== "read" && !observedMessagesRef.current.has(message.id)) {
2464
- element.setAttribute("data-message-id", message.id);
2465
- observerRef.current.observe(element);
2466
- }
2467
- },
2468
- [currentUserId]
2469
- );
2470
- return { observeMessage };
2471
- }
2472
2546
 
2473
- export { AttachmentPreview, CharacterCounter, ConversationHeader, ConversationListItem, ConversationListSkeleton, DateSeparator, EmptyState, LightboxModal, LoadMoreButton, MessageBubble, MessageComposer, MessageList, MessageStatusIcon, MessageThread, MessagingSplitView, ReadReceiptIndicator, SendButton, SkeletonMessage, TypingIndicator, bubbleVariants, formatDateLabel, formatFileSize, formatLastSeen, getConversationSubtitle, getConversationTitle, groupMessagesByDate, headerVariants, isSameSenderGroup, sendButtonVariants, useMessageScroll, useMessages, useReadReceipts, useTypingIndicator };
2474
- //# sourceMappingURL=chunk-PL6TFCYR.js.map
2475
- //# sourceMappingURL=chunk-PL6TFCYR.js.map
2547
+ export { AttachmentPreview, CharacterCounter, ConversationHeader, ConversationListItem, ConversationListSkeleton, DateSeparator, EmptyState, LightboxModal, LoadMoreButton, MessageBubble, MessageComposer, MessageList, MessageStatusIcon, MessageThread, MessagingSplitView, ReadReceiptIndicator, SendButton, SkeletonMessage, TypingIndicator, bubbleVariants, formatDateLabel, formatFileSize, formatLastSeen, getConversationSubtitle, getConversationTitle, groupMessagesByDate, headerVariants, isSameSenderGroup, sendButtonVariants, useMessageScroll, useMessages, useReadReceipts, useTypingEmulation, useTypingIndicator };
2548
+ //# sourceMappingURL=chunk-NORSXAXL.js.map
2549
+ //# sourceMappingURL=chunk-NORSXAXL.js.map