@banbox/chat 1.0.4 → 1.0.5
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 +252 -412
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +2273 -0
- package/dist/index.d.cts +19 -13
- package/dist/index.d.ts +19 -13
- package/dist/index.js +252 -412
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/chat/ChatRoot.tsx +15 -10
- package/src/chat/InboxPopup.tsx +105 -176
- package/src/chat/SinglePopup.tsx +39 -43
- package/src/index.ts +3 -0
- package/src/styles/index.css +231 -0
- package/src/ui/chat/ChatHeader.tsx +3 -14
- package/src/ui/chat/ChatListHeader.tsx +74 -103
- package/src/ui/chat/ChatScroll.tsx +5 -24
- package/src/ui/chat/TypingIndicator.tsx +8 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banbox/chat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
"types": "./dist/index.d.cts",
|
|
16
16
|
"default": "./dist/index.cjs"
|
|
17
17
|
}
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"./dist/index.css": "./dist/index.css"
|
|
19
20
|
},
|
|
20
21
|
"main": "./dist/index.cjs",
|
|
21
22
|
"module": "./dist/index.js",
|
|
22
23
|
"types": "./dist/index.d.ts",
|
|
23
|
-
"sideEffects":
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
24
27
|
"files": [
|
|
25
28
|
"dist",
|
|
26
29
|
"src",
|
|
@@ -75,8 +78,11 @@
|
|
|
75
78
|
"tailwind-merge": "^3.6.0"
|
|
76
79
|
},
|
|
77
80
|
"devDependencies": {
|
|
81
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
78
82
|
"@types/react": "^19.2.16",
|
|
79
83
|
"@types/react-dom": "^19.2.3",
|
|
84
|
+
"postcss": "^8.5.15",
|
|
85
|
+
"tailwindcss": "^4.3.0",
|
|
80
86
|
"tsup": "^8.5.0",
|
|
81
87
|
"typescript": "~6.0.3"
|
|
82
88
|
}
|
package/src/chat/ChatRoot.tsx
CHANGED
|
@@ -8,29 +8,33 @@ import { GalleryProvider } from "../contexts/GalleryProvider";
|
|
|
8
8
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
9
9
|
import InboxPopup from "./InboxPopup";
|
|
10
10
|
import SinglePopup from "./SinglePopup";
|
|
11
|
+
import type { ChatTheme } from "./InboxPopup";
|
|
11
12
|
|
|
12
13
|
export type ChatRootProps = {
|
|
13
14
|
/**
|
|
14
15
|
* The unified data adapter — provides all threads, messages, and send logic.
|
|
15
|
-
*
|
|
16
|
-
* Implement this in your host app:
|
|
17
|
-
* ```ts
|
|
18
|
-
* const adapter = createDemoChatAdapter(); // or createApiChatAdapter(...)
|
|
19
|
-
* ```
|
|
20
16
|
*/
|
|
21
17
|
adapter: ChatAdapter;
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
20
|
* Optional UI callbacks — controls toast notifications, navigation,
|
|
25
21
|
* and the kebab (⋮) menu renderer.
|
|
26
|
-
*
|
|
27
|
-
* These delegate UI side-effects back to the host app so the package
|
|
28
|
-
* stays decoupled from the host's routing and notification systems.
|
|
29
22
|
*/
|
|
30
23
|
uiCallbacks?: ChatUICallbacks;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Visual theme:
|
|
27
|
+
* - "marketplace" (default) — orange primary (#ff5300)
|
|
28
|
+
* - "admin" — black primary (#1a1a1a)
|
|
29
|
+
* - custom object — { primary, primaryActive, surfaceLow }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <ChatRoot adapter={adapter} theme="admin" />
|
|
33
|
+
*/
|
|
34
|
+
theme?: ChatTheme;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
37
|
+
export default function ChatRoot({ adapter, uiCallbacks, theme }: ChatRootProps) {
|
|
34
38
|
const { isOpen, variant } = useChatUI();
|
|
35
39
|
|
|
36
40
|
// Lock page scroll whenever the chat is open
|
|
@@ -42,7 +46,6 @@ export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
|
42
46
|
|
|
43
47
|
return createPortal(
|
|
44
48
|
// GalleryProvider is scoped to the chat only.
|
|
45
|
-
// It is completely separate from the host app's own gallery context.
|
|
46
49
|
<GalleryProvider>
|
|
47
50
|
<AnimatePresence mode="wait">
|
|
48
51
|
{isOpen && (
|
|
@@ -51,12 +54,14 @@ export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
|
51
54
|
key="inbox"
|
|
52
55
|
adapter={adapter}
|
|
53
56
|
uiCallbacks={uiCallbacks}
|
|
57
|
+
theme={theme}
|
|
54
58
|
/>
|
|
55
59
|
) : (
|
|
56
60
|
<SinglePopup
|
|
57
61
|
key="single"
|
|
58
62
|
adapter={adapter}
|
|
59
63
|
uiCallbacks={uiCallbacks}
|
|
64
|
+
theme={theme}
|
|
60
65
|
/>
|
|
61
66
|
)
|
|
62
67
|
)}
|
package/src/chat/InboxPopup.tsx
CHANGED
|
@@ -22,32 +22,47 @@ import ChatImagePreviewModal from "./ChatImagePreviewModal";
|
|
|
22
22
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
23
23
|
import type { Message, MessageRef, Thread } from "../types";
|
|
24
24
|
|
|
25
|
-
/*
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/* ─── Types ─── */
|
|
26
|
+
export type ChatTheme =
|
|
27
|
+
| "marketplace"
|
|
28
|
+
| "admin"
|
|
29
|
+
| { primary?: string; primaryActive?: string; surfaceLow?: string };
|
|
30
|
+
|
|
28
31
|
export type InboxPopupProps = {
|
|
29
32
|
adapter: ChatAdapter;
|
|
30
33
|
uiCallbacks?: ChatUICallbacks;
|
|
34
|
+
/** Dynamic theme: "marketplace" (orange), "admin" (black), or custom object */
|
|
35
|
+
theme?: ChatTheme;
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
/*
|
|
34
|
-
Constants
|
|
35
|
-
======================= */
|
|
38
|
+
/* ─── Helpers ─── */
|
|
36
39
|
const avatarBgByInitial: Record<string, string> = {
|
|
37
|
-
K: "#FFE7DB",
|
|
38
|
-
A: "#FFE5DA",
|
|
39
|
-
F: "#E8F7FF",
|
|
40
|
-
B: "#F0EDEB",
|
|
41
|
-
b: "#F0EDEB",
|
|
40
|
+
K: "#FFE7DB", A: "#FFE5DA", F: "#E8F7FF", B: "#F0EDEB", b: "#F0EDEB",
|
|
42
41
|
};
|
|
43
42
|
|
|
44
43
|
const GRADIENT_BORDER =
|
|
45
44
|
"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
45
|
|
|
47
|
-
|
|
46
|
+
function getThemeAttr(theme?: ChatTheme): string {
|
|
47
|
+
if (!theme || theme === "marketplace") return "marketplace";
|
|
48
|
+
if (theme === "admin") return "admin";
|
|
49
|
+
return "custom";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
53
|
+
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
54
|
+
// Custom theme object — set CSS vars directly
|
|
55
|
+
const vars: Record<string, string> = {};
|
|
56
|
+
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
57
|
+
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
58
|
+
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
59
|
+
return vars as React.CSSProperties;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ══════════════════════════════════════════════════
|
|
48
63
|
Component
|
|
49
|
-
|
|
50
|
-
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
64
|
+
══════════════════════════════════════════════════ */
|
|
65
|
+
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
51
66
|
const { close, selectThread, selectedThreadId, reference } = useChatUI();
|
|
52
67
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
53
68
|
|
|
@@ -62,10 +77,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
62
77
|
let rafId = 0;
|
|
63
78
|
rafId = requestAnimationFrame(refreshThreads);
|
|
64
79
|
const unsub = adapter.threads.subscribe(refreshThreads);
|
|
65
|
-
return () => {
|
|
66
|
-
cancelAnimationFrame(rafId);
|
|
67
|
-
unsub();
|
|
68
|
-
};
|
|
80
|
+
return () => { cancelAnimationFrame(rafId); unsub(); };
|
|
69
81
|
}, [adapter, reference, refreshThreads]);
|
|
70
82
|
|
|
71
83
|
/* ─── Active thread & messages ─── */
|
|
@@ -100,22 +112,13 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
100
112
|
const isVerified = Boolean(activeThread?.badge);
|
|
101
113
|
const avatarBg = avatarBgByInitial[initial] ?? "#FFF1EC";
|
|
102
114
|
|
|
103
|
-
const idLabel = activeThread?.orderId
|
|
104
|
-
|
|
105
|
-
: activeThread?.inquiryId
|
|
106
|
-
? "Inquiry ID"
|
|
107
|
-
: undefined;
|
|
108
|
-
const idButtonLabel = activeThread?.orderId
|
|
109
|
-
? "View Order"
|
|
110
|
-
: activeThread?.inquiryId
|
|
111
|
-
? "View Inquiry"
|
|
112
|
-
: undefined;
|
|
115
|
+
const idLabel = activeThread?.orderId ? "Order ID" : activeThread?.inquiryId ? "Inquiry ID" : undefined;
|
|
116
|
+
const idButtonLabel = activeThread?.orderId ? "View Order" : activeThread?.inquiryId ? "View Inquiry" : undefined;
|
|
113
117
|
const idValue = activeThread?.orderId ?? activeThread?.inquiryId ?? undefined;
|
|
114
118
|
|
|
115
119
|
const [showDelete, setShowDelete] = useState(false);
|
|
116
120
|
const scrollKey = `${activeId}-${messages.length}-${rev}`;
|
|
117
121
|
|
|
118
|
-
/* mark read on switch */
|
|
119
122
|
const prevActiveIdRef = useRef(activeId);
|
|
120
123
|
useEffect(() => {
|
|
121
124
|
if (prevActiveIdRef.current !== activeId) {
|
|
@@ -124,7 +127,6 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
124
127
|
}
|
|
125
128
|
}, [activeId, adapter]);
|
|
126
129
|
|
|
127
|
-
/* ─── Helpers ─── */
|
|
128
130
|
const toRef = (m: Message): MessageRef => ({
|
|
129
131
|
id: m.id,
|
|
130
132
|
author: typeof m.author === "string" ? m.author : "U",
|
|
@@ -136,23 +138,15 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
136
138
|
});
|
|
137
139
|
|
|
138
140
|
const handleConfirmDelete = () => {
|
|
139
|
-
if (!activeId) {
|
|
140
|
-
setShowDelete(false);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
141
|
+
if (!activeId) { setShowDelete(false); return; }
|
|
143
142
|
adapter.threads.delete(activeId);
|
|
144
143
|
const nextId = threads.filter((t) => t.id !== activeId)[0]?.id;
|
|
145
144
|
if (nextId) selectThread(nextId);
|
|
146
145
|
setReplyTo(undefined);
|
|
147
146
|
setShowDelete(false);
|
|
148
|
-
uiCallbacks?.showToast?.({
|
|
149
|
-
type: "success",
|
|
150
|
-
title: "Chat Deleted",
|
|
151
|
-
message: "The chat has been deleted successfully.",
|
|
152
|
-
});
|
|
147
|
+
uiCallbacks?.showToast?.({ type: "success", title: "Chat Deleted", message: "The chat has been deleted successfully." });
|
|
153
148
|
};
|
|
154
149
|
|
|
155
|
-
/* ─── Filtered threads ─── */
|
|
156
150
|
const filteredThreads = threads.filter((t) => {
|
|
157
151
|
if (!searchQuery.trim()) return true;
|
|
158
152
|
const q = searchQuery.toLowerCase();
|
|
@@ -168,30 +162,31 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
168
162
|
RENDER
|
|
169
163
|
══════════════════════════════════════════════════ */
|
|
170
164
|
return (
|
|
171
|
-
<div
|
|
165
|
+
<div className="fixed bottom-4 right-4 z-[10002]">
|
|
172
166
|
{/* Backdrop */}
|
|
173
167
|
<motion.button
|
|
174
168
|
aria-label="Close chat"
|
|
175
169
|
onClick={close}
|
|
176
|
-
|
|
170
|
+
className="fixed inset-0 cursor-auto!"
|
|
171
|
+
style={{ background: "transparent", border: "none" }}
|
|
177
172
|
initial={{ opacity: 0 }}
|
|
178
173
|
animate={{ opacity: 1 }}
|
|
179
174
|
exit={{ opacity: 0 }}
|
|
180
175
|
transition={{ type: "tween", duration: 0.25 }}
|
|
181
176
|
/>
|
|
182
177
|
|
|
183
|
-
{/* Outer gradient border
|
|
178
|
+
{/* Outer gradient border + theme root */}
|
|
184
179
|
<motion.div
|
|
185
180
|
role="dialog"
|
|
186
181
|
aria-modal="true"
|
|
182
|
+
data-theme={getThemeAttr(theme)}
|
|
183
|
+
className="banbox-chat-root relative rounded-[20px] p-[3px]"
|
|
187
184
|
style={{
|
|
188
|
-
position: "relative",
|
|
189
185
|
width: 800,
|
|
190
186
|
height: 650,
|
|
191
|
-
borderRadius: 20,
|
|
192
|
-
padding: 3,
|
|
193
187
|
boxShadow: "0px 2px 12px 0px #3B33331A",
|
|
194
188
|
background: GRADIENT_BORDER,
|
|
189
|
+
...getThemeVars(theme),
|
|
195
190
|
}}
|
|
196
191
|
initial={{ x: "110%" }}
|
|
197
192
|
animate={{ x: 0 }}
|
|
@@ -200,68 +195,31 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
200
195
|
>
|
|
201
196
|
{/* Inner white card */}
|
|
202
197
|
<div
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
height: "100%",
|
|
206
|
-
width: "100%",
|
|
207
|
-
overflow: "hidden",
|
|
208
|
-
borderRadius: 18,
|
|
209
|
-
backgroundColor: "#fff",
|
|
210
|
-
overscrollBehavior: "contain",
|
|
211
|
-
}}
|
|
198
|
+
className="relative h-full w-full overflow-hidden rounded-[18px] bg-white"
|
|
199
|
+
style={{ overscrollBehavior: "contain" }}
|
|
212
200
|
>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 }}>
|
|
201
|
+
<div className="pointer-events-none absolute inset-0 rounded-[14px] ring-1 ring-[#2F80ED]/40" />
|
|
202
|
+
|
|
203
|
+
{/* TWO-COLUMN GRID */}
|
|
204
|
+
<div className="grid h-full min-h-0 grid-cols-[1fr_310px]">
|
|
205
|
+
|
|
206
|
+
{/* ════ LEFT — chat ════ */}
|
|
207
|
+
<div className="flex h-full min-h-0 flex-col border-r border-[#9BBCCF]">
|
|
208
|
+
|
|
209
|
+
{/* Header */}
|
|
210
|
+
<div className="h-[64px] shrink-0">
|
|
234
211
|
<ChatHeader
|
|
235
212
|
left={
|
|
236
213
|
activeThread?.avatarSrc ? (
|
|
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
|
-
/>
|
|
214
|
+
<ChatIdentity variant="avatar" src={activeThread.avatarSrc} online={online} title={title} subtitle={subtitle} verified={isVerified} subtitleVariant="muted" />
|
|
246
215
|
) : (
|
|
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
|
-
/>
|
|
216
|
+
<ChatIdentity variant="initial" initial={initial} bg={avatarBg} online={online} title={title} subtitle={subtitle} verified={isVerified} subtitleVariant="muted" />
|
|
257
217
|
)
|
|
258
218
|
}
|
|
259
219
|
right={
|
|
260
220
|
uiCallbacks?.renderKebabMenu?.({
|
|
261
221
|
pinned: Boolean(activeThread?.pinned),
|
|
262
|
-
onPinToggle: () => {
|
|
263
|
-
if (activeId) adapter.threads.pin(activeId, !activeThread?.pinned);
|
|
264
|
-
},
|
|
222
|
+
onPinToggle: () => { if (activeId) adapter.threads.pin(activeId, !activeThread?.pinned); },
|
|
265
223
|
onDelete: () => setShowDelete(true),
|
|
266
224
|
}) ?? null
|
|
267
225
|
}
|
|
@@ -270,7 +228,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
270
228
|
|
|
271
229
|
{/* Optional inquiry bar */}
|
|
272
230
|
{idValue && (
|
|
273
|
-
<div
|
|
231
|
+
<div className="shrink-0">
|
|
274
232
|
<ChatInquiryBar
|
|
275
233
|
id={idValue}
|
|
276
234
|
label={idLabel}
|
|
@@ -283,66 +241,50 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
283
241
|
</div>
|
|
284
242
|
)}
|
|
285
243
|
|
|
286
|
-
{/* Messages
|
|
287
|
-
<div
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
m.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
m.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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" }} />
|
|
244
|
+
{/* Messages + typing */}
|
|
245
|
+
<div className="flex-1 min-h-0">
|
|
246
|
+
<div className="relative h-full min-h-0">
|
|
247
|
+
<ChatScroll
|
|
248
|
+
className="h-full pb-10"
|
|
249
|
+
bottomAlignWhenShort={false}
|
|
250
|
+
scrollKey={scrollKey}
|
|
251
|
+
>
|
|
252
|
+
{messages.map((m, idx) => {
|
|
253
|
+
const mine = m.author === "you";
|
|
254
|
+
const isLast = idx === messages.length - 1;
|
|
255
|
+
return (
|
|
256
|
+
<ChatMessageItem
|
|
257
|
+
key={m.id}
|
|
258
|
+
id={m.id}
|
|
259
|
+
mine={mine}
|
|
260
|
+
time={m.time ?? ""}
|
|
261
|
+
authorInitial={typeof m.author === "string" ? m.author : "U"}
|
|
262
|
+
avatarBg={avatarBg}
|
|
263
|
+
text={m.text ?? m.content}
|
|
264
|
+
businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
|
|
265
|
+
addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
|
|
266
|
+
images={m.images}
|
|
267
|
+
files={m.files}
|
|
268
|
+
audio={m.audio}
|
|
269
|
+
replyTo={m.replyTo}
|
|
270
|
+
showStatus={isLast}
|
|
271
|
+
status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
|
|
272
|
+
onReply={() => setReplyTo(toRef(m))}
|
|
273
|
+
initialSrc={m.avatarSrc}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</ChatScroll>
|
|
278
|
+
|
|
279
|
+
{/* Typing indicator — pinned at bottom */}
|
|
280
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-start px-4 pb-2 pt-1 bg-white">
|
|
281
|
+
<TypingIndicator className="pointer-events-auto" />
|
|
282
|
+
</div>
|
|
341
283
|
</div>
|
|
342
284
|
</div>
|
|
343
285
|
|
|
344
|
-
{/*
|
|
345
|
-
<div
|
|
286
|
+
{/* Footer */}
|
|
287
|
+
<div className="shrink-0">
|
|
346
288
|
<ChatFooter
|
|
347
289
|
key={activeId}
|
|
348
290
|
replyTo={replyTo}
|
|
@@ -355,28 +297,19 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
355
297
|
</div>
|
|
356
298
|
</div>
|
|
357
299
|
|
|
358
|
-
{/*
|
|
359
|
-
<div
|
|
360
|
-
|
|
361
|
-
<div style={{ flexShrink: 0 }}>
|
|
300
|
+
{/* ════ RIGHT — thread list ════ */}
|
|
301
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
302
|
+
<div className="shrink-0">
|
|
362
303
|
<ChatListHeader onClose={close} onSearchChange={(val) => setSearchQuery(val)} />
|
|
363
304
|
</div>
|
|
364
|
-
|
|
365
|
-
{/* Scrollable thread list */}
|
|
366
|
-
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
|
|
305
|
+
<div className="flex-1 min-h-0 overflow-y-auto custom-scroll">
|
|
367
306
|
{filteredThreads.map((t) => {
|
|
368
307
|
const status: ChatThreadStatus =
|
|
369
|
-
t.status ??
|
|
370
|
-
(t.unread && t.unread > 0
|
|
371
|
-
? { kind: "new", count: t.unread }
|
|
372
|
-
: { kind: "seen" });
|
|
308
|
+
t.status ?? (t.unread && t.unread > 0 ? { kind: "new", count: t.unread } : { kind: "seen" });
|
|
373
309
|
return (
|
|
374
310
|
<ChatThreadItem
|
|
375
311
|
key={t.id}
|
|
376
|
-
onClick={() => {
|
|
377
|
-
setReplyTo(undefined);
|
|
378
|
-
selectThread(t.id);
|
|
379
|
-
}}
|
|
312
|
+
onClick={() => { setReplyTo(undefined); selectThread(t.id); }}
|
|
380
313
|
active={t.id === activeId}
|
|
381
314
|
pinned={Boolean(t.pinned)}
|
|
382
315
|
online={t.online}
|
|
@@ -395,11 +328,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
395
328
|
</div>
|
|
396
329
|
|
|
397
330
|
{/* Modals */}
|
|
398
|
-
<ChatConfirmModal
|
|
399
|
-
open={showDelete}
|
|
400
|
-
onClose={() => setShowDelete(false)}
|
|
401
|
-
onConfirm={handleConfirmDelete}
|
|
402
|
-
/>
|
|
331
|
+
<ChatConfirmModal open={showDelete} onClose={() => setShowDelete(false)} onConfirm={handleConfirmDelete} />
|
|
403
332
|
<ChatImagePreviewModal isOpen={isGalleryOpen} onClose={closeGallery} />
|
|
404
333
|
</div>
|
|
405
334
|
</motion.div>
|