@banbox/chat 1.0.1 → 1.0.2

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.
@@ -1,214 +1,193 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
-
5
- import ChatAddressCard from "./message-items/ChatAddressCard";
6
- import ChatBubbleAudio from "./message-items/ChatBubbleAudio";
7
- import ChatBubbleFiles from "./message-items/ChatBubbleFiles";
8
- import ChatBubbleImages from "./message-items/ChatBubbleImages";
9
- import ChatBubbleText from "./message-items/ChatBubbleText";
10
- import ChatBusinessCard from "./message-items/ChatBusinessCard";
11
- import MessageHoverActions from "./MessageHoverActions";
12
- import ReplyCard from "./ReplyCard";
13
- import type { AddressCard, BusinessCard, MessageRef } from "./types";
14
- import { cn } from "../../utils/cn";
15
-
16
- /* =======================
17
- Demo translator
18
- ======================= */
19
-
20
- const toBanglaDemo = (s: string): string => {
21
- const map: Record<string, string> = {
22
- Hi: "হাই",
23
- "Do you have a freight forwarder in China?": "আপনার কি চীনে কোনো ফ্রেইট ফরওয়ার্ডার আছে?",
24
- "This conversation is empty. Say hi 👋": "এই কথোপকথনটি খালি হাই হাই হাই বলুন 👋",
25
- "Can we schedule a call for tomorrow?": "আমরা কি আগামীকাল একটি কল নির্ধারণ করতে পারি?",
26
- "Sure, what time suits you?": "অবশ্যই, আপনার জন্য কোন সময়টি সুবিধাজনক?",
27
- "Welcome to Global Marketplace": "গ্লোবাল মার্কেটপ্লেসে আপনাকে স্বাগতম 🎉",
28
- "Happy to be here!": "এখানে থাকতে পেরে আনন্দিত!",
29
- };
30
-
31
- if (map[s]) {
32
- return map[s];
33
- }
34
-
35
- return `বাংলা ${s}`;
36
- };
37
-
38
- /* =======================
39
- Types
40
- ======================= */
41
-
42
- export type ChatAudio = {
43
- src?: string;
44
- duration?: string;
45
- };
46
-
47
- export type ChatFile = {
48
- name: string;
49
- sizeMB: number;
50
- ext: string;
51
- href?: string;
52
- downloadName?: string;
53
- };
54
-
55
- export type ChatMessageItemProps = {
56
- id: string;
57
- mine?: boolean;
58
- time: string;
59
- authorInitial?: string;
60
- avatarBg?: string;
61
-
62
- text?: string;
63
- businessCard?: BusinessCard;
64
- addressCard?: AddressCard;
65
- images?: string[];
66
- files?: ChatFile[];
67
- audio?: ChatAudio;
68
-
69
- replyTo?: MessageRef;
70
- showStatus?: boolean;
71
- status?: string;
72
- className?: string;
73
-
74
- onReply?: () => void;
75
- onTranslate?: () => void;
76
-
77
- initialSrc?: string;
78
- };
79
-
80
- /* =======================
81
- Component
82
- ======================= */
83
-
84
- const ChatMessageItem = ({
85
- id,
86
- mine = false,
87
- time,
88
- authorInitial = "U",
89
- avatarBg = "#ffffff",
90
- text,
91
- businessCard,
92
- addressCard,
93
- images,
94
- files,
95
- audio,
96
- replyTo,
97
- showStatus = false,
98
- status = "Seen",
99
- className,
100
- onReply,
101
- onTranslate,
102
- initialSrc,
103
- }: ChatMessageItemProps) => {
104
- const originalText = useMemo(() => text ?? "", [text]);
105
- const [translated, setTranslated] = useState(false);
106
-
107
- const displayText = translated ? toBanglaDemo(originalText) : originalText;
108
-
109
- const handleTranslateClick = () => {
110
- setTranslated((v) => !v);
111
- onTranslate?.();
112
- };
113
-
114
- const isOnline = true;
115
-
116
- return (
117
- <div className={cn("mb-4", className)} data-msg-id={id}>
118
- <div className={cn("flex items-end gap-3", mine && "justify-end")}>
119
- {!mine ? (
120
- <div className={cn(showStatus ? "mb-5" : "mb-0")}>
121
- {initialSrc ? (
122
- <div className="relative h-10 w-10 shrink-0 rounded-full border border-[#F1F1F1]">
123
- <img
124
- src={initialSrc}
125
- alt="avatar image"
126
- className="h-full w-full rounded-full object-cover"
127
- />
128
-
129
- {isOnline ? (
130
- <span className="absolute bottom-0 right-0 h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
131
- ) : null}
132
- </div>
133
- ) : (
134
- <div
135
- className="relative grid h-10 w-10 shrink-0 place-items-center rounded-full border border-[#f1f1f1] font-semibold text-[#2c2c2c]"
136
- style={{ backgroundColor: avatarBg }}
137
- >
138
- {authorInitial}
139
-
140
- {isOnline ? (
141
- <span className="absolute bottom-0 right-0 h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
142
- ) : null}
143
- </div>
144
- )}
145
- </div>
146
- ) : null}
147
-
148
- <div className="flex w-full flex-col gap-1">
149
- <div
150
- className={cn(
151
- "text-xs font-light text-[#636363]",
152
- mine ? "text-right" : "text-left",
153
- )}
154
- >
155
- {time}
156
- </div>
157
-
158
- <div className={cn("flex w-full flex-col gap-1", mine ? "items-end" : "items-start")}>
159
- {/* Reply preview */}
160
- <div className={cn("flex w-full", mine ? "justify-end" : "justify-start")}>
161
- {replyTo ? <ReplyCard jumpOnClick refMsg={replyTo} compact className="mb-1" /> : null}
162
- </div>
163
-
164
- {businessCard ? <ChatBusinessCard mine={mine} card={businessCard} /> : null}
165
- {addressCard ? <ChatAddressCard mine={mine} card={addressCard} /> : null}
166
-
167
- {files?.length ? (
168
- <MessageHoverActions mine={mine} onReply={onReply} isItemButton={["replay"]}>
169
- <ChatBubbleFiles files={files} />
170
- </MessageHoverActions>
171
- ) : null}
172
-
173
- {images?.length ? (
174
- <MessageHoverActions mine={mine} onReply={onReply} isItemButton={["replay"]}>
175
- <ChatBubbleImages images={images} />
176
- </MessageHoverActions>
177
- ) : null}
178
-
179
- {audio ? (
180
- <MessageHoverActions
181
- mine={mine}
182
- onReply={onReply}
183
- onTranslate={onTranslate}
184
- isItemButton={["replay", "translate"]}
185
- >
186
- <ChatBubbleAudio mine={mine} audio={audio} />
187
- </MessageHoverActions>
188
- ) : null}
189
-
190
- {typeof text === "string" && text.length > 0 ? (
191
- <MessageHoverActions
192
- mine={mine}
193
- onReply={onReply}
194
- onTranslate={handleTranslateClick}
195
- isItemButton={["replay", "translate"]}
196
- activeButtons={translated ? ["translate"] : []}
197
- >
198
- <ChatBubbleText mine={mine} text={displayText} />
199
- </MessageHoverActions>
200
- ) : null}
201
-
202
- {showStatus ? (
203
- <div className={cn("text-xs text-[#9AA1A9]", mine ? "text-right" : "text-left")}>
204
- {status}
205
- </div>
206
- ) : null}
207
- </div>
208
- </div>
209
- </div>
210
- </div>
211
- );
212
- };
213
-
214
- export default ChatMessageItem;
1
+ // components/ui/chat/ChatMessageItem.tsx
2
+ "use client";
3
+
4
+ import clsx from "clsx";
5
+ import React from "react";
6
+ import ChatAddressCard from "./message-items/ChatAddressCard";
7
+ import ChatBubbleAudio from "./message-items/ChatBubbleAudio";
8
+ import ChatBubbleFiles from "./message-items/ChatBubbleFiles";
9
+ import ChatBubbleImages from "./message-items/ChatBubbleImages";
10
+ import ChatBubbleText from "./message-items/ChatBubbleText";
11
+ import ChatBusinessCard from "./message-items/ChatBusinessCard";
12
+ import MessageHoverActions from "./MessageHoverActions";
13
+ import ReplyCard from "./ReplyCard";
14
+ import type { AddressCard, BusinessCard, MessageRef } from "./types";
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
+
33
+ export type ChatAudio = { src?: string; duration?: string };
34
+ export type ChatFile = {
35
+ name: string;
36
+ sizeMB: number;
37
+ ext: string;
38
+ href?: string;
39
+ downloadName?: string;
40
+ };
41
+
42
+ export type ChatMessageItemProps = {
43
+ id: string;
44
+ mine?: boolean;
45
+ time: string;
46
+ authorInitial?: string;
47
+ avatarBg?: string;
48
+
49
+ text?: string;
50
+ businessCard?: BusinessCard;
51
+ addressCard?: AddressCard;
52
+ images?: string[];
53
+ files?: ChatFile[];
54
+ audio?: ChatAudio;
55
+
56
+ replyTo?: MessageRef;
57
+ showStatus?: boolean;
58
+ status?: string;
59
+ className?: string;
60
+
61
+ onReply?: () => void;
62
+ onTranslate?: () => void; // optional external hook
63
+
64
+ initialSrc?: string;
65
+ };
66
+
67
+ const ChatMessageItem: React.FC<ChatMessageItemProps> = ({
68
+ id,
69
+ mine = false,
70
+ time,
71
+ authorInitial = "U",
72
+ avatarBg = "#ffffff",
73
+ text,
74
+ businessCard,
75
+ addressCard,
76
+ images,
77
+ files,
78
+ audio,
79
+ replyTo,
80
+ showStatus = false,
81
+ status = "Seen",
82
+ className,
83
+ onReply,
84
+ onTranslate,
85
+ initialSrc,
86
+ }) => {
87
+ // translation state only affects the text bubble
88
+ const originalTextRef = React.useRef(text ?? "");
89
+ const [translated, setTranslated] = React.useState(false);
90
+ const displayText = translated ? toBanglaDemo(originalTextRef.current) : originalTextRef.current;
91
+
92
+ const handleTranslateClick = () => {
93
+ setTranslated((v) => !v); // toggle EN ⇄ BN
94
+ onTranslate?.(); // still let parent know if needed
95
+ };
96
+
97
+ const isOnline = true;
98
+
99
+ return (
100
+ <div className={clsx("mb-4", className)} data-msg-id={id}>
101
+ <div className={clsx("flex items-end gap-3", mine && "justify-end")}>
102
+ {!mine && (
103
+ <div className={`${showStatus ? "mb-5" : "mb-0"}`}>
104
+ {initialSrc ? (
105
+ <div className="relative h-10 w-10 shrink-0 rounded-full border border-[#F1F1F1]">
106
+ <img
107
+ src={initialSrc}
108
+ alt="avatar image"
109
+ className="h-full w-full rounded-full object-cover"
110
+ />
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
+ </div>
116
+ ) : (
117
+ <div
118
+ className="relative grid h-10 w-10 shrink-0 place-items-center rounded-full border border-[#f1f1f1] font-semibold text-[#2c2c2c]"
119
+ style={{ backgroundColor: avatarBg }}
120
+ >
121
+ {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
+ </div>
127
+ )}
128
+ </div>
129
+ )}
130
+
131
+ <div className={clsx("flex flex-col gap-1 w-full")}>
132
+ <div
133
+ className={clsx(
134
+ " text-xs font-light text-[#636363]",
135
+ mine ? "text-right" : "text-left",
136
+ )}
137
+ >
138
+ {time}
139
+ </div>
140
+
141
+ <div className={clsx("flex flex-col gap-1 w-full", mine ? "items-end" : "items-start")}>
142
+ {/* reply-to preview */}
143
+ <div className={clsx("flex w-full", mine ? "justify-end" : "justify-start")}>
144
+ {replyTo && <ReplyCard jumpOnClick refMsg={replyTo} compact className="mb-1" />}
145
+ </div>
146
+
147
+ {businessCard ? <ChatBusinessCard mine={mine} card={businessCard} /> : null}
148
+ {addressCard ? <ChatAddressCard mine={mine} card={addressCard} /> : null}
149
+
150
+ {images?.length ? (
151
+ <MessageHoverActions mine={mine} onReply={onReply} isItemButton={["replay"]}>
152
+ <ChatBubbleImages images={images} />
153
+ </MessageHoverActions>
154
+ ) : null}
155
+
156
+ {files?.length ? (
157
+ <MessageHoverActions mine={mine} onReply={onReply} isItemButton={["replay"]}>
158
+ <ChatBubbleFiles files={files} />
159
+ </MessageHoverActions>
160
+ ) : null}
161
+
162
+ {audio ? (
163
+ <MessageHoverActions mine={mine} onReply={onReply} isItemButton={["replay"]}>
164
+ <ChatBubbleAudio mine={mine} audio={audio} />
165
+ </MessageHoverActions>
166
+ ) : null}
167
+
168
+ {typeof text === "string" && text.length > 0 ? (
169
+ <MessageHoverActions
170
+ mine={mine}
171
+ onReply={onReply}
172
+ onTranslate={handleTranslateClick}
173
+ // show both buttons for text; mark Translate active when toggled
174
+ isItemButton={["replay", "translate"]}
175
+ activeButtons={translated ? ["translate"] : []}
176
+ >
177
+ <ChatBubbleText mine={mine} text={displayText} />
178
+ </MessageHoverActions>
179
+ ) : null}
180
+
181
+ {showStatus && (
182
+ <div className={clsx("text-xs text-[#9AA1A9]", mine ? "text-right" : "text-left")}>
183
+ {status}
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ );
191
+ };
192
+
193
+ export default ChatMessageItem;