@banbox/chat 1.0.9 → 1.0.11
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 +53 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +53 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/ChatRoot.tsx +14 -1
- package/src/chat/InboxPopup.tsx +12 -17
- package/src/chat/SinglePopup.tsx +26 -27
- package/src/ui/ChatMessageItem.tsx +9 -31
- package/src/utils/theme.ts +37 -0
package/package.json
CHANGED
package/src/chat/ChatRoot.tsx
CHANGED
|
@@ -32,9 +32,20 @@ export type ChatRootProps = {
|
|
|
32
32
|
* <ChatRoot adapter={adapter} theme="admin" />
|
|
33
33
|
*/
|
|
34
34
|
theme?: ChatTheme;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Keys of footer toolbar actions to hide.
|
|
38
|
+
*
|
|
39
|
+
* Available keys: "attachment" | "emoji" | "businessCard" | "addressCard" | "translate"
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Hide the Delivery Address button in the seller app:
|
|
43
|
+
* <ChatRoot hiddenActionKeys={["addressCard"]} ... />
|
|
44
|
+
*/
|
|
45
|
+
hiddenActionKeys?: string[];
|
|
35
46
|
};
|
|
36
47
|
|
|
37
|
-
export default function ChatRoot({ adapter, uiCallbacks, theme }: ChatRootProps) {
|
|
48
|
+
export default function ChatRoot({ adapter, uiCallbacks, theme, hiddenActionKeys }: ChatRootProps) {
|
|
38
49
|
const { isOpen, variant } = useChatUI();
|
|
39
50
|
|
|
40
51
|
// Lock page scroll whenever the chat is open
|
|
@@ -55,6 +66,7 @@ export default function ChatRoot({ adapter, uiCallbacks, theme }: ChatRootProps)
|
|
|
55
66
|
adapter={adapter}
|
|
56
67
|
uiCallbacks={uiCallbacks}
|
|
57
68
|
theme={theme}
|
|
69
|
+
hiddenActionKeys={hiddenActionKeys}
|
|
58
70
|
/>
|
|
59
71
|
) : (
|
|
60
72
|
<SinglePopup
|
|
@@ -62,6 +74,7 @@ export default function ChatRoot({ adapter, uiCallbacks, theme }: ChatRootProps)
|
|
|
62
74
|
adapter={adapter}
|
|
63
75
|
uiCallbacks={uiCallbacks}
|
|
64
76
|
theme={theme}
|
|
77
|
+
hiddenActionKeys={hiddenActionKeys}
|
|
65
78
|
/>
|
|
66
79
|
)
|
|
67
80
|
)}
|
package/src/chat/InboxPopup.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type { ChatThreadStatus } from "../ui/ChatThreadItem";
|
|
|
18
18
|
import ChatThreadItem from "../ui/ChatThreadItem";
|
|
19
19
|
import TypingIndicator from "../ui/TypingIndicator";
|
|
20
20
|
import ChatImagePreviewModal from "./ChatImagePreviewModal";
|
|
21
|
+
import { GRADIENT_BORDER, getThemeAttr, getThemeVars } from "../utils/theme";
|
|
21
22
|
|
|
22
23
|
import ChatKebabMenu from "../ui/ChatKebabMenu";
|
|
23
24
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
@@ -33,6 +34,8 @@ export type InboxPopupProps = {
|
|
|
33
34
|
adapter: ChatAdapter;
|
|
34
35
|
uiCallbacks?: ChatUICallbacks;
|
|
35
36
|
theme?: ChatTheme;
|
|
37
|
+
/** Keys of footer toolbar actions to hide. e.g. ["addressCard", "translate"] */
|
|
38
|
+
hiddenActionKeys?: string[];
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
/* ─── Helpers ─── */
|
|
@@ -44,28 +47,12 @@ const avatarBgByInitial: Record<string, string> = {
|
|
|
44
47
|
b: "#F0EDEB",
|
|
45
48
|
};
|
|
46
49
|
|
|
47
|
-
const GRADIENT_BORDER =
|
|
48
|
-
"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%)";
|
|
49
50
|
|
|
50
|
-
function getThemeAttr(theme?: ChatTheme): string {
|
|
51
|
-
if (!theme || theme === "marketplace") return "marketplace";
|
|
52
|
-
if (theme === "admin") return "admin";
|
|
53
|
-
return "custom";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
57
|
-
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
58
|
-
const vars: Record<string, string> = {};
|
|
59
|
-
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
60
|
-
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
61
|
-
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
62
|
-
return vars as React.CSSProperties;
|
|
63
|
-
}
|
|
64
51
|
|
|
65
52
|
/* ══════════════════════════════════════════════════
|
|
66
53
|
Component
|
|
67
54
|
══════════════════════════════════════════════════ */
|
|
68
|
-
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
55
|
+
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme, hiddenActionKeys }) => {
|
|
69
56
|
const { close, selectThread, selectedThreadId, reference } = useChatUI();
|
|
70
57
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
71
58
|
|
|
@@ -136,12 +123,19 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
136
123
|
|
|
137
124
|
const prevActiveIdRef = useRef(activeId);
|
|
138
125
|
useEffect(() => {
|
|
126
|
+
// Mark read on thread SWITCH
|
|
139
127
|
if (prevActiveIdRef.current !== activeId) {
|
|
140
128
|
prevActiveIdRef.current = activeId;
|
|
141
129
|
if (activeId) adapter.threads.markRead?.(activeId);
|
|
142
130
|
}
|
|
143
131
|
}, [activeId, adapter]);
|
|
144
132
|
|
|
133
|
+
// Mark read on initial open (prevActiveIdRef starts equal to activeId so the above won't fire)
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (activeId) adapter.threads.markRead?.(activeId);
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
145
139
|
const toRef = (m: Message): MessageRef => ({
|
|
146
140
|
id: m.id,
|
|
147
141
|
author: typeof m.author === "string" ? m.author : "U",
|
|
@@ -345,6 +339,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
|
|
|
345
339
|
replyTo={replyTo}
|
|
346
340
|
clearReply={() => setReplyTo(undefined)}
|
|
347
341
|
onAfterSend={() => setRev((v) => v + 1)}
|
|
342
|
+
hiddenActionKeys={hiddenActionKeys}
|
|
348
343
|
onSend={(payload) => {
|
|
349
344
|
if (activeId) adapter.messages.send(activeId, payload);
|
|
350
345
|
}}
|
package/src/chat/SinglePopup.tsx
CHANGED
|
@@ -16,25 +16,24 @@ import ChatMessageItem from "../ui/ChatMessageItem";
|
|
|
16
16
|
import ChatScroll from "../ui/ChatScroll";
|
|
17
17
|
import TypingIndicator from "../ui/TypingIndicator";
|
|
18
18
|
import ChatImagePreviewModal from "./ChatImagePreviewModal";
|
|
19
|
+
import { GRADIENT_BORDER, getThemeAttr, getThemeVars } from "../utils/theme";
|
|
19
20
|
|
|
20
21
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
21
22
|
import type { Message, MessageRef, Reference, Thread } from "../types";
|
|
22
23
|
import type { ChatTheme } from "./InboxPopup";
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
const GRADIENT_BORDER =
|
|
26
|
-
"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%)";
|
|
25
|
+
|
|
27
26
|
|
|
28
27
|
function coalesceThreadId(reference: Reference | undefined, threads: Thread[]): string {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
28
|
+
if (!reference?.id) return threads[0]?.id ?? "";
|
|
29
|
+
const refId = reference.id;
|
|
30
|
+
// Priority: exact thread.id match → orderId match → inquiryId match → first thread
|
|
33
31
|
return (
|
|
34
|
-
(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
threads.find((t) => t.id === refId)?.id ??
|
|
33
|
+
threads.find((t) => t.orderId === refId)?.id ??
|
|
34
|
+
threads.find((t) => t.inquiryId === refId)?.id ??
|
|
35
|
+
threads[0]?.id ??
|
|
36
|
+
""
|
|
38
37
|
);
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -50,37 +49,36 @@ function toRef(m: Message): MessageRef {
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
function getThemeAttr(theme?: ChatTheme): string {
|
|
54
|
-
if (!theme || theme === "marketplace") return "marketplace";
|
|
55
|
-
if (theme === "admin") return "admin";
|
|
56
|
-
return "custom";
|
|
57
|
-
}
|
|
58
52
|
|
|
59
|
-
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
60
|
-
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
61
|
-
const vars: Record<string, string> = {};
|
|
62
|
-
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
63
|
-
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
64
|
-
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
65
|
-
return vars as React.CSSProperties;
|
|
66
|
-
}
|
|
67
53
|
|
|
68
54
|
export type SinglePopupProps = {
|
|
69
55
|
adapter: ChatAdapter;
|
|
70
56
|
uiCallbacks?: ChatUICallbacks;
|
|
71
57
|
theme?: ChatTheme;
|
|
58
|
+
/** Keys of footer toolbar actions to hide. e.g. ["addressCard", "translate"] */
|
|
59
|
+
hiddenActionKeys?: string[];
|
|
72
60
|
};
|
|
73
61
|
|
|
74
62
|
/* ══════════════════════════════════════════════════
|
|
75
63
|
Component
|
|
76
64
|
══════════════════════════════════════════════════ */
|
|
77
|
-
const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
65
|
+
const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme, hiddenActionKeys }) => {
|
|
78
66
|
const { close, reference } = useChatUI();
|
|
79
67
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
80
68
|
|
|
81
|
-
|
|
69
|
+
// ── Threads — subscribed so real-API updates (new msg, pin, delete) are reflected
|
|
70
|
+
const [threads, setThreads] = React.useState<Thread[]>(() => adapter.threads.list(reference));
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
// Refresh once on mount (covers any gap between render and subscribe)
|
|
73
|
+
setThreads(adapter.threads.list(reference));
|
|
74
|
+
const unsub = adapter.threads.subscribe(() => {
|
|
75
|
+
setThreads(adapter.threads.list(reference));
|
|
76
|
+
});
|
|
77
|
+
return unsub;
|
|
78
|
+
}, [adapter, reference]);
|
|
79
|
+
|
|
82
80
|
const initialThreadId = React.useMemo(
|
|
83
|
-
() => coalesceThreadId(reference, threads),
|
|
81
|
+
() => coalesceThreadId(reference, adapter.threads.list(reference)),
|
|
84
82
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
83
|
[reference],
|
|
86
84
|
);
|
|
@@ -262,6 +260,7 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme }
|
|
|
262
260
|
replyTo={replyTo}
|
|
263
261
|
clearReply={() => setReplyTo(undefined)}
|
|
264
262
|
onAfterSend={handleAfterSend}
|
|
263
|
+
hiddenActionKeys={hiddenActionKeys}
|
|
265
264
|
onSend={(payload) => {
|
|
266
265
|
if (activeId) adapter.messages.send(activeId, payload);
|
|
267
266
|
}}
|
|
@@ -13,22 +13,6 @@ import MessageHoverActions from "./MessageHoverActions";
|
|
|
13
13
|
import ReplyCard from "./ReplyCard";
|
|
14
14
|
import type { AddressCard, BusinessCard, MessageRef } from "./types";
|
|
15
15
|
|
|
16
|
-
/** super-simple demo translator for your seed data */
|
|
17
|
-
const toBanglaDemo = (s: string) => {
|
|
18
|
-
const map: Record<string, string> = {
|
|
19
|
-
Hi: "হাই",
|
|
20
|
-
"Do you have a freight forwarder in China?": "আপনার কি চীনে কোনো ফ্রেইট ফরওয়ার্ডার আছে?",
|
|
21
|
-
"This conversation is empty. Say hi 👋": "এই কথোপকথনটি খালি । হাই হাই হাই বলুন 👋",
|
|
22
|
-
"Can we schedule a call for tomorrow?": "আমরা কি আগামীকাল একটি কল নির্ধারণ করতে পারি?",
|
|
23
|
-
"Sure, what time suits you?": "অবশ্যই, আপনার জন্য কোন সময়টি সুবিধাজনক?",
|
|
24
|
-
"Welcome to Global Marketplace": "গ্লোবাল মার্কেটপ্লেসে আপনাকে স্বাগতম 🎉",
|
|
25
|
-
"Happy to be here!": "এখানে থাকতে পেরে আনন্দিত!",
|
|
26
|
-
};
|
|
27
|
-
if (map[s]) {
|
|
28
|
-
return map[s];
|
|
29
|
-
}
|
|
30
|
-
return `বাংলা ${s}`;
|
|
31
|
-
};
|
|
32
16
|
|
|
33
17
|
export type ChatAudio = { src?: string; duration?: string };
|
|
34
18
|
export type ChatFile = {
|
|
@@ -59,7 +43,13 @@ export type ChatMessageItemProps = {
|
|
|
59
43
|
className?: string;
|
|
60
44
|
|
|
61
45
|
onReply?: () => void;
|
|
62
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Optional async translator. Receives the original text and returns the
|
|
48
|
+
* translated string. When omitted the translate button is still shown but
|
|
49
|
+
* does nothing (good for demo mode).
|
|
50
|
+
* Example: `onTranslate={(t) => googleTranslate(t, "bn")}`
|
|
51
|
+
*/
|
|
52
|
+
onTranslate?: (text: string) => string | undefined;
|
|
63
53
|
|
|
64
54
|
initialSrc?: string;
|
|
65
55
|
};
|
|
@@ -84,18 +74,14 @@ const ChatMessageItem: React.FC<ChatMessageItemProps> = ({
|
|
|
84
74
|
onTranslate,
|
|
85
75
|
initialSrc,
|
|
86
76
|
}) => {
|
|
87
|
-
// translation state only affects the text bubble
|
|
88
77
|
const originalTextRef = React.useRef(text ?? "");
|
|
89
78
|
const [translated, setTranslated] = React.useState(false);
|
|
90
|
-
const displayText = translated ?
|
|
79
|
+
const displayText = translated ? onTranslate?.(originalTextRef.current) ?? originalTextRef.current : originalTextRef.current;
|
|
91
80
|
|
|
92
81
|
const handleTranslateClick = () => {
|
|
93
|
-
setTranslated((v) => !v);
|
|
94
|
-
onTranslate?.(); // still let parent know if needed
|
|
82
|
+
setTranslated((v) => !v);
|
|
95
83
|
};
|
|
96
84
|
|
|
97
|
-
const isOnline = true;
|
|
98
|
-
|
|
99
85
|
return (
|
|
100
86
|
<div className={clsx("mb-4", className)} data-msg-id={id}>
|
|
101
87
|
<div className={clsx("flex items-end gap-3", mine && "justify-end")}>
|
|
@@ -108,10 +94,6 @@ const ChatMessageItem: React.FC<ChatMessageItemProps> = ({
|
|
|
108
94
|
alt="avatar image"
|
|
109
95
|
className="h-full w-full rounded-full object-cover"
|
|
110
96
|
/>
|
|
111
|
-
|
|
112
|
-
{isOnline && (
|
|
113
|
-
<span className="absolute bottom-0 right-0 h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
|
|
114
|
-
)}
|
|
115
97
|
</div>
|
|
116
98
|
) : (
|
|
117
99
|
<div
|
|
@@ -119,10 +101,6 @@ const ChatMessageItem: React.FC<ChatMessageItemProps> = ({
|
|
|
119
101
|
style={{ backgroundColor: avatarBg }}
|
|
120
102
|
>
|
|
121
103
|
{authorInitial}
|
|
122
|
-
|
|
123
|
-
{isOnline && (
|
|
124
|
-
<span className="absolute bottom-0 right-0 h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
|
|
125
|
-
)}
|
|
126
104
|
</div>
|
|
127
105
|
)}
|
|
128
106
|
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// utils/theme.ts
|
|
2
|
+
// Shared theme helpers used by InboxPopup and SinglePopup.
|
|
3
|
+
// Centralised here to avoid duplication.
|
|
4
|
+
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import type { ChatTheme } from "../chat/InboxPopup";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The gradient border used on both InboxPopup and SinglePopup outer wrapper.
|
|
10
|
+
* Semi-transparent so the white inner card shows through cleanly.
|
|
11
|
+
*/
|
|
12
|
+
export const GRADIENT_BORDER =
|
|
13
|
+
"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%)";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the `data-theme` attribute value for the root element.
|
|
17
|
+
* Used by CSS to apply theme-specific colour variables.
|
|
18
|
+
*/
|
|
19
|
+
export function getThemeAttr(theme?: ChatTheme): string {
|
|
20
|
+
if (!theme || theme === "marketplace") return "marketplace";
|
|
21
|
+
if (theme === "admin") return "admin";
|
|
22
|
+
return "custom";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns inline CSS variables for a custom theme object.
|
|
27
|
+
* Returns an empty object for named themes ("marketplace" / "admin")
|
|
28
|
+
* because those are handled by CSS data-theme selectors.
|
|
29
|
+
*/
|
|
30
|
+
export function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
31
|
+
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
32
|
+
const vars: Record<string, string> = {};
|
|
33
|
+
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
34
|
+
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
35
|
+
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
36
|
+
return vars as React.CSSProperties;
|
|
37
|
+
}
|