@mieweb/ui 0.9.0-dev.5 → 0.9.0-dev.7

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 (37) hide show
  1. package/MIGRATION.md +112 -0
  2. package/dist/{chunk-QWJGEDHV.js → chunk-22CYDCXA.js} +13 -1183
  3. package/dist/chunk-22CYDCXA.js.map +1 -0
  4. package/dist/chunk-IIFQ6BSJ.js +1888 -0
  5. package/dist/chunk-IIFQ6BSJ.js.map +1 -0
  6. package/dist/{chunk-ZXRVVNT5.cjs → chunk-OFUKTVBN.cjs} +367 -798
  7. package/dist/chunk-OFUKTVBN.cjs.map +1 -0
  8. package/dist/chunk-OYQ7GYAA.cjs +1921 -0
  9. package/dist/chunk-OYQ7GYAA.cjs.map +1 -0
  10. package/dist/{chunk-PL6TFCYR.js → chunk-VSVGMMNO.js} +365 -793
  11. package/dist/chunk-VSVGMMNO.js.map +1 -0
  12. package/dist/{chunk-XJUIZA4G.cjs → chunk-WIAQQNFU.cjs} +12 -1184
  13. package/dist/chunk-WIAQQNFU.cjs.map +1 -0
  14. package/dist/components/SuperChat/index.cjs +11 -8
  15. package/dist/components/SuperChat/index.cjs.map +1 -1
  16. package/dist/components/SuperChat/index.js +5 -2
  17. package/dist/components/SuperChat/index.js.map +1 -1
  18. package/dist/components/SuperChat/plugins/index.cjs +10 -3
  19. package/dist/components/SuperChat/plugins/index.cjs.map +1 -1
  20. package/dist/components/SuperChat/plugins/index.js +9 -2
  21. package/dist/components/SuperChat/plugins/index.js.map +1 -1
  22. package/dist/index.cjs +91 -132
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +38 -98
  25. package/dist/index.d.ts +38 -98
  26. package/dist/index.js +20 -48
  27. package/dist/index.js.map +1 -1
  28. package/dist/styles.css +1 -1
  29. package/package.json +2 -1
  30. package/dist/chunk-H3GRO43Z.cjs +0 -735
  31. package/dist/chunk-H3GRO43Z.cjs.map +0 -1
  32. package/dist/chunk-MD3ZY4UO.js +0 -705
  33. package/dist/chunk-MD3ZY4UO.js.map +0 -1
  34. package/dist/chunk-PL6TFCYR.js.map +0 -1
  35. package/dist/chunk-QWJGEDHV.js.map +0 -1
  36. package/dist/chunk-XJUIZA4G.cjs.map +0 -1
  37. 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 { notifyComposerMigrationOnce, DragDropZone, ChatComposer, DEFAULT_MAX_FILE_SIZE, DEFAULT_ACCEPTED_FILE_TYPES, CameraButton } from './chunk-IIFQ6BSJ.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];
@@ -1039,516 +1318,11 @@ var MessageList = React6.forwardRef(
1039
1318
  }
1040
1319
  );
1041
1320
  MessageList.displayName = "MessageList";
