@banbox/chat 1.0.17 → 1.0.19
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 +191 -166
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +191 -166
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/ChatImagePreviewModal.tsx +55 -47
- package/src/chat/SinglePopup.tsx +81 -31
- package/src/modals/ChatTranslateSettingsModal.tsx +2 -2
- package/src/ui/AttachmentPreviewStrip.tsx +17 -12
- package/src/ui/ChatComposerBar.tsx +21 -13
- package/src/ui/ChatFooter.tsx +21 -11
- package/src/ui/ChatInquiryBar.tsx +1 -1
- package/src/ui/ChatThreadItem.tsx +1 -1
- package/src/ui/MessageHoverActions.tsx +2 -2
- package/src/ui/ReplyCard.tsx +1 -1
- package/src/ui/Select.tsx +2 -1
- package/src/ui/drop-up/BusinessCardDropup.tsx +1 -1
- package/src/ui/drop-up/EmojiDropup.tsx +3 -3
- package/src/ui/message-items/ChatBubbleAudio.tsx +1 -1
- package/src/ui/message-items/ChatBubbleFiles.tsx +8 -2
package/package.json
CHANGED
|
@@ -18,14 +18,17 @@ interface ChatImagePreviewModalProps {
|
|
|
18
18
|
/* =======================
|
|
19
19
|
Slide direction variants
|
|
20
20
|
======================= */
|
|
21
|
+
// Percentage-based so the slide distance always matches the container width exactly.
|
|
22
|
+
// Both exit and enter run simultaneously (no mode="wait"), giving a natural
|
|
23
|
+
// cross-slide feel without the jerk caused by a sequential pause.
|
|
21
24
|
const slideVariants = {
|
|
22
25
|
enter: (dir: number) => ({
|
|
23
|
-
x: dir > 0 ?
|
|
26
|
+
x: dir > 0 ? "100%" : "-100%",
|
|
24
27
|
opacity: 0,
|
|
25
28
|
}),
|
|
26
29
|
center: { x: 0, opacity: 1 },
|
|
27
30
|
exit: (dir: number) => ({
|
|
28
|
-
x: dir > 0 ? -
|
|
31
|
+
x: dir > 0 ? "-100%" : "100%",
|
|
29
32
|
opacity: 0,
|
|
30
33
|
}),
|
|
31
34
|
};
|
|
@@ -80,7 +83,7 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
|
|
|
80
83
|
<AnimatePresence>
|
|
81
84
|
{isOpen && total > 0 && (
|
|
82
85
|
<motion.div
|
|
83
|
-
className="fixed inset-0 z-
|
|
86
|
+
className="fixed inset-0 z-10010 flex items-center justify-center"
|
|
84
87
|
initial={{ opacity: 0, backgroundColor: "rgba(0,0,0,0)" }}
|
|
85
88
|
animate={{ opacity: 1, backgroundColor: "rgba(0,0,0,0.55)" }}
|
|
86
89
|
exit={{ opacity: 0, backgroundColor: "rgba(0,0,0,0)" }}
|
|
@@ -96,13 +99,13 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
|
|
|
96
99
|
transition={{ duration: 0.25, ease: "easeInOut" }}
|
|
97
100
|
onClick={(e) => e.stopPropagation()}
|
|
98
101
|
>
|
|
99
|
-
{/* Fixed
|
|
102
|
+
{/* Fixed 953×679 frame — image adapts dynamically inside */}
|
|
100
103
|
<div
|
|
101
104
|
className="relative flex items-center justify-center overflow-hidden rounded-[6px] bg-white"
|
|
102
|
-
style={{ width:
|
|
105
|
+
style={{ width: 953, height: 679 }}
|
|
103
106
|
>
|
|
104
|
-
{/* Sliding image */}
|
|
105
|
-
<AnimatePresence
|
|
107
|
+
{/* Sliding image — both exit and enter animate together (no mode="wait") */}
|
|
108
|
+
<AnimatePresence initial={false} custom={direction}>
|
|
106
109
|
<motion.img
|
|
107
110
|
key={current}
|
|
108
111
|
custom={direction}
|
|
@@ -110,53 +113,58 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
|
|
|
110
113
|
initial="enter"
|
|
111
114
|
animate="center"
|
|
112
115
|
exit="exit"
|
|
113
|
-
transition={{ duration: 0.
|
|
116
|
+
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
|
114
117
|
src={currentImage?.url}
|
|
115
118
|
alt={currentImage?.altText ?? `Image ${current + 1}`}
|
|
116
|
-
className="w-full rounded-[6px] object-contain"
|
|
119
|
+
className="absolute inset-0 w-full h-full rounded-[6px] object-contain"
|
|
117
120
|
draggable={false}
|
|
118
121
|
/>
|
|
119
122
|
</AnimatePresence>
|
|
120
123
|
|
|
121
|
-
{/*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
{/* Arrows — only shown when there are multiple images */}
|
|
125
|
+
{total > 1 && (
|
|
126
|
+
<>
|
|
127
|
+
{/* Left arrow */}
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={goPrev}
|
|
131
|
+
disabled={!hasPrev}
|
|
132
|
+
className={`absolute left-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tr-[3px] rounded-br-[3px] p-[7px] backdrop-blur-[2px] shadow-[3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-all duration-200 ${hasPrev ? "cursor-pointer opacity-100 hover:bg-white hover:shadow-[3px_0px_10px_0px_rgba(0,0,0,0.18)]" : "cursor-default opacity-30"}`}
|
|
133
|
+
style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
|
|
134
|
+
aria-label="Previous image"
|
|
135
|
+
>
|
|
136
|
+
<svg width="42" height="42" viewBox="0 0 24 24" fill="none">
|
|
137
|
+
<path
|
|
138
|
+
d="M15 18L9 12L15 6"
|
|
139
|
+
stroke="#2C2C2C"
|
|
140
|
+
strokeWidth="2"
|
|
141
|
+
strokeLinecap="round"
|
|
142
|
+
strokeLinejoin="round"
|
|
143
|
+
/>
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
140
146
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
{/* Right arrow */}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={goNext}
|
|
151
|
+
disabled={!hasNext}
|
|
152
|
+
className={`absolute right-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tl-[3px] rounded-bl-[3px] p-[7px] backdrop-blur-[2px] shadow-[-3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-all duration-200 ${hasNext ? "cursor-pointer opacity-100 hover:bg-white hover:shadow-[-3px_0px_10px_0px_rgba(0,0,0,0.18)]" : "cursor-default opacity-30"}`}
|
|
153
|
+
style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
|
|
154
|
+
aria-label="Next image"
|
|
155
|
+
>
|
|
156
|
+
<svg width="42" height="42" viewBox="0 0 24 24" fill="none">
|
|
157
|
+
<path
|
|
158
|
+
d="M9 18L15 12L9 6"
|
|
159
|
+
stroke="#2C2C2C"
|
|
160
|
+
strokeWidth="2"
|
|
161
|
+
strokeLinecap="round"
|
|
162
|
+
strokeLinejoin="round"
|
|
163
|
+
/>
|
|
164
|
+
</svg>
|
|
165
|
+
</button>
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
160
168
|
</div>
|
|
161
169
|
|
|
162
170
|
{/* Close button — top-right outside the image */}
|
package/src/chat/SinglePopup.tsx
CHANGED
|
@@ -22,9 +22,10 @@ import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
|
22
22
|
import type { Message, MessageRef, Reference, Thread } from "../types";
|
|
23
23
|
import type { ChatTheme } from "./InboxPopup";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
function coalesceThreadId(
|
|
26
|
+
reference: Reference | undefined,
|
|
27
|
+
threads: Thread[],
|
|
28
|
+
): string {
|
|
28
29
|
if (!reference?.id) return threads[0]?.id ?? "";
|
|
29
30
|
const refId = reference.id;
|
|
30
31
|
// Priority: exact thread.id match → orderId match → inquiryId match → first thread
|
|
@@ -51,16 +52,34 @@ function toRef(m: Message): MessageRef {
|
|
|
51
52
|
|
|
52
53
|
/** Maps the first letter of a name to a deterministic background colour. */
|
|
53
54
|
const avatarBgByInitial: Record<string, string> = {
|
|
54
|
-
a: "#FFE4E4",
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
a: "#FFE4E4",
|
|
56
|
+
b: "#E4F0FF",
|
|
57
|
+
c: "#E4FFE9",
|
|
58
|
+
d: "#FFF4E4",
|
|
59
|
+
e: "#F4E4FF",
|
|
60
|
+
f: "#FFE4F4",
|
|
61
|
+
g: "#E4FFFF",
|
|
62
|
+
h: "#FFFFE4",
|
|
63
|
+
i: "#E4E4FF",
|
|
64
|
+
j: "#FFE9E4",
|
|
65
|
+
k: "#E4FFE4",
|
|
66
|
+
l: "#FFE4EA",
|
|
67
|
+
m: "#E8E4FF",
|
|
68
|
+
n: "#E4F8FF",
|
|
69
|
+
o: "#FFF0E4",
|
|
70
|
+
p: "#F0FFE4",
|
|
71
|
+
q: "#FFE4F8",
|
|
72
|
+
r: "#E4FFEC",
|
|
73
|
+
s: "#FFEEE4",
|
|
74
|
+
t: "#E4EAFF",
|
|
75
|
+
u: "#F8FFE4",
|
|
76
|
+
v: "#FFE4EE",
|
|
77
|
+
w: "#E4FFFA",
|
|
78
|
+
x: "#FFF8E4",
|
|
79
|
+
y: "#EAE4FF",
|
|
59
80
|
z: "#E4FFF0",
|
|
60
81
|
};
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
83
|
export type SinglePopupProps = {
|
|
65
84
|
adapter: ChatAdapter;
|
|
66
85
|
uiCallbacks?: ChatUICallbacks;
|
|
@@ -75,12 +94,19 @@ export type SinglePopupProps = {
|
|
|
75
94
|
/* ══════════════════════════════════════════════════
|
|
76
95
|
Component
|
|
77
96
|
══════════════════════════════════════════════════ */
|
|
78
|
-
const SinglePopup: React.FC<SinglePopupProps> = ({
|
|
97
|
+
const SinglePopup: React.FC<SinglePopupProps> = ({
|
|
98
|
+
adapter,
|
|
99
|
+
uiCallbacks,
|
|
100
|
+
theme,
|
|
101
|
+
footerActions,
|
|
102
|
+
}) => {
|
|
79
103
|
const { close, reference } = useChatUI();
|
|
80
104
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
81
105
|
|
|
82
106
|
// ── Threads — subscribed so real-API updates (new msg, pin, delete) are reflected
|
|
83
|
-
const [threads, setThreads] = React.useState<Thread[]>(() =>
|
|
107
|
+
const [threads, setThreads] = React.useState<Thread[]>(() =>
|
|
108
|
+
adapter.threads.list(reference),
|
|
109
|
+
);
|
|
84
110
|
React.useEffect(() => {
|
|
85
111
|
// Refresh once on mount (covers any gap between render and subscribe)
|
|
86
112
|
setThreads(adapter.threads.list(reference));
|
|
@@ -97,7 +123,9 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
97
123
|
);
|
|
98
124
|
const [activeId] = React.useState<string>(initialThreadId);
|
|
99
125
|
|
|
100
|
-
const activeThread: Thread | undefined = threads.find(
|
|
126
|
+
const activeThread: Thread | undefined = threads.find(
|
|
127
|
+
(t) => t.id === activeId,
|
|
128
|
+
);
|
|
101
129
|
const isVerified = activeThread?.badge === true;
|
|
102
130
|
|
|
103
131
|
const meta = (reference?.meta ?? {}) as {
|
|
@@ -114,13 +142,11 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
114
142
|
const avatarSrc: string | undefined =
|
|
115
143
|
meta.avatarSrc ?? activeThread?.avatarSrc;
|
|
116
144
|
|
|
117
|
-
const initial =
|
|
145
|
+
const initial =
|
|
118
146
|
meta.initial ??
|
|
119
147
|
activeThread?.avatarText ??
|
|
120
|
-
(activeThread?.title ?? meta.title ?? "?").charAt(0).toUpperCase()
|
|
121
|
-
);
|
|
122
|
-
const avatarBg =
|
|
123
|
-
avatarBgByInitial[initial.toLowerCase()] ?? "#E4F0FF";
|
|
148
|
+
(activeThread?.title ?? meta.title ?? "?").charAt(0).toUpperCase();
|
|
149
|
+
const avatarBg = avatarBgByInitial[initial.toLowerCase()] ?? "#E4F0FF";
|
|
124
150
|
|
|
125
151
|
const title = meta.title ?? activeThread?.title ?? "Unknown";
|
|
126
152
|
const online = meta.online ?? activeThread?.online ?? false;
|
|
@@ -130,7 +156,9 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
130
156
|
activeId ? adapter.messages.list(activeId) : [],
|
|
131
157
|
);
|
|
132
158
|
const [scrollKey, setScrollKey] = React.useState<number>(Date.now());
|
|
133
|
-
const [replyTo, setReplyTo] = React.useState<MessageRef | undefined>(
|
|
159
|
+
const [replyTo, setReplyTo] = React.useState<MessageRef | undefined>(
|
|
160
|
+
undefined,
|
|
161
|
+
);
|
|
134
162
|
const [showDelete, setShowDelete] = React.useState(false);
|
|
135
163
|
|
|
136
164
|
React.useEffect(() => {
|
|
@@ -149,7 +177,10 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
149
177
|
}, [activeId, adapter]);
|
|
150
178
|
|
|
151
179
|
const handleConfirmDelete = React.useCallback(() => {
|
|
152
|
-
if (!activeId) {
|
|
180
|
+
if (!activeId) {
|
|
181
|
+
setShowDelete(false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
153
184
|
adapter.threads.delete(activeId);
|
|
154
185
|
uiCallbacks?.showToast?.({
|
|
155
186
|
type: "success",
|
|
@@ -190,9 +221,12 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
190
221
|
exit={{ x: "110%" }}
|
|
191
222
|
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
192
223
|
>
|
|
193
|
-
{/* Inner white card
|
|
224
|
+
{/* Inner white card — `relative` is required so that ChatTranslateSettingsModal's
|
|
225
|
+
`absolute inset-0` overlay is contained here rather than escaping to an outer
|
|
226
|
+
positioned ancestor. Combined with `overflow-hidden` this clips the backdrop
|
|
227
|
+
to the rounded corners of the popup. */}
|
|
194
228
|
<div
|
|
195
|
-
className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
|
|
229
|
+
className="relative flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
|
|
196
230
|
style={{ overscrollBehavior: "contain" }}
|
|
197
231
|
>
|
|
198
232
|
{/* Header — 64px */}
|
|
@@ -250,29 +284,45 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
|
|
|
250
284
|
id={m.id}
|
|
251
285
|
mine={mine}
|
|
252
286
|
time={m.time ?? ""}
|
|
253
|
-
authorInitial={
|
|
287
|
+
authorInitial={
|
|
288
|
+
typeof m.author === "string" ? m.author : "U"
|
|
289
|
+
}
|
|
254
290
|
text={m.text ?? m.content}
|
|
255
|
-
businessCard={
|
|
256
|
-
|
|
291
|
+
businessCard={
|
|
292
|
+
m.businessCard as Parameters<
|
|
293
|
+
typeof ChatMessageItem
|
|
294
|
+
>[0]["businessCard"]
|
|
295
|
+
}
|
|
296
|
+
addressCard={
|
|
297
|
+
m.addressCard as Parameters<
|
|
298
|
+
typeof ChatMessageItem
|
|
299
|
+
>[0]["addressCard"]
|
|
300
|
+
}
|
|
257
301
|
images={m.images}
|
|
258
302
|
files={m.files}
|
|
259
303
|
audio={m.audio}
|
|
260
304
|
replyTo={m.replyTo}
|
|
261
305
|
initialSrc={m.avatarSrc}
|
|
262
306
|
showStatus={isLast}
|
|
263
|
-
status={
|
|
307
|
+
status={
|
|
308
|
+
activeThread?.status?.kind === "seen"
|
|
309
|
+
? "Seen"
|
|
310
|
+
: "Delivered"
|
|
311
|
+
}
|
|
264
312
|
onReply={() => setReplyTo(toRef(m))}
|
|
265
313
|
/>
|
|
266
314
|
);
|
|
267
315
|
})}
|
|
268
|
-
|
|
269
|
-
{/* Typing indicator */}
|
|
270
|
-
<div className="flex items-center justify-start">
|
|
271
|
-
<TypingIndicator />
|
|
272
|
-
</div>
|
|
273
316
|
</ChatScroll>
|
|
274
317
|
</div>
|
|
275
318
|
|
|
319
|
+
{/* Typing indicator — sticky above footer */}
|
|
320
|
+
<div className="shrink-0">
|
|
321
|
+
<div className="flex items-center justify-start py-2 px-[16px]">
|
|
322
|
+
<TypingIndicator />
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
276
326
|
{/* Footer */}
|
|
277
327
|
<div className="shrink-0">
|
|
278
328
|
<ChatFooter
|
|
@@ -97,7 +97,7 @@ const ChatTranslateSettingsModal: React.FC<Props> = ({
|
|
|
97
97
|
}}
|
|
98
98
|
>
|
|
99
99
|
{/* Semi-transparent backdrop over the chat panel */}
|
|
100
|
-
<div className="absolute inset-0 bg-black/30 rounded-[inherit]" />
|
|
100
|
+
<div className="absolute inset-0 bg-black/30 rounded-[inherit] overflow-hidden" />
|
|
101
101
|
|
|
102
102
|
{/* Modal card */}
|
|
103
103
|
<div
|
|
@@ -107,7 +107,7 @@ const ChatTranslateSettingsModal: React.FC<Props> = ({
|
|
|
107
107
|
onClick={(e) => e.stopPropagation()}
|
|
108
108
|
className={clsx(
|
|
109
109
|
"relative w-[460px] max-w-[95%]",
|
|
110
|
-
"
|
|
110
|
+
"rounded-[6px] bg-white shadow-[0px_2px_12px_0px_rgba(59,51,51,0.1)]",
|
|
111
111
|
className,
|
|
112
112
|
)}
|
|
113
113
|
>
|
|
@@ -24,7 +24,7 @@ export const FilePreviewChip: React.FC<{
|
|
|
24
24
|
ext: string;
|
|
25
25
|
onRemove: () => void;
|
|
26
26
|
}> = ({ name, sizeMB, ext, onRemove }) => (
|
|
27
|
-
<div className="mr-2 inline-flex items-center gap-
|
|
27
|
+
<div className="mr-2 inline-flex items-center gap-2 whitespace-nowrap rounded-sm border border-[#e1e1e1] bg-white px-3 py-2 h-[65px] max-w-[185px]">
|
|
28
28
|
<div className="flex min-w-0 items-center gap-2">
|
|
29
29
|
<div className="min-w-0">
|
|
30
30
|
<div className="flex items-center gap-1">
|
|
@@ -38,25 +38,30 @@ export const FilePreviewChip: React.FC<{
|
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
<span>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onRemove}
|
|
45
|
+
className="grid h-[24px] w-[24px] cursor-pointer place-items-center rounded-full bg-white text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5"
|
|
46
|
+
title="Remove"
|
|
47
|
+
aria-label="Remove file"
|
|
48
|
+
>
|
|
49
|
+
<ChatXIcon className="h-[18px] w-[18px]" />
|
|
50
|
+
</button>
|
|
51
|
+
</span>
|
|
50
52
|
</div>
|
|
51
53
|
);
|
|
52
54
|
|
|
53
|
-
const ImageThumb: React.FC<{ url: string; onRemove: () => void }> = ({
|
|
55
|
+
const ImageThumb: React.FC<{ url: string; onRemove: () => void }> = ({
|
|
56
|
+
url,
|
|
57
|
+
onRemove,
|
|
58
|
+
}) => (
|
|
54
59
|
<div className="relative mr-2 inline-block h-[65px] w-[65px] rounded-sm border border-[#e1e1e1] bg-[#F7F7F7]">
|
|
55
60
|
<img src={url} alt="" className="h-full w-full object-cover rounded-sm" />
|
|
56
61
|
<button
|
|
57
62
|
type="button"
|
|
58
63
|
onClick={onRemove}
|
|
59
|
-
className="absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-black/30 text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D]"
|
|
64
|
+
className="absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 cursor-pointer place-items-center rounded-full bg-black/30 text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D]"
|
|
60
65
|
aria-label="Remove image"
|
|
61
66
|
title="Remove image"
|
|
62
67
|
>
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import React, { useRef, useState } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ArrowSendAngleIcon,
|
|
8
|
+
ArrowSendIcon,
|
|
9
|
+
RecordMicIcon,
|
|
10
|
+
ChatXIcon,
|
|
11
|
+
} from "../icons";
|
|
7
12
|
|
|
8
13
|
type Props = {
|
|
9
14
|
recording: boolean;
|
|
@@ -63,18 +68,18 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
63
68
|
return (
|
|
64
69
|
<div className="flex w-full items-stretch gap-2">
|
|
65
70
|
<div
|
|
66
|
-
className="w-full rounded-
|
|
71
|
+
className="w-full rounded-[6px] p-px transition-[background] duration-200"
|
|
67
72
|
style={{
|
|
68
73
|
background: isActiveBorder ? activeGradient : idleGradient,
|
|
69
74
|
}}
|
|
70
75
|
>
|
|
71
|
-
<div className="flex min-h-[50px] w-full items-center justify-between rounded-[
|
|
76
|
+
<div className="flex min-h-[50px] w-full items-center justify-between rounded-[5px] bg-white">
|
|
72
77
|
<div className="flex w-full items-center justify-between p-[3px]">
|
|
73
78
|
{!isTyping ? (
|
|
74
79
|
<button
|
|
75
80
|
type="button"
|
|
76
81
|
onClick={startRecording}
|
|
77
|
-
className="grid h-[44px] w-[44px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
82
|
+
className="grid h-[44px] w-[44px] cursor-pointer place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
78
83
|
title="Record voice"
|
|
79
84
|
aria-label="Record voice"
|
|
80
85
|
>
|
|
@@ -117,7 +122,9 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
117
122
|
/>
|
|
118
123
|
</div>
|
|
119
124
|
|
|
120
|
-
{!canSendArrow &&
|
|
125
|
+
{!canSendArrow && (
|
|
126
|
+
<div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />
|
|
127
|
+
)}
|
|
121
128
|
|
|
122
129
|
<div className="px-2">
|
|
123
130
|
{isTyping ? (
|
|
@@ -127,7 +134,7 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
127
134
|
type="button"
|
|
128
135
|
onClick={sendText}
|
|
129
136
|
className={clsx(
|
|
130
|
-
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full text-[#ff5301] hover:bg-[#f8f8f8]",
|
|
137
|
+
"ms-1 grid h-[40px] w-[40px] cursor-pointer place-items-center rounded-full text-[#ff5301] hover:bg-[#f8f8f8]",
|
|
131
138
|
)}
|
|
132
139
|
title={hasAttachments ? "Send attachments" : "Send"}
|
|
133
140
|
aria-label="Send"
|
|
@@ -144,7 +151,7 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
144
151
|
disabled={!hasAttachments}
|
|
145
152
|
className={clsx(
|
|
146
153
|
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full hover:bg-[#f8f8f8]",
|
|
147
|
-
hasAttachments ? "text-[#ff5301]" : "text-[#B9C3D4]",
|
|
154
|
+
hasAttachments ? "text-[#ff5301] cursor-pointer" : "text-[#B9C3D4] cursor-not-allowed",
|
|
148
155
|
)}
|
|
149
156
|
title={hasAttachments ? "Send attachments" : "Send"}
|
|
150
157
|
aria-label="Send"
|
|
@@ -163,7 +170,10 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
163
170
|
// Recording state UI
|
|
164
171
|
return (
|
|
165
172
|
<div className="flex w-full items-stretch gap-2">
|
|
166
|
-
<div
|
|
173
|
+
<div
|
|
174
|
+
className="w-full rounded-sm p-px"
|
|
175
|
+
style={{ background: activeGradient }}
|
|
176
|
+
>
|
|
167
177
|
<div className="flex min-h-[50px] w-full items-center justify-between rounded-[3px] bg-white">
|
|
168
178
|
<button
|
|
169
179
|
type="button"
|
|
@@ -174,9 +184,7 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
174
184
|
<RecordMicIcon
|
|
175
185
|
className={clsx(
|
|
176
186
|
"h-6 w-6",
|
|
177
|
-
seconds % 2 === 0
|
|
178
|
-
? "text-[#929292]"
|
|
179
|
-
: "text-[#ff5301]",
|
|
187
|
+
seconds % 2 === 0 ? "text-[#929292]" : "text-[#ff5301]",
|
|
180
188
|
)}
|
|
181
189
|
/>
|
|
182
190
|
</button>
|
|
@@ -187,7 +195,7 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
187
195
|
<button
|
|
188
196
|
type="button"
|
|
189
197
|
onClick={() => stopRecording(false)}
|
|
190
|
-
className="grid h-8 w-8 place-items-center rounded-full text-[#3D3D3D] hover:bg-black/5"
|
|
198
|
+
className="grid h-8 w-8 cursor-pointer place-items-center rounded-full text-[#3D3D3D] hover:bg-black/5"
|
|
191
199
|
title="Discard"
|
|
192
200
|
aria-label="Discard recording"
|
|
193
201
|
>
|
|
@@ -199,7 +207,7 @@ const ChatComposerBar: React.FC<Props> = ({
|
|
|
199
207
|
<button
|
|
200
208
|
type="button"
|
|
201
209
|
onClick={() => stopRecording(true)}
|
|
202
|
-
className="grid h-10 w-[40px] place-items-center rounded-full text-[#ff5301]"
|
|
210
|
+
className="grid h-10 w-[40px] cursor-pointer place-items-center rounded-full text-[#ff5301]"
|
|
203
211
|
title="Send"
|
|
204
212
|
aria-label="Send"
|
|
205
213
|
>
|
package/src/ui/ChatFooter.tsx
CHANGED
|
@@ -225,13 +225,23 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
225
225
|
const previewFilesToPayload = (files: PreviewFile[]) =>
|
|
226
226
|
files.map((f) => ({ name: f.name, sizeMB: f.sizeMB, ext: f.ext, href: f.href, downloadName: f.downloadName }));
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
// Used by the preview strip's ✕ / remove buttons — safe to revoke because
|
|
229
|
+
// the user is cancelling the attachment (it will never appear in a message).
|
|
230
|
+
const cancelAttachments = () => {
|
|
229
231
|
imgPreviews.forEach((u) => u.startsWith("blob:") && URL.revokeObjectURL(u));
|
|
230
232
|
filePreviews.forEach((f) => f.href?.startsWith("blob:") && URL.revokeObjectURL(f.href));
|
|
231
233
|
setImgPreviews([]);
|
|
232
234
|
setFilePreviews([]);
|
|
233
235
|
};
|
|
234
236
|
|
|
237
|
+
// Used after a successful send — do NOT revoke blobs here.
|
|
238
|
+
// The sent message object already holds these blob URLs and ChatMessageItem
|
|
239
|
+
// needs them to render the image. Revoking immediately makes <img src> fail.
|
|
240
|
+
const dismissAttachments = () => {
|
|
241
|
+
setImgPreviews([]);
|
|
242
|
+
setFilePreviews([]);
|
|
243
|
+
};
|
|
244
|
+
|
|
235
245
|
/* ─────── Send handlers — all delegate to the single onSend prop ─────── */
|
|
236
246
|
|
|
237
247
|
const sendText = async () => {
|
|
@@ -248,7 +258,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
248
258
|
images: imgPreviews,
|
|
249
259
|
replyTo,
|
|
250
260
|
});
|
|
251
|
-
|
|
261
|
+
dismissAttachments(); // ← do NOT revoke — blob URLs are now in the message
|
|
252
262
|
} else {
|
|
253
263
|
onSend({ type: "text", text: t, replyTo });
|
|
254
264
|
}
|
|
@@ -268,7 +278,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
268
278
|
files: previewFilesToPayload(filePreviews),
|
|
269
279
|
replyTo,
|
|
270
280
|
});
|
|
271
|
-
|
|
281
|
+
dismissAttachments(); // ← do NOT revoke — blob URLs are now in the message
|
|
272
282
|
clearReply?.();
|
|
273
283
|
onAfterSend?.();
|
|
274
284
|
};
|
|
@@ -368,7 +378,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
368
378
|
return (
|
|
369
379
|
<span key={a.key} className="relative inline-flex">
|
|
370
380
|
<Tooltip text={a.title}>
|
|
371
|
-
<button ref={emojiBtnRef} type="button" onClick={() => setShowEmoji(
|
|
381
|
+
<button ref={emojiBtnRef} type="button" onClick={() => setShowEmoji(true)} className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
372
382
|
<span>{a.icon}</span>
|
|
373
383
|
</button>
|
|
374
384
|
</Tooltip>
|
|
@@ -378,7 +388,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
378
388
|
if (isTranslate) {
|
|
379
389
|
return (
|
|
380
390
|
<Tooltip key={a.key} text={a.title}>
|
|
381
|
-
<button type="button" onClick={() => setShowTranslate(true)} className="flex h-6 w-6 items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
391
|
+
<button type="button" onClick={() => setShowTranslate(true)} className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
382
392
|
<span>{a.icon}</span>
|
|
383
393
|
</button>
|
|
384
394
|
</Tooltip>
|
|
@@ -388,7 +398,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
388
398
|
return (
|
|
389
399
|
<span key={a.key} className="relative inline-flex">
|
|
390
400
|
<Tooltip text={a.title}>
|
|
391
|
-
<button ref={bizBtnRef} type="button" onClick={() => setShowBiz((v) => !v)} className="flex h-6 w-6 items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
401
|
+
<button ref={bizBtnRef} type="button" onClick={() => setShowBiz((v) => !v)} className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
392
402
|
<span>{a.icon}</span>
|
|
393
403
|
</button>
|
|
394
404
|
</Tooltip>
|
|
@@ -399,7 +409,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
399
409
|
return (
|
|
400
410
|
<span key={a.key} className="relative inline-flex">
|
|
401
411
|
<Tooltip text={a.title}>
|
|
402
|
-
<button ref={addrBtnRef} type="button" onClick={() => setShowAddress(true)} className="flex h-6 w-6 items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
412
|
+
<button ref={addrBtnRef} type="button" onClick={() => setShowAddress(true)} className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
403
413
|
<span>{a.icon}</span>
|
|
404
414
|
</button>
|
|
405
415
|
</Tooltip>
|
|
@@ -408,7 +418,7 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
408
418
|
}
|
|
409
419
|
return (
|
|
410
420
|
<Tooltip key={a.key} text={a.title}>
|
|
411
|
-
<button type="button" onClick={isAttach ? () => fileInputRef.current?.click() : a.onClick} className="flex h-6 w-6 items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
421
|
+
<button type="button" onClick={isAttach ? () => fileInputRef.current?.click() : a.onClick} className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#0F0F0F] hover:bg-[#F4F6F8]">
|
|
412
422
|
<span>{a.icon}</span>
|
|
413
423
|
</button>
|
|
414
424
|
</Tooltip>
|
|
@@ -432,9 +442,9 @@ const ChatFooter: React.FC<Props> = ({
|
|
|
432
442
|
</div>
|
|
433
443
|
|
|
434
444
|
{micError && (
|
|
435
|
-
<div className="mb-2 flex items-
|
|
436
|
-
<span><ChatInfoIcon className="
|
|
437
|
-
<span>{micError}</span>
|
|
445
|
+
<div className="mb-2 flex items-center gap-2 rounded-sm bg-[#f8f8f8] px-2 py-1.5 text-xs text-[#ff5301]">
|
|
446
|
+
<span><ChatInfoIcon className="h-3.5 w-3.5 shrink-0" /></span>
|
|
447
|
+
<span className="flex-1">{micError}</span>
|
|
438
448
|
<span className="pointer flex cursor-pointer items-center justify-center rounded-full p-1 hover:bg-black/10" onClick={() => setMicError("")}>
|
|
439
449
|
<ChatXIcon className="h-3.5 w-3.5" />
|
|
440
450
|
</span>
|
|
@@ -33,7 +33,7 @@ const ChatInquiryBar: React.FC<Props> = ({ id, onView, className, label, buttonL
|
|
|
33
33
|
<button
|
|
34
34
|
type="button"
|
|
35
35
|
onClick={onView}
|
|
36
|
-
className="group relative inline-flex w-fit items-center justify-end text-xs font-medium text-black"
|
|
36
|
+
className="group relative inline-flex w-fit cursor-pointer items-center justify-end text-xs font-medium text-black"
|
|
37
37
|
>
|
|
38
38
|
<span className="flex items-center transition-opacity duration-300 ease-in-out group-hover:opacity-0">
|
|
39
39
|
<span>{buttonLabel}</span>
|
|
@@ -63,7 +63,7 @@ const ChatThreadItem: React.FC<Props> = ({
|
|
|
63
63
|
<button
|
|
64
64
|
onClick={onClick}
|
|
65
65
|
className={clsx(
|
|
66
|
-
"relative w-full text-left px-5 py-2 hover:bg-[#f8f8f8] focus:outline-none h-[75px]",
|
|
66
|
+
"relative w-full cursor-pointer text-left px-5 py-2 hover:bg-[#f8f8f8] focus:outline-none h-[75px]",
|
|
67
67
|
active && "bg-[#f8f8f8]",
|
|
68
68
|
className,
|
|
69
69
|
)}
|