@banbox/chat 1.0.9 → 1.0.10

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.10",
4
4
  "description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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";
@@ -44,23 +45,7 @@ const avatarBgByInitial: Record<string, string> = {
44
45
  b: "#F0EDEB",
45
46
  };
46
47
 
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
48
 
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
49
 
65
50
  /* ══════════════════════════════════════════════════
66
51
  Component
@@ -136,12 +121,19 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme })
136
121
 
137
122
  const prevActiveIdRef = useRef(activeId);
138
123
  useEffect(() => {
124
+ // Mark read on thread SWITCH
139
125
  if (prevActiveIdRef.current !== activeId) {
140
126
  prevActiveIdRef.current = activeId;
141
127
  if (activeId) adapter.threads.markRead?.(activeId);
142
128
  }
143
129
  }, [activeId, adapter]);
144
130
 
131
+ // Mark read on initial open (prevActiveIdRef starts equal to activeId so the above won't fire)
132
+ useEffect(() => {
133
+ if (activeId) adapter.threads.markRead?.(activeId);
134
+ // eslint-disable-next-line react-hooks/exhaustive-deps
135
+ }, []);
136
+
145
137
  const toRef = (m: Message): MessageRef => ({
146
138
  id: m.id,
147
139
  author: typeof m.author === "string" ? m.author : "U",
@@ -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,20 +49,7 @@ 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;
@@ -78,9 +64,19 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme }
78
64
  const { close, reference } = useChatUI();
79
65
  const { isOpen: isGalleryOpen, closeGallery } = useGallery();
80
66
 
81
- const threads = adapter.threads.list(reference);
67
+ // ── Threads — subscribed so real-API updates (new msg, pin, delete) are reflected
68
+ const [threads, setThreads] = React.useState<Thread[]>(() => adapter.threads.list(reference));
69
+ React.useEffect(() => {
70
+ // Refresh once on mount (covers any gap between render and subscribe)
71
+ setThreads(adapter.threads.list(reference));
72
+ const unsub = adapter.threads.subscribe(() => {
73
+ setThreads(adapter.threads.list(reference));
74
+ });
75
+ return unsub;
76
+ }, [adapter, reference]);
77
+
82
78
  const initialThreadId = React.useMemo(
83
- () => coalesceThreadId(reference, threads),
79
+ () => coalesceThreadId(reference, adapter.threads.list(reference)),
84
80
  // eslint-disable-next-line react-hooks/exhaustive-deps
85
81
  [reference],
86
82
  );
@@ -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
+ }