@banbox/chat 1.0.3 → 1.0.4
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/dist/index.cjs +477 -295
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +475 -293
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +208 -115
- package/src/chat/SinglePopup.tsx +55 -40
- package/src/ui/chat/ChatHeader.tsx +32 -24
- package/src/ui/chat/ChatListHeader.tsx +157 -156
- package/src/ui/chat/ChatScroll.tsx +71 -64
- package/src/ui/chat/TypingIndicator.tsx +36 -17
package/package.json
CHANGED
package/src/chat/InboxPopup.tsx
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import { motion } from "framer-motion";
|
|
5
|
-
import React, { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
6
6
|
|
|
7
|
-
import ChatConfirmModal from "../modals/chat/ChatConfirmModal";
|
|
8
7
|
import { useChatUI } from "../contexts/ChatUIContext";
|
|
9
8
|
import { useGallery } from "../contexts/GalleryContext";
|
|
9
|
+
import ChatConfirmModal from "../modals/chat/ChatConfirmModal";
|
|
10
10
|
import ChatFooter from "../ui/chat/ChatFooter";
|
|
11
11
|
import ChatHeader from "../ui/chat/ChatHeader";
|
|
12
12
|
import ChatIdentity from "../ui/chat/ChatIdentity";
|
|
@@ -17,19 +17,16 @@ import ChatScroll from "../ui/chat/ChatScroll";
|
|
|
17
17
|
import type { ChatThreadStatus } from "../ui/chat/ChatThreadItem";
|
|
18
18
|
import ChatThreadItem from "../ui/chat/ChatThreadItem";
|
|
19
19
|
import TypingIndicator from "../ui/chat/TypingIndicator";
|
|
20
|
-
import ChatSpinner from "../ui/chat/ChatSpinner";
|
|
21
20
|
import ChatImagePreviewModal from "./ChatImagePreviewModal";
|
|
22
21
|
|
|
23
|
-
import type { Thread, Message, MessageRef } from "../types";
|
|
24
22
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
23
|
+
import type { Message, MessageRef, Thread } from "../types";
|
|
25
24
|
|
|
26
25
|
/* =======================
|
|
27
26
|
Props
|
|
28
27
|
======================= */
|
|
29
28
|
export type InboxPopupProps = {
|
|
30
|
-
/** The unified data adapter — provides threads, messages, and send */
|
|
31
29
|
adapter: ChatAdapter;
|
|
32
|
-
/** UI-level callbacks (toast, navigation, kebab menu) */
|
|
33
30
|
uiCallbacks?: ChatUICallbacks;
|
|
34
31
|
};
|
|
35
32
|
|
|
@@ -37,9 +34,16 @@ export type InboxPopupProps = {
|
|
|
37
34
|
Constants
|
|
38
35
|
======================= */
|
|
39
36
|
const avatarBgByInitial: Record<string, string> = {
|
|
40
|
-
K: "#FFE7DB",
|
|
37
|
+
K: "#FFE7DB",
|
|
38
|
+
A: "#FFE5DA",
|
|
39
|
+
F: "#E8F7FF",
|
|
40
|
+
B: "#F0EDEB",
|
|
41
|
+
b: "#F0EDEB",
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const GRADIENT_BORDER =
|
|
45
|
+
"linear-gradient(236.83deg, rgba(51,201,212,0.3) 0.4%, rgba(39,83,251,0.3) 30.28%, rgba(39,83,251,0.3) 50.2%, rgba(39,83,251,0.3) 65.14%, rgba(235,67,255,0.3) 100%)";
|
|
46
|
+
|
|
43
47
|
/* =======================
|
|
44
48
|
Component
|
|
45
49
|
======================= */
|
|
@@ -47,7 +51,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
47
51
|
const { close, selectThread, selectedThreadId, reference } = useChatUI();
|
|
48
52
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
49
53
|
|
|
50
|
-
/* ───
|
|
54
|
+
/* ─── Threads ─── */
|
|
51
55
|
const [threads, setThreads] = useState<Thread[]>(() => adapter.threads.list(reference));
|
|
52
56
|
const refreshThreads = useCallback(
|
|
53
57
|
() => setThreads(adapter.threads.list(reference)),
|
|
@@ -55,11 +59,13 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
55
59
|
);
|
|
56
60
|
|
|
57
61
|
useEffect(() => {
|
|
58
|
-
// Immediate sync on mount / reference change
|
|
59
62
|
let rafId = 0;
|
|
60
63
|
rafId = requestAnimationFrame(refreshThreads);
|
|
61
64
|
const unsub = adapter.threads.subscribe(refreshThreads);
|
|
62
|
-
return () => {
|
|
65
|
+
return () => {
|
|
66
|
+
cancelAnimationFrame(rafId);
|
|
67
|
+
unsub();
|
|
68
|
+
};
|
|
63
69
|
}, [adapter, reference, refreshThreads]);
|
|
64
70
|
|
|
65
71
|
/* ─── Active thread & messages ─── */
|
|
@@ -74,12 +80,10 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
74
80
|
activeId ? adapter.messages.list(activeId) : [],
|
|
75
81
|
);
|
|
76
82
|
|
|
77
|
-
// Refresh messages when active thread changes or rev bumps
|
|
78
83
|
useEffect(() => {
|
|
79
84
|
if (activeId) setMessages(adapter.messages.list(activeId));
|
|
80
85
|
}, [activeId, rev, adapter]);
|
|
81
86
|
|
|
82
|
-
// Subscribe to real-time message updates for the active thread
|
|
83
87
|
useEffect(() => {
|
|
84
88
|
if (!activeId || !adapter.messages.subscribe) return;
|
|
85
89
|
const unsub = adapter.messages.subscribe(activeId, () => {
|
|
@@ -88,7 +92,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
88
92
|
return unsub;
|
|
89
93
|
}, [activeId, adapter]);
|
|
90
94
|
|
|
91
|
-
/* ─── Derived
|
|
95
|
+
/* ─── Derived ─── */
|
|
92
96
|
const initial = activeThread?.avatarText ?? "U";
|
|
93
97
|
const title = activeThread?.title ?? "Unknown";
|
|
94
98
|
const subtitle = activeThread?.subTitle ?? "";
|
|
@@ -96,28 +100,31 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
96
100
|
const isVerified = Boolean(activeThread?.badge);
|
|
97
101
|
const avatarBg = avatarBgByInitial[initial] ?? "#FFF1EC";
|
|
98
102
|
|
|
99
|
-
const idLabel = activeThread?.orderId
|
|
100
|
-
|
|
103
|
+
const idLabel = activeThread?.orderId
|
|
104
|
+
? "Order ID"
|
|
105
|
+
: activeThread?.inquiryId
|
|
106
|
+
? "Inquiry ID"
|
|
107
|
+
: undefined;
|
|
108
|
+
const idButtonLabel = activeThread?.orderId
|
|
109
|
+
? "View Order"
|
|
110
|
+
: activeThread?.inquiryId
|
|
111
|
+
? "View Inquiry"
|
|
112
|
+
: undefined;
|
|
101
113
|
const idValue = activeThread?.orderId ?? activeThread?.inquiryId ?? undefined;
|
|
102
114
|
|
|
103
|
-
/* ─── Loading state ─── */
|
|
104
115
|
const [showDelete, setShowDelete] = useState(false);
|
|
105
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
106
116
|
const scrollKey = `${activeId}-${messages.length}-${rev}`;
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
/* mark read on switch */
|
|
119
|
+
const prevActiveIdRef = useRef(activeId);
|
|
109
120
|
useEffect(() => {
|
|
110
121
|
if (prevActiveIdRef.current !== activeId) {
|
|
111
122
|
prevActiveIdRef.current = activeId;
|
|
112
|
-
setIsLoading(true);
|
|
113
|
-
const t = setTimeout(() => setIsLoading(false), 300);
|
|
114
|
-
// Mark thread as read when switching to it
|
|
115
123
|
if (activeId) adapter.threads.markRead?.(activeId);
|
|
116
|
-
return () => clearTimeout(t);
|
|
117
124
|
}
|
|
118
125
|
}, [activeId, adapter]);
|
|
119
126
|
|
|
120
|
-
/* ───
|
|
127
|
+
/* ─── Helpers ─── */
|
|
121
128
|
const toRef = (m: Message): MessageRef => ({
|
|
122
129
|
id: m.id,
|
|
123
130
|
author: typeof m.author === "string" ? m.author : "U",
|
|
@@ -128,9 +135,11 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
128
135
|
audio: m.audio,
|
|
129
136
|
});
|
|
130
137
|
|
|
131
|
-
/* ─── Delete ─── */
|
|
132
138
|
const handleConfirmDelete = () => {
|
|
133
|
-
if (!activeId) {
|
|
139
|
+
if (!activeId) {
|
|
140
|
+
setShowDelete(false);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
134
143
|
adapter.threads.delete(activeId);
|
|
135
144
|
const nextId = threads.filter((t) => t.id !== activeId)[0]?.id;
|
|
136
145
|
if (nextId) selectThread(nextId);
|
|
@@ -143,66 +152,125 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
143
152
|
});
|
|
144
153
|
};
|
|
145
154
|
|
|
155
|
+
/* ─── Filtered threads ─── */
|
|
156
|
+
const filteredThreads = threads.filter((t) => {
|
|
157
|
+
if (!searchQuery.trim()) return true;
|
|
158
|
+
const q = searchQuery.toLowerCase();
|
|
159
|
+
return (
|
|
160
|
+
t.title.toLowerCase().includes(q) ||
|
|
161
|
+
t.last?.toLowerCase().includes(q) ||
|
|
162
|
+
t.orderId?.toLowerCase().includes(q) ||
|
|
163
|
+
t.inquiryId?.toLowerCase().includes(q)
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/* ══════════════════════════════════════════════════
|
|
168
|
+
RENDER
|
|
169
|
+
══════════════════════════════════════════════════ */
|
|
146
170
|
return (
|
|
147
|
-
<div
|
|
171
|
+
<div style={{ position: "fixed", bottom: 16, right: 16, zIndex: 10002 }}>
|
|
148
172
|
{/* Backdrop */}
|
|
149
173
|
<motion.button
|
|
150
174
|
aria-label="Close chat"
|
|
151
175
|
onClick={close}
|
|
152
|
-
|
|
176
|
+
style={{ position: "fixed", inset: 0, background: "transparent", border: "none", cursor: "auto" }}
|
|
153
177
|
initial={{ opacity: 0 }}
|
|
154
178
|
animate={{ opacity: 1 }}
|
|
155
179
|
exit={{ opacity: 0 }}
|
|
156
180
|
transition={{ type: "tween", duration: 0.25 }}
|
|
157
181
|
/>
|
|
158
182
|
|
|
159
|
-
{/*
|
|
183
|
+
{/* Outer gradient border wrapper */}
|
|
160
184
|
<motion.div
|
|
161
185
|
role="dialog"
|
|
162
186
|
aria-modal="true"
|
|
163
|
-
className="relative rounded-[20px] p-[3px]"
|
|
164
187
|
style={{
|
|
188
|
+
position: "relative",
|
|
165
189
|
width: 800,
|
|
166
190
|
height: 650,
|
|
191
|
+
borderRadius: 20,
|
|
192
|
+
padding: 3,
|
|
167
193
|
boxShadow: "0px 2px 12px 0px #3B33331A",
|
|
168
|
-
background:
|
|
169
|
-
"linear-gradient(236.83deg, rgba(51, 201, 212, 0.3) 0.4%, rgba(39, 83, 251, 0.3) 30.28%, rgba(39, 83, 251, 0.3) 50.2%, rgba(39, 83, 251, 0.3) 65.14%, rgba(235, 67, 255, 0.3) 100%)",
|
|
194
|
+
background: GRADIENT_BORDER,
|
|
170
195
|
}}
|
|
171
196
|
initial={{ x: "110%" }}
|
|
172
197
|
animate={{ x: 0 }}
|
|
173
198
|
exit={{ x: "110%" }}
|
|
174
199
|
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
175
200
|
>
|
|
201
|
+
{/* Inner white card */}
|
|
176
202
|
<div
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
style={{
|
|
204
|
+
position: "relative",
|
|
205
|
+
height: "100%",
|
|
206
|
+
width: "100%",
|
|
207
|
+
overflow: "hidden",
|
|
208
|
+
borderRadius: 18,
|
|
209
|
+
backgroundColor: "#fff",
|
|
210
|
+
overscrollBehavior: "contain",
|
|
211
|
+
}}
|
|
179
212
|
>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
213
|
+
{/* ── TWO-COLUMN GRID: LEFT chat | RIGHT thread list ── */}
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
display: "grid",
|
|
217
|
+
gridTemplateColumns: "1fr 310px",
|
|
218
|
+
height: "100%",
|
|
219
|
+
minHeight: 0,
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
{/* ════════ LEFT — chat area ════════ */}
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
display: "flex",
|
|
226
|
+
flexDirection: "column",
|
|
227
|
+
height: "100%",
|
|
228
|
+
minHeight: 0,
|
|
229
|
+
borderRight: "1px solid #9BBCCF",
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{/* Chat header — 64px fixed */}
|
|
233
|
+
<div style={{ height: 64, flexShrink: 0 }}>
|
|
186
234
|
<ChatHeader
|
|
187
235
|
left={
|
|
188
236
|
activeThread?.avatarSrc ? (
|
|
189
|
-
<ChatIdentity
|
|
237
|
+
<ChatIdentity
|
|
238
|
+
variant="avatar"
|
|
239
|
+
src={activeThread.avatarSrc}
|
|
240
|
+
online={online}
|
|
241
|
+
title={title}
|
|
242
|
+
subtitle={subtitle}
|
|
243
|
+
verified={isVerified}
|
|
244
|
+
subtitleVariant="muted"
|
|
245
|
+
/>
|
|
190
246
|
) : (
|
|
191
|
-
<ChatIdentity
|
|
247
|
+
<ChatIdentity
|
|
248
|
+
variant="initial"
|
|
249
|
+
initial={initial}
|
|
250
|
+
bg={avatarBg}
|
|
251
|
+
online={online}
|
|
252
|
+
title={title}
|
|
253
|
+
subtitle={subtitle}
|
|
254
|
+
verified={isVerified}
|
|
255
|
+
subtitleVariant="muted"
|
|
256
|
+
/>
|
|
192
257
|
)
|
|
193
258
|
}
|
|
194
259
|
right={
|
|
195
260
|
uiCallbacks?.renderKebabMenu?.({
|
|
196
261
|
pinned: Boolean(activeThread?.pinned),
|
|
197
|
-
onPinToggle: () => {
|
|
262
|
+
onPinToggle: () => {
|
|
263
|
+
if (activeId) adapter.threads.pin(activeId, !activeThread?.pinned);
|
|
264
|
+
},
|
|
198
265
|
onDelete: () => setShowDelete(true),
|
|
199
266
|
}) ?? null
|
|
200
267
|
}
|
|
201
268
|
/>
|
|
202
269
|
</div>
|
|
203
270
|
|
|
271
|
+
{/* Optional inquiry bar */}
|
|
204
272
|
{idValue && (
|
|
205
|
-
<div
|
|
273
|
+
<div style={{ flexShrink: 0 }}>
|
|
206
274
|
<ChatInquiryBar
|
|
207
275
|
id={idValue}
|
|
208
276
|
label={idLabel}
|
|
@@ -215,47 +283,66 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
215
283
|
</div>
|
|
216
284
|
)}
|
|
217
285
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
286
|
+
{/* Messages area — fills remaining space */}
|
|
287
|
+
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
|
288
|
+
<ChatScroll
|
|
289
|
+
className="h-full pb-10"
|
|
290
|
+
bottomAlignWhenShort={false}
|
|
291
|
+
scrollKey={scrollKey}
|
|
292
|
+
style={{ height: "100%", overflowY: "auto" }}
|
|
293
|
+
>
|
|
294
|
+
{messages.map((m, idx) => {
|
|
295
|
+
const mine = m.author === "you";
|
|
296
|
+
const isLast = idx === messages.length - 1;
|
|
297
|
+
return (
|
|
298
|
+
<ChatMessageItem
|
|
299
|
+
key={m.id}
|
|
300
|
+
id={m.id}
|
|
301
|
+
mine={mine}
|
|
302
|
+
time={m.time ?? ""}
|
|
303
|
+
authorInitial={typeof m.author === "string" ? m.author : "U"}
|
|
304
|
+
avatarBg={avatarBg}
|
|
305
|
+
text={m.text ?? m.content}
|
|
306
|
+
businessCard={
|
|
307
|
+
m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]
|
|
308
|
+
}
|
|
309
|
+
addressCard={
|
|
310
|
+
m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]
|
|
311
|
+
}
|
|
312
|
+
images={m.images}
|
|
313
|
+
files={m.files}
|
|
314
|
+
audio={m.audio}
|
|
315
|
+
replyTo={m.replyTo}
|
|
316
|
+
showStatus={isLast}
|
|
317
|
+
status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
|
|
318
|
+
onReply={() => setReplyTo(toRef(m))}
|
|
319
|
+
initialSrc={m.avatarSrc}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
})}
|
|
323
|
+
</ChatScroll>
|
|
251
324
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
325
|
+
{/* Typing indicator — pinned at bottom of messages */}
|
|
326
|
+
<div
|
|
327
|
+
style={{
|
|
328
|
+
position: "absolute",
|
|
329
|
+
left: 0,
|
|
330
|
+
right: 0,
|
|
331
|
+
bottom: 0,
|
|
332
|
+
display: "flex",
|
|
333
|
+
alignItems: "center",
|
|
334
|
+
justifyContent: "flex-start",
|
|
335
|
+
padding: "4px 16px 8px",
|
|
336
|
+
background: "#fff",
|
|
337
|
+
pointerEvents: "none",
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
<TypingIndicator style={{ pointerEvents: "auto" }} />
|
|
255
341
|
</div>
|
|
256
342
|
</div>
|
|
257
343
|
|
|
258
|
-
|
|
344
|
+
{/* Chat footer — shrinks to content height */}
|
|
345
|
+
<div style={{ flexShrink: 0 }}>
|
|
259
346
|
<ChatFooter
|
|
260
347
|
key={activeId}
|
|
261
348
|
replyTo={replyTo}
|
|
@@ -268,45 +355,51 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
268
355
|
</div>
|
|
269
356
|
</div>
|
|
270
357
|
|
|
271
|
-
{/* RIGHT —
|
|
272
|
-
<div
|
|
273
|
-
|
|
274
|
-
<div
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
358
|
+
{/* ════════ RIGHT — thread list ════════ */}
|
|
359
|
+
<div style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }}>
|
|
360
|
+
{/* List header — 64px fixed */}
|
|
361
|
+
<div style={{ flexShrink: 0 }}>
|
|
362
|
+
<ChatListHeader onClose={close} onSearchChange={(val) => setSearchQuery(val)} />
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Scrollable thread list */}
|
|
366
|
+
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
|
|
367
|
+
{filteredThreads.map((t) => {
|
|
368
|
+
const status: ChatThreadStatus =
|
|
369
|
+
t.status ??
|
|
370
|
+
(t.unread && t.unread > 0
|
|
371
|
+
? { kind: "new", count: t.unread }
|
|
372
|
+
: { kind: "seen" });
|
|
373
|
+
return (
|
|
374
|
+
<ChatThreadItem
|
|
375
|
+
key={t.id}
|
|
376
|
+
onClick={() => {
|
|
377
|
+
setReplyTo(undefined);
|
|
378
|
+
selectThread(t.id);
|
|
379
|
+
}}
|
|
380
|
+
active={t.id === activeId}
|
|
381
|
+
pinned={Boolean(t.pinned)}
|
|
382
|
+
online={t.online}
|
|
383
|
+
verified={Boolean(t.badge)}
|
|
384
|
+
title={t.title}
|
|
385
|
+
preview={t.last ?? ""}
|
|
386
|
+
time={t.time ?? ""}
|
|
387
|
+
status={status}
|
|
388
|
+
avatarText={t.avatarText ?? ""}
|
|
389
|
+
avatarSrc={t.avatarSrc}
|
|
390
|
+
/>
|
|
391
|
+
);
|
|
392
|
+
})}
|
|
305
393
|
</div>
|
|
306
394
|
</div>
|
|
307
395
|
</div>
|
|
308
396
|
|
|
309
|
-
|
|
397
|
+
{/* Modals */}
|
|
398
|
+
<ChatConfirmModal
|
|
399
|
+
open={showDelete}
|
|
400
|
+
onClose={() => setShowDelete(false)}
|
|
401
|
+
onConfirm={handleConfirmDelete}
|
|
402
|
+
/>
|
|
310
403
|
<ChatImagePreviewModal isOpen={isGalleryOpen} onClose={closeGallery} />
|
|
311
404
|
</div>
|
|
312
405
|
</motion.div>
|