@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.
- package/MIGRATION.md +112 -0
- package/dist/{chunk-QWJGEDHV.js → chunk-22CYDCXA.js} +13 -1183
- package/dist/chunk-22CYDCXA.js.map +1 -0
- package/dist/chunk-IIFQ6BSJ.js +1888 -0
- package/dist/chunk-IIFQ6BSJ.js.map +1 -0
- package/dist/{chunk-ZXRVVNT5.cjs → chunk-OFUKTVBN.cjs} +367 -798
- package/dist/chunk-OFUKTVBN.cjs.map +1 -0
- package/dist/chunk-OYQ7GYAA.cjs +1921 -0
- package/dist/chunk-OYQ7GYAA.cjs.map +1 -0
- package/dist/{chunk-PL6TFCYR.js → chunk-VSVGMMNO.js} +365 -793
- package/dist/chunk-VSVGMMNO.js.map +1 -0
- package/dist/{chunk-XJUIZA4G.cjs → chunk-WIAQQNFU.cjs} +12 -1184
- package/dist/chunk-WIAQQNFU.cjs.map +1 -0
- package/dist/components/SuperChat/index.cjs +11 -8
- package/dist/components/SuperChat/index.cjs.map +1 -1
- package/dist/components/SuperChat/index.js +5 -2
- package/dist/components/SuperChat/index.js.map +1 -1
- package/dist/components/SuperChat/plugins/index.cjs +10 -3
- package/dist/components/SuperChat/plugins/index.cjs.map +1 -1
- package/dist/components/SuperChat/plugins/index.js +9 -2
- package/dist/components/SuperChat/plugins/index.js.map +1 -1
- package/dist/index.cjs +91 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -98
- package/dist/index.d.ts +38 -98
- package/dist/index.js +20 -48
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -1
- package/dist/chunk-H3GRO43Z.cjs +0 -735
- package/dist/chunk-H3GRO43Z.cjs.map +0 -1
- package/dist/chunk-MD3ZY4UO.js +0 -705
- package/dist/chunk-MD3ZY4UO.js.map +0 -1
- package/dist/chunk-PL6TFCYR.js.map +0 -1
- package/dist/chunk-QWJGEDHV.js.map +0 -1
- package/dist/chunk-XJUIZA4G.cjs.map +0 -1
- package/dist/chunk-ZXRVVNT5.cjs.map +0 -1
|
@@ -1,9 +1,288 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
882
|
-
const bottomRef =
|
|
883
|
-
const [isUserScrolled, setIsUserScrolled] =
|
|
884
|
-
const prevMessageCountRef =
|
|
885
|
-
|
|
886
|
-
const handleScroll =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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] =
|
|
2065
|
-
const [replyTo, setReplyTo] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1933
|
+
"div",
|
|
2134
1934
|
{
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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,
|
|
2474
|
-
//# sourceMappingURL=chunk-
|
|
2475
|
-
//# sourceMappingURL=chunk-
|
|
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
|