1042
- function isFileAccepted(file, accepted) {
1043
- if (!accepted || accepted.length === 0) return true;
1044
- const mime = file.type;
1045
- const name = file.name.toLowerCase();
1046
- return accepted.some((token) => {
1047
- if (token.startsWith(".")) return name.endsWith(token.toLowerCase());
1048
- if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
1049
- if (token.includes("/")) return mime === token;
1050
- return false;
1051
- });
1052
- }
1053
- function CharacterCounter({
1054
- current,
1055
- max,
1056
- showWarningAt = 0.9,
1057
- className
1058
- }) {
1059
- const percentage = current / max;
1060
- const isWarning = percentage >= showWarningAt && percentage < 1;
1061
- const isOver = current > max;
1062
- return /* @__PURE__ */ jsxs(
1063
- "span",
1064
- {
1065
- className: cn(
1066
- "text-xs tabular-nums",
1067
- 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",
1068
- className
1069
- ),
1070
- "aria-live": "polite",
1071
- "aria-label": `${current} of ${max} characters`,
1072
- children: [
1073
- current,
1074
- "/",
1075
- max
1076
- ]
1077
- }
1078
- );
1079
- }
1080
- CharacterCounter.displayName = "CharacterCounter";
1081
- var sendButtonVariants = cva(
1321
+ var headerVariants = cva(
1082
1322
  [
1083
- "inline-flex items-center justify-center",
1084
- "rounded-full p-3 self-start",
1085
- "transition-all duration-200",
1086
- "focus:outline-none focus:ring-2 focus:ring-primary-500",
1087
- "disabled:opacity-50 disabled:cursor-not-allowed"
1088
- ],
1089
- {
1090
- variants: {
1091
- variant: {
1092
- primary: [
1093
- "bg-primary-800 text-white",
1094
- "hover:bg-primary-900",
1095
- "active:scale-95"
1096
- ],
1097
- subtle: [
1098
- "bg-transparent text-primary-800",
1099
- "hover:bg-primary-50 dark:hover:bg-primary-900/20"
1100
- ]
1101
- },
1102
- canSend: {
1103
- true: "",
1104
- false: "opacity-50 cursor-not-allowed"
1105
- }
1106
- },
1107
- defaultVariants: {
1108
- variant: "primary",
1109
- canSend: false
1110
- }
1111
- }
1112
- );
1113
- var SendButton = React6.forwardRef(
1114
- ({ className, variant, canSend, isLoading, disabled, ...props }, ref) => {
1115
- return /* @__PURE__ */ jsx(
1116
- "button",
1117
- {
1118
- ref,
1119
- type: "submit",
1120
- disabled: disabled || !canSend || isLoading,
1121
- "data-slot": "composer-send-button",
1122
- className: cn(sendButtonVariants({ variant, canSend }), className),
1123
- "aria-label": isLoading ? "Sending message" : "Send message",
1124
- ...props,
1125
- children: isLoading ? /* @__PURE__ */ jsxs(
1126
- "svg",
1127
- {
1128
- "aria-hidden": "true",
1129
- className: "h-5 w-5 animate-spin",
1130
- fill: "none",
1131
- viewBox: "0 0 24 24",
1132
- children: [
1133
- /* @__PURE__ */ jsx(
1134
- "circle",
1135
- {
1136
- className: "opacity-25",
1137
- cx: "12",
1138
- cy: "12",
1139
- r: "10",
1140
- stroke: "currentColor",
1141
- strokeWidth: "4"
1142
- }
1143
- ),
1144
- /* @__PURE__ */ jsx(
1145
- "path",
1146
- {
1147
- className: "opacity-75",
1148
- fill: "currentColor",
1149
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
1150
- }
1151
- )
1152
- ]
1153
- }
1154
- ) : /* @__PURE__ */ jsx(
1155
- "svg",
1156
- {
1157
- "aria-hidden": "true",
1158
- className: "h-5 w-5",
1159
- fill: "none",
1160
- viewBox: "0 0 24 24",
1161
- stroke: "currentColor",
1162
- children: /* @__PURE__ */ jsx(
1163
- "path",
1164
- {
1165
- strokeLinecap: "round",
1166
- strokeLinejoin: "round",
1167
- strokeWidth: 2,
1168
- d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
1169
- }
1170
- )
1171
- }
1172
- )
1173
- }
1174
- );
1175
- }
1176
- );
1177
- SendButton.displayName = "SendButton";
1178
- var MessageComposer = React6.forwardRef(
1179
- ({
1180
- onSend,
1181
- onTypingStart,
1182
- onTypingStop,
1183
- value: controlledValue,
1184
- onValueChange,
1185
- placeholder = "Type a message...",
1186
- maxLength = 1600,
1187
- showCharacterCount = false,
1188
- disabled = false,
1189
- isSending = false,
1190
- showAttachmentPicker = true,
1191
- showCameraButton = false,
1192
- acceptedFileTypes = ["image/*", "video/*", ".pdf", ".doc", ".docx"],
1193
- maxFileSize = 25 * 1024 * 1024,
1194
- maxAttachments = 10,
1195
- onError,
1196
- autoFocus = false,
1197
- replyTo = null,
1198
- onCancelReply,
1199
- variant = "default",
1200
- inputTrailing,
1201
- mentionOptions,
1202
- className
1203
- }, ref) => {
1204
- const textareaRef = React6.useRef(null);
1205
- const [internalContent, setInternalContent] = React6.useState("");
1206
- const isControlled = controlledValue !== void 0;
1207
- const content = isControlled ? controlledValue : internalContent;
1208
- const setContent = React6.useCallback(
1209
- (val) => {
1210
- if (isControlled) {
1211
- const newVal = typeof val === "function" ? val(controlledValue) : val;
1212
- onValueChange?.(newVal);
1213
- } else {
1214
- setInternalContent(val);
1215
- }
1216
- },
1217
- [isControlled, controlledValue, onValueChange]
1218
- );
1219
- const [attachments, setAttachments] = React6.useState(
1220
- []
1221
- );
1222
- const [isTyping, setIsTyping] = React6.useState(false);
1223
- const typingTimeoutRef = React6.useRef(null);
1224
- React6.useImperativeHandle(ref, () => textareaRef.current);
1225
- React6.useEffect(() => {
1226
- const textarea = textareaRef.current;
1227
- if (textarea) {
1228
- textarea.style.height = "auto";
1229
- textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`;
1230
- }
1231
- }, [content]);
1232
- React6.useEffect(() => {
1233
- if (content.length > 0 && !isTyping) {
1234
- setIsTyping(true);
1235
- onTypingStart?.();
1236
- }
1237
- if (typingTimeoutRef.current) {
1238
- clearTimeout(typingTimeoutRef.current);
1239
- }
1240
- typingTimeoutRef.current = setTimeout(() => {
1241
- if (isTyping) {
1242
- setIsTyping(false);
1243
- onTypingStop?.();
1244
- }
1245
- }, 2e3);
1246
- return () => {
1247
- if (typingTimeoutRef.current) {
1248
- clearTimeout(typingTimeoutRef.current);
1249
- }
1250
- };
1251
- }, [content, isTyping, onTypingStart, onTypingStop]);
1252
- React6.useEffect(() => {
1253
- if (autoFocus) {
1254
- textareaRef.current?.focus();
1255
- }
1256
- }, [autoFocus]);
1257
- const replyToId = replyTo?.id;
1258
- React6.useEffect(() => {
1259
- if (replyToId !== void 0) {
1260
- textareaRef.current?.focus();
1261
- }
1262
- }, [replyToId]);
1263
- const mention = useMentionAutocomplete({
1264
- options: mentionOptions,
1265
- value: content,
1266
- setValue: setContent,
1267
- textareaRef
1268
- });
1269
- const canSend = (content.trim().length > 0 || attachments.length > 0) && content.length <= maxLength && !disabled && !isSending;
1270
- const handleSubmit = async (event) => {
1271
- event.preventDefault();
1272
- if (!canSend) return;
1273
- const message = {
1274
- content: content.trim(),
1275
- attachments: attachments.map((a) => a.file),
1276
- replyToId: replyTo?.id
1277
- };
1278
- setContent("");
1279
- setAttachments([]);
1280
- setIsTyping(false);
1281
- onTypingStop?.();
1282
- try {
1283
- await onSend(message);
1284
- } catch {
1285
- setContent(message.content);
1286
- onError?.("Failed to send message");
1287
- }
1288
- };
1289
- const handleKeyDown = (event) => {
1290
- if (mention.handleKeyDown(event)) return;
1291
- if (event.key === "Enter" && !event.shiftKey) {
1292
- event.preventDefault();
1293
- if (canSend) {
1294
- handleSubmit(event);
1295
- }
1296
- }
1297
- };
1298
- const handleFilesSelected = (files) => {
1299
- const remainingSlots = maxAttachments - attachments.length;
1300
- const filesToAdd = files.slice(0, remainingSlots);
1301
- if (files.length > remainingSlots) {
1302
- onError?.(`Maximum ${maxAttachments} attachments allowed`);
1303
- }
1304
- const newAttachments = filesToAdd.map((file) => {
1305
- const type = getFileType(file.type);
1306
- let previewUrl;
1307
- if (type === "image" || type === "video") {
1308
- previewUrl = URL.createObjectURL(file);
1309
- }
1310
- return {
1311
- id: generateAttachmentId(),
1312
- file,
1313
- previewUrl,
1314
- type,
1315
- state: "pending"
1316
- };
1317
- });
1318
- setAttachments((prev) => [...prev, ...newAttachments]);
1319
- };
1320
- const handlePaste = (event) => {
1321
- if (!showAttachmentPicker || disabled) return;
1322
- 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));
1323
- if (pasted.length === 0) return;
1324
- event.preventDefault();
1325
- handleFilesSelected(pasted);
1326
- };
1327
- const handleRemoveAttachment = (attachmentId) => {
1328
- setAttachments((prev) => {
1329
- const attachment = prev.find((a) => a.id === attachmentId);
1330
- if (attachment?.previewUrl) {
1331
- URL.revokeObjectURL(attachment.previewUrl);
1332
- }
1333
- return prev.filter((a) => a.id !== attachmentId);
1334
- });
1335
- };
1336
- React6.useEffect(() => {
1337
- const currentAttachments = attachments;
1338
- return () => {
1339
- currentAttachments.forEach((a) => {
1340
- if (a.previewUrl) {
1341
- URL.revokeObjectURL(a.previewUrl);
1342
- }
1343
- });
1344
- };
1345
- }, [attachments]);
1346
- return /* @__PURE__ */ jsx(
1347
- DragDropZone,
1348
- {
1349
- onFilesDropped: handleFilesSelected,
1350
- acceptedTypes: acceptedFileTypes,
1351
- maxFileSize,
1352
- maxFiles: maxAttachments - attachments.length,
1353
- disabled: disabled || attachments.length >= maxAttachments,
1354
- onError,
1355
- className: cn("w-full", className),
1356
- children: /* @__PURE__ */ jsxs(
1357
- "form",
1358
- {
1359
- onSubmit: handleSubmit,
1360
- "data-slot": "message-composer",
1361
- className: "w-full",
1362
- children: [
1363
- replyTo && /* @__PURE__ */ jsxs(
1364
- "div",
1365
- {
1366
- "data-slot": "composer-reply-preview",
1367
- className: cn(
1368
- "flex items-center gap-2 px-4 py-2",
1369
- "bg-neutral-50 dark:bg-neutral-800/50",
1370
- "border-primary-500 border-s-4"
1371
- ),
1372
- children: [
1373
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1374
- /* @__PURE__ */ jsxs("span", { className: "text-primary-800 dark:text-primary-400 text-xs font-medium", children: [
1375
- "Replying to ",
1376
- replyTo.senderName
1377
- ] }),
1378
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-neutral-600 dark:text-neutral-300", children: replyTo.content })
1379
- ] }),
1380
- /* @__PURE__ */ jsx(
1381
- "button",
1382
- {
1383
- type: "button",
1384
- onClick: onCancelReply,
1385
- className: cn(
1386
- "shrink-0 rounded p-1",
1387
- "text-neutral-500 hover:text-neutral-700",
1388
- "dark:text-neutral-400 dark:hover:text-neutral-200",
1389
- "focus:ring-primary-500 focus:ring-2 focus:outline-none"
1390
- ),
1391
- "aria-label": "Cancel reply",
1392
- children: /* @__PURE__ */ jsx(
1393
- "svg",
1394
- {
1395
- "aria-hidden": "true",
1396
- className: "h-4 w-4",
1397
- fill: "none",
1398
- viewBox: "0 0 24 24",
1399
- stroke: "currentColor",
1400
- children: /* @__PURE__ */ jsx(
1401
- "path",
1402
- {
1403
- strokeLinecap: "round",
1404
- strokeLinejoin: "round",
1405
- strokeWidth: 2,
1406
- d: "M6 18L18 6M6 6l12 12"
1407
- }
1408
- )
1409
- }
1410
- )
1411
- }
1412
- )
1413
- ]
1414
- }
1415
- ),
1416
- attachments.length > 0 && /* @__PURE__ */ jsx(
1417
- "div",
1418
- {
1419
- "data-slot": "composer-attachments",
1420
- className: cn(
1421
- "flex flex-wrap gap-2 p-3",
1422
- "border-t border-neutral-200 dark:border-neutral-700"
1423
- ),
1424
- children: attachments.map((attachment) => /* @__PURE__ */ jsx(
1425
- AttachmentPreviewItem,
1426
- {
1427
- attachment,
1428
- onRemove: () => handleRemoveAttachment(attachment.id)
1429
- },
1430
- attachment.id
1431
- ))
1432
- }
1433
- ),
1434
- /* @__PURE__ */ jsxs(
1435
- "div",
1436
- {
1437
- "data-slot": "composer-input-area",
1438
- className: cn(
1439
- "flex items-center gap-2 p-3",
1440
- "bg-white dark:bg-neutral-900",
1441
- variant === "default" && "border-t border-neutral-200 dark:border-neutral-700"
1442
- ),
1443
- children: [
1444
- showAttachmentPicker && /* @__PURE__ */ jsx(
1445
- AttachmentPicker,
1446
- {
1447
- onFilesSelected: handleFilesSelected,
1448
- acceptedTypes: acceptedFileTypes,
1449
- maxFileSize,
1450
- maxFiles: maxAttachments - attachments.length,
1451
- disabled: disabled || attachments.length >= maxAttachments,
1452
- onError
1453
- }
1454
- ),
1455
- showCameraButton && /* @__PURE__ */ jsx(
1456
- CameraButton,
1457
- {
1458
- onCapture: (file) => handleFilesSelected([file]),
1459
- disabled: disabled || attachments.length >= maxAttachments
1460
- }
1461
- ),
1462
- /* @__PURE__ */ jsxs(
1463
- "div",
1464
- {
1465
- "data-slot": "composer-input-wrapper",
1466
- className: "relative flex-1",
1467
- ref: mention.anchorRef,
1468
- children: [
1469
- /* @__PURE__ */ jsx(MentionMenu, { mention }),
1470
- /* @__PURE__ */ jsx(
1471
- "textarea",
1472
- {
1473
- ref: textareaRef,
1474
- "data-slot": "composer-input",
1475
- value: content,
1476
- onChange: (e) => {
1477
- setContent(e.target.value);
1478
- mention.sync(
1479
- e.target.value,
1480
- e.target.selectionStart ?? e.target.value.length
1481
- );
1482
- },
1483
- onClick: (e) => {
1484
- const el = e.currentTarget;
1485
- mention.sync(el.value, el.selectionStart ?? el.value.length);
1486
- },
1487
- onKeyDown: handleKeyDown,
1488
- onPaste: handlePaste,
1489
- placeholder,
1490
- disabled: disabled || isSending,
1491
- rows: 1,
1492
- className: cn(
1493
- "w-full resize-none rounded-2xl py-2.5",
1494
- inputTrailing ? "ps-4 pe-10" : "px-4",
1495
- "bg-neutral-100 dark:bg-neutral-800",
1496
- "text-neutral-900 dark:text-neutral-100",
1497
- "placeholder:text-neutral-400 dark:placeholder:text-neutral-500",
1498
- "focus:ring-primary-500 focus:ring-2 focus:outline-none",
1499
- "disabled:cursor-not-allowed disabled:opacity-50",
1500
- "transition-colors",
1501
- "max-h-[150px]"
1502
- ),
1503
- "aria-label": "Message",
1504
- "aria-describedby": showCharacterCount ? "char-count" : void 0,
1505
- ...mention.inputProps
1506
- }
1507
- ),
1508
- inputTrailing && /* @__PURE__ */ jsx(
1509
- "div",
1510
- {
1511
- "data-slot": "composer-input-trailing",
1512
- className: "pointer-events-none absolute end-1 top-0 flex h-[44px] items-center [&>*]:pointer-events-auto",
1513
- children: inputTrailing
1514
- }
1515
- ),
1516
- showCharacterCount && /* @__PURE__ */ jsx(
1517
- "div",
1518
- {
1519
- "data-slot": "composer-char-count",
1520
- id: "char-count",
1521
- className: "absolute end-3 bottom-1.5",
1522
- children: /* @__PURE__ */ jsx(CharacterCounter, { current: content.length, max: maxLength })
1523
- }
1524
- )
1525
- ]
1526
- }
1527
- ),
1528
- /* @__PURE__ */ jsx(
1529
- SendButton,
1530
- {
1531
- canSend,
1532
- isLoading: isSending,
1533
- disabled
1534
- }
1535
- )
1536
- ]
1537
- }
1538
- )
1539
- ]
1540
- }
1541
- )
1542
- }
1543
- );
1544
- }
1545
- );
1546
- MessageComposer.displayName = "MessageComposer";
1547
- var headerVariants = cva(
1548
- [
1549
- "flex items-center gap-3 px-4 py-3",
1550
- "bg-white dark:bg-neutral-900",
1551
- "border-b border-neutral-200 dark:border-neutral-700"
1323
+ "flex items-center gap-3 px-4 py-3",
1324
+ "bg-white dark:bg-neutral-900",
1325
+ "border-b border-neutral-200 dark:border-neutral-700"
1552
1326
  ],
1553
1327
  {
1554
1328
  variants: {
@@ -1603,7 +1377,7 @@ function formatLastSeen(date) {
1603
1377
  if (diffDays < 7) return `${diffDays}d ago`;
1604
1378
  return date.toLocaleDateString();
1605
1379
  }
1606
- var ConversationHeader = React6.forwardRef(
1380
+ var ConversationHeader = React.forwardRef(
1607
1381
  ({
1608
1382
  className,
1609
1383
  size,
@@ -1725,7 +1499,7 @@ var ConversationHeader = React6.forwardRef(
1725
1499
  }
1726
1500
  );
1727
1501
  ConversationHeader.displayName = "ConversationHeader";
1728
- var ConversationListItem = React6.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
1502
+ var ConversationListItem = React.forwardRef(({ className, conversation, isSelected, onSelect, ...props }, ref) => {
1729
1503
  const participant = conversation.participants.find((p) => !p.isCurrentUser);
1730
1504
  const title = getConversationTitle(conversation, participant);
1731
1505
  const avatarUrl = conversation.avatarUrl || participant?.avatarUrl;
@@ -1933,7 +1707,7 @@ function ConversationListSkeleton({
1933
1707
  }
1934
1708
  ConversationListSkeleton.displayName = "ConversationListSkeleton";
1935
1709
  function LightboxModal({ attachment, onClose }) {
1936
- React6.useEffect(() => {
1710
+ React.useEffect(() => {
1937
1711
  const handleKeyDown = (event) => {
1938
1712
  if (event.key === "Escape") {
1939
1713
  onClose();
@@ -2030,7 +1804,7 @@ function LightboxModal({ attachment, onClose }) {
2030
1804
  );
2031
1805
  }
2032
1806
  LightboxModal.displayName = "LightboxModal";
2033
- var MessageThread = React6.forwardRef(
1807
+ var MessageThread = React.forwardRef(
2034
1808
  ({
2035
1809
  conversation,
2036
1810
  messages,
@@ -2061,8 +1835,23 @@ var MessageThread = React6.forwardRef(
2061
1835
  onError,
2062
1836
  className
2063
1837
  }, ref) => {
2064
- const [lightboxAttachment, setLightboxAttachment] = React6.useState(null);
2065
- const [replyTo, setReplyTo] = React6.useState(null);
1838
+ const [lightboxAttachment, setLightboxAttachment] = React.useState(null);
1839
+ const [replyTo, setReplyTo] = React.useState(null);
1840
+ const composerRef = React.useRef(null);
1841
+ React.useEffect(() => {
1842
+ notifyComposerMigrationOnce("MessageThread");
1843
+ }, []);
1844
+ const [draft, setDraft] = React.useState("");
1845
+ const draftEpochRef = React.useRef(0);
1846
+ const handleDraftChange = React.useCallback((value) => {
1847
+ draftEpochRef.current += 1;
1848
+ setDraft(value);
1849
+ }, []);
1850
+ const { stopTyping } = useTypingEmulation({
1851
+ value: draft,
1852
+ onTypingStart: eventHandlers.onTypingStart,
1853
+ onTypingStop: eventHandlers.onTypingStop
1854
+ });
2066
1855
  const participant = conversation?.type === "direct" ? conversation.participants.find((p) => p.id !== currentUser.id) : void 0;
2067
1856
  const handleAttachmentClick = (attachment, message) => {
2068
1857
  if (attachment.type === "image" || attachment.type === "video") {
@@ -2071,12 +1860,21 @@ var MessageThread = React6.forwardRef(
2071
1860
  eventHandlers.onAttachmentClick?.(attachment, message);
2072
1861
  };
2073
1862
  const handleSendMessage = async (newMessage) => {
1863
+ stopTyping();
2074
1864
  const messageWithReply = {
2075
1865
  ...newMessage,
2076
1866
  replyToId: replyTo?.id || newMessage.replyToId
2077
1867
  };
2078
1868
  setReplyTo(null);
2079
- await eventHandlers.onSendMessage?.(messageWithReply);
1869
+ const epoch = draftEpochRef.current;
1870
+ try {
1871
+ await Promise.resolve(eventHandlers.onSendMessage?.(messageWithReply));
1872
+ } catch (error) {
1873
+ if (draftEpochRef.current === epoch) {
1874
+ setDraft(newMessage.content);
1875
+ }
1876
+ throw error;
1877
+ }
2080
1878
  };
2081
1879
  return /* @__PURE__ */ jsxs(
2082
1880
  "div",
@@ -2102,9 +1900,11 @@ var MessageThread = React6.forwardRef(
2102
1900
  /* @__PURE__ */ jsx(
2103
1901
  DragDropZone,
2104
1902
  {
2105
- onFilesDropped: () => {
2106
- onError?.("Drop files on the composer to attach them");
1903
+ onFilesDropped: (files) => {
1904
+ composerRef.current?.addFiles(files);
2107
1905
  },
1906
+ maxFiles: maxAttachments,
1907
+ onError,
2108
1908
  disabled: !showAttachmentPicker,
2109
1909
  className: "flex-1 overflow-hidden",
2110
1910
  children: /* @__PURE__ */ jsx(
@@ -2130,23 +1930,38 @@ var MessageThread = React6.forwardRef(
2130
1930
  }
2131
1931
  ),
2132
1932
  /* @__PURE__ */ jsx(
2133
- MessageComposer,
1933
+ "div",
2134
1934
  {
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)
1935
+ "data-slot": "message-thread-composer",
1936
+ className: "shrink-0 border-t border-neutral-200 p-3 dark:border-neutral-700",
1937
+ children: /* @__PURE__ */ jsx(
1938
+ ChatComposer,
1939
+ {
1940
+ ref: composerRef,
1941
+ value: draft,
1942
+ onValueChange: handleDraftChange,
1943
+ onSend: handleSendMessage,
1944
+ placeholder,
1945
+ maxLength: maxMessageLength,
1946
+ showCharacterCount,
1947
+ isSending,
1948
+ allowAttachments: showAttachmentPicker,
1949
+ acceptedFileTypes: acceptedFileTypes ?? DEFAULT_ACCEPTED_FILE_TYPES,
1950
+ maxFileSize: maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
1951
+ maxAttachments,
1952
+ onError,
1953
+ replyTo,
1954
+ onCancelReply: () => setReplyTo(null),
1955
+ inputLabel: "Message",
1956
+ micSlot: showCameraButton ? /* @__PURE__ */ jsx(
1957
+ CameraButton,
1958
+ {
1959
+ onCapture: (file) => composerRef.current?.addFiles([file]),
1960
+ disabled: isSending
1961
+ }
1962
+ ) : void 0
1963
+ }
1964
+ )
2150
1965
  }
2151
1966
  ),
2152
1967
  /* @__PURE__ */ jsx(
@@ -2226,250 +2041,7 @@ function MessagingSplitView({
2226
2041
  );
2227
2042
  }
2228
2043
  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
2044
 
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
2045
+ export { AttachmentPreview, ConversationHeader, ConversationListItem, ConversationListSkeleton, DateSeparator, EmptyState, LightboxModal, LoadMoreButton, MessageBubble, MessageList, MessageStatusIcon, MessageThread, MessagingSplitView, ReadReceiptIndicator, SkeletonMessage, TypingIndicator, bubbleVariants, formatDateLabel, formatFileSize, formatLastSeen, getConversationSubtitle, getConversationTitle, groupMessagesByDate, headerVariants, isSameSenderGroup, useMessageScroll, useMessages, useReadReceipts, useTypingEmulation, useTypingIndicator };
2046
+ //# sourceMappingURL=chunk-VSVGMMNO.js.map
2047
+ //# sourceMappingURL=chunk-VSVGMMNO.js.map