@banbox/chat 1.0.7 → 1.0.8
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 +110 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +108 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +78 -26
- package/src/ui/chat/ChatHeader.tsx +7 -4
- package/src/ui/chat/ChatListHeader.tsx +48 -24
- package/src/ui/chat/ChatScroll.tsx +23 -16
- package/src/ui/chat/ChatThreadItem.tsx +8 -15
package/package.json
CHANGED
package/src/chat/InboxPopup.tsx
CHANGED
|
@@ -31,13 +31,16 @@ export type ChatTheme =
|
|
|
31
31
|
export type InboxPopupProps = {
|
|
32
32
|
adapter: ChatAdapter;
|
|
33
33
|
uiCallbacks?: ChatUICallbacks;
|
|
34
|
-
/** Dynamic theme: "marketplace" (orange), "admin" (black), or custom object */
|
|
35
34
|
theme?: ChatTheme;
|
|
36
35
|
};
|
|
37
36
|
|
|
38
37
|
/* ─── Helpers ─── */
|
|
39
38
|
const avatarBgByInitial: Record<string, string> = {
|
|
40
|
-
K: "#FFE7DB",
|
|
39
|
+
K: "#FFE7DB",
|
|
40
|
+
A: "#FFF1EC",
|
|
41
|
+
F: "#E8F7FF",
|
|
42
|
+
B: "#F0EDEB",
|
|
43
|
+
b: "#F0EDEB",
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
const GRADIENT_BORDER =
|
|
@@ -51,7 +54,6 @@ function getThemeAttr(theme?: ChatTheme): string {
|
|
|
51
54
|
|
|
52
55
|
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
53
56
|
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
54
|
-
// Custom theme object — set CSS vars directly
|
|
55
57
|
const vars: Record<string, string> = {};
|
|
56
58
|
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
57
59
|
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
@@ -77,7 +79,10 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
77
79
|
let rafId = 0;
|
|
78
80
|
rafId = requestAnimationFrame(refreshThreads);
|
|
79
81
|
const unsub = adapter.threads.subscribe(refreshThreads);
|
|
80
|
-
return () => {
|
|
82
|
+
return () => {
|
|
83
|
+
cancelAnimationFrame(rafId);
|
|
84
|
+
unsub();
|
|
85
|
+
};
|
|
81
86
|
}, [adapter, reference, refreshThreads]);
|
|
82
87
|
|
|
83
88
|
/* ─── Active thread & messages ─── */
|
|
@@ -111,9 +116,18 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
111
116
|
const online = Boolean(activeThread?.online);
|
|
112
117
|
const isVerified = Boolean(activeThread?.badge);
|
|
113
118
|
const avatarBg = avatarBgByInitial[initial] ?? "#FFF1EC";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
119
|
+
const initialSrc = activeThread?.avatarSrc ?? "/chat/banbox_chat_logo.png";
|
|
120
|
+
|
|
121
|
+
const idLabel = activeThread?.orderId
|
|
122
|
+
? "Order ID"
|
|
123
|
+
: activeThread?.inquiryId
|
|
124
|
+
? "Inquiry ID"
|
|
125
|
+
: undefined;
|
|
126
|
+
const idButtonLabel = activeThread?.orderId
|
|
127
|
+
? "View Order"
|
|
128
|
+
: activeThread?.inquiryId
|
|
129
|
+
? "View Inquiry"
|
|
130
|
+
: undefined;
|
|
117
131
|
const idValue = activeThread?.orderId ?? activeThread?.inquiryId ?? undefined;
|
|
118
132
|
|
|
119
133
|
const [showDelete, setShowDelete] = useState(false);
|
|
@@ -138,13 +152,20 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
138
152
|
});
|
|
139
153
|
|
|
140
154
|
const handleConfirmDelete = () => {
|
|
141
|
-
if (!activeId) {
|
|
155
|
+
if (!activeId) {
|
|
156
|
+
setShowDelete(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
142
159
|
adapter.threads.delete(activeId);
|
|
143
160
|
const nextId = threads.filter((t) => t.id !== activeId)[0]?.id;
|
|
144
161
|
if (nextId) selectThread(nextId);
|
|
145
162
|
setReplyTo(undefined);
|
|
146
163
|
setShowDelete(false);
|
|
147
|
-
uiCallbacks?.showToast?.({
|
|
164
|
+
uiCallbacks?.showToast?.({
|
|
165
|
+
type: "success",
|
|
166
|
+
title: "Chat Deleted",
|
|
167
|
+
message: "The chat has been deleted successfully.",
|
|
168
|
+
});
|
|
148
169
|
};
|
|
149
170
|
|
|
150
171
|
const filteredThreads = threads.filter((t) => {
|
|
@@ -167,7 +188,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
167
188
|
<motion.button
|
|
168
189
|
aria-label="Close chat"
|
|
169
190
|
onClick={close}
|
|
170
|
-
className="fixed inset-0
|
|
191
|
+
className="fixed inset-0"
|
|
171
192
|
style={{ background: "transparent", border: "none" }}
|
|
172
193
|
initial={{ opacity: 0 }}
|
|
173
194
|
animate={{ opacity: 1 }}
|
|
@@ -200,8 +221,8 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
200
221
|
>
|
|
201
222
|
<div className="pointer-events-none absolute inset-0 rounded-[14px] ring-1 ring-[#2F80ED]/40" />
|
|
202
223
|
|
|
203
|
-
{/* TWO-COLUMN GRID */}
|
|
204
|
-
<div className="grid h-full min-h-0 grid-cols-[
|
|
224
|
+
{/* TWO-COLUMN GRID — exact marketplace: grid-cols-[1fr_350px] */}
|
|
225
|
+
<div className="grid h-full min-h-0 grid-cols-[1fr_350px]">
|
|
205
226
|
|
|
206
227
|
{/* ════ LEFT — chat ════ */}
|
|
207
228
|
<div className="flex h-full min-h-0 flex-col border-r border-[#9BBCCF]">
|
|
@@ -211,15 +232,34 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
211
232
|
<ChatHeader
|
|
212
233
|
left={
|
|
213
234
|
activeThread?.avatarSrc ? (
|
|
214
|
-
<ChatIdentity
|
|
235
|
+
<ChatIdentity
|
|
236
|
+
variant="avatar"
|
|
237
|
+
src={initialSrc}
|
|
238
|
+
online={online}
|
|
239
|
+
title={title}
|
|
240
|
+
subtitle={subtitle}
|
|
241
|
+
verified={isVerified}
|
|
242
|
+
subtitleVariant="muted"
|
|
243
|
+
/>
|
|
215
244
|
) : (
|
|
216
|
-
<ChatIdentity
|
|
245
|
+
<ChatIdentity
|
|
246
|
+
variant="initial"
|
|
247
|
+
initial={initial}
|
|
248
|
+
bg={avatarBg}
|
|
249
|
+
online={online}
|
|
250
|
+
title={title}
|
|
251
|
+
subtitle={subtitle}
|
|
252
|
+
verified={isVerified}
|
|
253
|
+
subtitleVariant="muted"
|
|
254
|
+
/>
|
|
217
255
|
)
|
|
218
256
|
}
|
|
219
257
|
right={
|
|
220
258
|
uiCallbacks?.renderKebabMenu?.({
|
|
221
259
|
pinned: Boolean(activeThread?.pinned),
|
|
222
|
-
onPinToggle: () => {
|
|
260
|
+
onPinToggle: () => {
|
|
261
|
+
if (activeId) adapter.threads.pin(activeId, !activeThread?.pinned);
|
|
262
|
+
},
|
|
223
263
|
onDelete: () => setShowDelete(true),
|
|
224
264
|
}) ?? null
|
|
225
265
|
}
|
|
@@ -261,8 +301,12 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
261
301
|
authorInitial={typeof m.author === "string" ? m.author : "U"}
|
|
262
302
|
avatarBg={avatarBg}
|
|
263
303
|
text={m.text ?? m.content}
|
|
264
|
-
businessCard={
|
|
265
|
-
|
|
304
|
+
businessCard={
|
|
305
|
+
m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]
|
|
306
|
+
}
|
|
307
|
+
addressCard={
|
|
308
|
+
m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]
|
|
309
|
+
}
|
|
266
310
|
images={m.images}
|
|
267
311
|
files={m.files}
|
|
268
312
|
audio={m.audio}
|
|
@@ -297,19 +341,23 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
297
341
|
</div>
|
|
298
342
|
</div>
|
|
299
343
|
|
|
300
|
-
{/* ════ RIGHT — thread list ════ */}
|
|
301
|
-
<div className="
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
</div>
|
|
305
|
-
<div className="flex-1 min-h-0 overflow-y-auto custom-scroll">
|
|
344
|
+
{/* ════ RIGHT — thread list (exact marketplace: h-full min-h-0 only) ════ */}
|
|
345
|
+
<div className="h-full min-h-0">
|
|
346
|
+
<ChatListHeader onClose={close} onSearchChange={(val) => setSearchQuery(val)} />
|
|
347
|
+
<div className="h-full overflow-y-auto custom-scroll py-0">
|
|
306
348
|
{filteredThreads.map((t) => {
|
|
307
349
|
const status: ChatThreadStatus =
|
|
308
|
-
t.status ??
|
|
350
|
+
t.status ??
|
|
351
|
+
(t.unread && t.unread > 0
|
|
352
|
+
? { kind: "new", count: t.unread }
|
|
353
|
+
: { kind: "seen" });
|
|
309
354
|
return (
|
|
310
355
|
<ChatThreadItem
|
|
311
356
|
key={t.id}
|
|
312
|
-
onClick={() => {
|
|
357
|
+
onClick={() => {
|
|
358
|
+
setReplyTo(undefined);
|
|
359
|
+
selectThread(t.id);
|
|
360
|
+
}}
|
|
313
361
|
active={t.id === activeId}
|
|
314
362
|
pinned={Boolean(t.pinned)}
|
|
315
363
|
online={t.online}
|
|
@@ -328,7 +376,11 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
328
376
|
</div>
|
|
329
377
|
|
|
330
378
|
{/* Modals */}
|
|
331
|
-
<ChatConfirmModal
|
|
379
|
+
<ChatConfirmModal
|
|
380
|
+
open={showDelete}
|
|
381
|
+
onClose={() => setShowDelete(false)}
|
|
382
|
+
onConfirm={handleConfirmDelete}
|
|
383
|
+
/>
|
|
332
384
|
<ChatImagePreviewModal isOpen={isGalleryOpen} onClose={closeGallery} />
|
|
333
385
|
</div>
|
|
334
386
|
</motion.div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import clsx from "clsx";
|
|
2
3
|
import React from "react";
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
@@ -10,10 +11,12 @@ type Props = {
|
|
|
10
11
|
|
|
11
12
|
export default function ChatHeader({ left, right, below, className }: Props) {
|
|
12
13
|
return (
|
|
13
|
-
<div
|
|
14
|
-
<div className="
|
|
15
|
-
<div className="flex items-center
|
|
16
|
-
|
|
14
|
+
<div>
|
|
15
|
+
<div className={clsx("border-b border-[#ededed] h-[64px]", className)}>
|
|
16
|
+
<div className="flex items-center justify-between px-4 pt-2.5">
|
|
17
|
+
<div className="flex items-center gap-3">{left}</div>
|
|
18
|
+
{right}
|
|
19
|
+
</div>
|
|
17
20
|
</div>
|
|
18
21
|
{below && <>{below}</>}
|
|
19
22
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import clsx from "clsx";
|
|
3
4
|
import type { Variants } from "framer-motion";
|
|
4
5
|
import { AnimatePresence, motion } from "framer-motion";
|
|
5
6
|
import React from "react";
|
|
@@ -9,22 +10,34 @@ type Props = {
|
|
|
9
10
|
className?: string;
|
|
10
11
|
onClose?: () => void;
|
|
11
12
|
onSearchChange?: (value: string) => void;
|
|
13
|
+
hideSearch?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
const ChatListHeader: React.FC<Props> = ({
|
|
16
|
+
const ChatListHeader: React.FC<Props> = ({
|
|
17
|
+
className,
|
|
18
|
+
onClose,
|
|
19
|
+
onSearchChange,
|
|
20
|
+
hideSearch = false,
|
|
21
|
+
}) => {
|
|
15
22
|
const [searching, setSearching] = React.useState(false);
|
|
16
23
|
const [q, setQ] = React.useState("");
|
|
17
24
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
18
25
|
|
|
19
26
|
React.useEffect(() => {
|
|
20
27
|
const timer = searching
|
|
21
|
-
? setTimeout(() => {
|
|
28
|
+
? setTimeout(() => {
|
|
29
|
+
inputRef.current?.focus();
|
|
30
|
+
}, 220)
|
|
22
31
|
: undefined;
|
|
23
|
-
return () => {
|
|
32
|
+
return () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
};
|
|
24
35
|
}, [searching]);
|
|
25
36
|
|
|
26
37
|
React.useEffect(() => {
|
|
27
|
-
if (!searching)
|
|
38
|
+
if (!searching) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
28
41
|
const onKey = (e: KeyboardEvent) => {
|
|
29
42
|
if (e.key === "Escape") {
|
|
30
43
|
setSearching(false);
|
|
@@ -33,18 +46,20 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
33
46
|
}
|
|
34
47
|
};
|
|
35
48
|
window.addEventListener("keydown", onKey);
|
|
36
|
-
return () => {
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener("keydown", onKey);
|
|
51
|
+
};
|
|
37
52
|
}, [searching, onSearchChange]);
|
|
38
53
|
|
|
39
54
|
const variants: Variants = {
|
|
40
55
|
inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
41
|
-
outToLeft:
|
|
42
|
-
inFromLeft:
|
|
43
|
-
outToRight:
|
|
56
|
+
outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
57
|
+
inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
58
|
+
outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
44
59
|
};
|
|
45
60
|
|
|
46
61
|
return (
|
|
47
|
-
<div className={
|
|
62
|
+
<div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
|
|
48
63
|
<div className="flex h-full items-center px-[20px]">
|
|
49
64
|
<AnimatePresence initial={false} mode="wait">
|
|
50
65
|
{!searching ? (
|
|
@@ -64,13 +79,16 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
64
79
|
</div>
|
|
65
80
|
|
|
66
81
|
<div className="flex items-center gap-2">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
{!hideSearch && (
|
|
83
|
+
<button
|
|
84
|
+
title="Search"
|
|
85
|
+
onClick={() => setSearching(true)}
|
|
86
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center cursor-pointer border-none bg-transparent"
|
|
87
|
+
>
|
|
88
|
+
<ChatSearchIcon className="w-5 h-5" />
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
|
|
74
92
|
<button
|
|
75
93
|
title="Close"
|
|
76
94
|
onClick={onClose}
|
|
@@ -95,7 +113,7 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
95
113
|
<span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
|
|
96
114
|
<ChatSearchIcon className="w-5 h-5" />
|
|
97
115
|
</span>
|
|
98
|
-
|
|
116
|
+
|
|
99
117
|
<input
|
|
100
118
|
ref={inputRef}
|
|
101
119
|
value={q}
|
|
@@ -108,13 +126,19 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
108
126
|
/>
|
|
109
127
|
</div>
|
|
110
128
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
<div>
|
|
130
|
+
<button
|
|
131
|
+
title="Close search"
|
|
132
|
+
onClick={() => {
|
|
133
|
+
setSearching(false);
|
|
134
|
+
setQ("");
|
|
135
|
+
onSearchChange?.("");
|
|
136
|
+
}}
|
|
137
|
+
className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5 cursor-pointer border-none bg-transparent"
|
|
138
|
+
>
|
|
139
|
+
<ChatXIcon className="w-5 h-5" />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
118
142
|
</div>
|
|
119
143
|
</div>
|
|
120
144
|
</motion.div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
// components/ui/chat/ChatScroll.tsx
|
|
1
2
|
"use client";
|
|
3
|
+
import clsx from "clsx";
|
|
2
4
|
import React from "react";
|
|
3
5
|
|
|
4
6
|
type Props = {
|
|
@@ -6,47 +8,52 @@ type Props = {
|
|
|
6
8
|
children: React.ReactNode;
|
|
7
9
|
className?: string;
|
|
8
10
|
style?: React.CSSProperties;
|
|
11
|
+
/** set true if you want short threads anchored at the bottom */
|
|
9
12
|
bottomAlignWhenShort?: boolean;
|
|
13
|
+
/** when this value changes, we auto-scroll to the bottom */
|
|
10
14
|
scrollKey?: string | number;
|
|
11
15
|
};
|
|
12
16
|
|
|
13
|
-
const ChatScroll
|
|
14
|
-
top,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
style,
|
|
18
|
-
bottomAlignWhenShort = false,
|
|
19
|
-
scrollKey,
|
|
20
|
-
}) => {
|
|
17
|
+
const ChatScroll = React.forwardRef<HTMLDivElement, Props>(function ChatScroll(
|
|
18
|
+
{ top, children, className, bottomAlignWhenShort = false, scrollKey },
|
|
19
|
+
_,
|
|
20
|
+
) {
|
|
21
21
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
22
22
|
|
|
23
23
|
const scrollToBottom = React.useCallback(() => {
|
|
24
24
|
const el = ref.current;
|
|
25
|
-
if (!el)
|
|
25
|
+
if (!el) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
26
28
|
el.scrollTop = el.scrollHeight;
|
|
27
29
|
}, []);
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
// On mount & when scrollKey changes — useLayoutEffect guarantees synchronous scroll before paint!
|
|
32
|
+
React.useLayoutEffect(() => {
|
|
30
33
|
scrollToBottom();
|
|
31
|
-
const id = window.setTimeout(scrollToBottom, 0);
|
|
32
|
-
return () => window.clearTimeout(id);
|
|
33
34
|
}, [scrollKey, scrollToBottom]);
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
37
|
<div
|
|
37
38
|
ref={ref}
|
|
38
39
|
data-chat-scroll
|
|
39
|
-
className={
|
|
40
|
-
|
|
40
|
+
className={clsx(
|
|
41
|
+
"h-full min-h-0 overflow-y-auto bg-white p-4 custom-scroll-hidden",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
41
44
|
>
|
|
45
|
+
{/* This wrapper ensures content is at least as tall as the scroll area */}
|
|
42
46
|
<div
|
|
43
|
-
className={
|
|
47
|
+
className={clsx(
|
|
48
|
+
"min-h-full flex flex-col",
|
|
49
|
+
bottomAlignWhenShort ? "justify-end" : "justify-between",
|
|
50
|
+
)}
|
|
44
51
|
>
|
|
45
52
|
{top}
|
|
46
53
|
{children}
|
|
47
54
|
</div>
|
|
48
55
|
</div>
|
|
49
56
|
);
|
|
50
|
-
};
|
|
57
|
+
});
|
|
51
58
|
|
|
52
59
|
export default ChatScroll;
|
|
@@ -16,14 +16,13 @@ type Props = {
|
|
|
16
16
|
verified?: boolean;
|
|
17
17
|
|
|
18
18
|
title: string;
|
|
19
|
-
preview: string;
|
|
20
|
-
time: string;
|
|
19
|
+
preview: string;
|
|
20
|
+
time: string;
|
|
21
21
|
status: ChatThreadStatus;
|
|
22
22
|
|
|
23
|
-
avatarText: string;
|
|
24
|
-
avatarSrc?: string;
|
|
25
|
-
|
|
26
|
-
avatarBg?: string; // default soft peach
|
|
23
|
+
avatarText: string;
|
|
24
|
+
avatarSrc?: string;
|
|
25
|
+
avatarBg?: string;
|
|
27
26
|
className?: string;
|
|
28
27
|
onClick?: () => void;
|
|
29
28
|
};
|
|
@@ -39,28 +38,22 @@ const ChatThreadItem: React.FC<Props> = ({
|
|
|
39
38
|
status,
|
|
40
39
|
avatarText,
|
|
41
40
|
avatarSrc,
|
|
42
|
-
_size = 46,
|
|
43
41
|
avatarBg = "#FFF1EC",
|
|
44
42
|
className,
|
|
45
43
|
onClick,
|
|
46
44
|
}) => {
|
|
47
|
-
const count = status.kind === "new" ? String(Math.max(0, status.count)).padStart(2, "0") : "";
|
|
48
|
-
|
|
49
45
|
const statusEl = (() => {
|
|
50
46
|
switch (status.kind) {
|
|
51
47
|
case "seen":
|
|
52
48
|
return <span className="text-[#0D5EA8]">Seen</span>;
|
|
53
|
-
|
|
54
49
|
case "delivered":
|
|
55
50
|
return <span className="text-[#B7B7B7]">Delivered</span>;
|
|
56
|
-
|
|
57
51
|
case "new":
|
|
58
52
|
return (
|
|
59
53
|
<span className="text-[#E63946]">
|
|
60
|
-
{count} New
|
|
54
|
+
{String(Math.max(0, status.count)).padStart(2, "0")} New
|
|
61
55
|
</span>
|
|
62
56
|
);
|
|
63
|
-
|
|
64
57
|
default:
|
|
65
58
|
return null;
|
|
66
59
|
}
|
|
@@ -81,10 +74,10 @@ const ChatThreadItem: React.FC<Props> = ({
|
|
|
81
74
|
)}
|
|
82
75
|
|
|
83
76
|
<div className="flex items-start gap-3 border-b border-[#f8f8f8] pb-2">
|
|
84
|
-
{/* Avatar + online */}
|
|
77
|
+
{/* Avatar + online dot */}
|
|
85
78
|
<div className="relative mt-[2px]">
|
|
86
79
|
{avatarSrc ? (
|
|
87
|
-
<div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1]
|
|
80
|
+
<div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1]">
|
|
88
81
|
<img
|
|
89
82
|
src={avatarSrc}
|
|
90
83
|
alt={title}
|