@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banbox/chat",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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
  )}
@@ -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
  }}
@@ -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
- /* ─── Helpers ─── */
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
- const referenceId = reference?.id;
30
- if (reference?.kind === "quotation") {
31
- return threads.find((t) => t.id === "t4")?.id ?? (threads[0]?.id ?? "");
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
- (referenceId &&
35
- (threads.find((t) => t.id === referenceId)?.id ||
36
- threads.find((t) => t.inquiryId === referenceId)?.id)) ||
37
- (threads.length ? threads[0].id : "")
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
- const threads = adapter.threads.list(reference);
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
- onTranslate?: () => void; // optional external hook
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 ? toBanglaDemo(originalTextRef.current) : originalTextRef.current;
79
+ const displayText = translated ? onTranslate?.(originalTextRef.current) ?? originalTextRef.current : originalTextRef.current;
91
80
 
92
81
  const handleTranslateClick = () => {
93
- setTranslated((v) => !v); // toggle EN ⇄ BN
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
+ }