@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/dist/index.cjs +44 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +44 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +8 -16
- package/src/chat/SinglePopup.tsx +22 -26
- package/src/ui/ChatMessageItem.tsx +9 -31
- package/src/utils/theme.ts +37 -0
package/package.json
CHANGED
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";
|
|
@@ -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",
|
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,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
|
-
|
|
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
|
-
|
|
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
|
+
}
|