@banbox/chat 1.0.7 → 1.0.9
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 +1236 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1160 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +105 -42
- package/src/chat/SinglePopup.tsx +59 -14
- package/src/icons/index.tsx +55 -0
- package/src/index.ts +14 -12
- package/src/modals/ChatAddressModal.tsx +844 -0
- package/src/modals/{chat/ChatConfirmModal.tsx → ChatConfirmModal.tsx} +2 -2
- package/src/modals/ChatTranslateSettingsModal.tsx +182 -0
- package/src/styles/index.build.css +15 -0
- package/src/styles/index.css +10 -2
- package/src/ui/{chat/AttachmentPreviewStrip.tsx → AttachmentPreviewStrip.tsx} +2 -2
- package/src/ui/{chat/ChatComposerBar.tsx → ChatComposerBar.tsx} +2 -2
- package/src/ui/{chat/ChatFooter.tsx → ChatFooter.tsx} +102 -8
- package/src/ui/{chat/ChatHeader.tsx → ChatHeader.tsx} +7 -4
- package/src/ui/{chat/ChatIdentity.tsx → ChatIdentity.tsx} +2 -2
- package/src/ui/{chat/ChatInquiryBar.tsx → ChatInquiryBar.tsx} +1 -1
- package/src/ui/ChatKebabMenu.tsx +125 -0
- package/src/ui/{chat/ChatListHeader.tsx → ChatListHeader.tsx} +49 -25
- package/src/ui/{chat/ChatMessageItem.tsx → ChatMessageItem.tsx} +1 -1
- package/src/ui/ChatScroll.tsx +59 -0
- package/src/ui/{chat/ChatSpinner.tsx → ChatSpinner.tsx} +1 -1
- package/src/ui/{chat/ChatThreadItem.tsx → ChatThreadItem.tsx} +9 -16
- package/src/ui/{chat/MessageHoverActions.tsx → MessageHoverActions.tsx} +2 -2
- package/src/ui/{chat/ReplyCard.tsx → ReplyCard.tsx} +2 -2
- package/src/ui/{chat/TypingIndicator.tsx → TypingIndicator.tsx} +1 -1
- package/src/ui/{chat/drop-up → drop-up}/BusinessCardDropup.tsx +15 -3
- package/src/ui/{chat/drop-up → drop-up}/EmojiDropup.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatAddressCard.tsx +4 -4
- package/src/ui/{chat/message-items → message-items}/ChatBubbleFiles.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatBubbleImages.tsx +2 -2
- package/src/ui/{chat/message-items → message-items}/ChatBusinessCard.tsx +1 -1
- package/src/ui/{chat/scrollToMessage.ts → scrollToMessage.ts} +1 -1
- package/src/ui/{chat/types.ts → types.ts} +2 -2
- package/src/modals/chat/ChatTranslateSettingsModal.tsx +0 -180
- package/src/ui/chat/ChatScroll.tsx +0 -52
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleAudio.tsx +0 -0
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleText.tsx +0 -0
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import clsx from "clsx";
|
|
3
4
|
import type { Variants } from "framer-motion";
|
|
4
5
|
import { AnimatePresence, motion } from "framer-motion";
|
|
5
6
|
import React from "react";
|
|
6
|
-
import { ChatSearchIcon, ChatXIcon, MessageIcon } from "
|
|
7
|
+
import { ChatSearchIcon, ChatXIcon, MessageIcon } from "../icons";
|
|
7
8
|
|
|
8
9
|
type Props = {
|
|
9
10
|
className?: string;
|
|
10
11
|
onClose?: () => void;
|
|
11
12
|
onSearchChange?: (value: string) => void;
|
|
13
|
+
hideSearch?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
const ChatListHeader: React.FC<Props> = ({
|
|
16
|
+
const ChatListHeader: React.FC<Props> = ({
|
|
17
|
+
className,
|
|
18
|
+
onClose,
|
|
19
|
+
onSearchChange,
|
|
20
|
+
hideSearch = false,
|
|
21
|
+
}) => {
|
|
15
22
|
const [searching, setSearching] = React.useState(false);
|
|
16
23
|
const [q, setQ] = React.useState("");
|
|
17
24
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
18
25
|
|
|
19
26
|
React.useEffect(() => {
|
|
20
27
|
const timer = searching
|
|
21
|
-
? setTimeout(() => {
|
|
28
|
+
? setTimeout(() => {
|
|
29
|
+
inputRef.current?.focus();
|
|
30
|
+
}, 220)
|
|
22
31
|
: undefined;
|
|
23
|
-
return () => {
|
|
32
|
+
return () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
};
|
|
24
35
|
}, [searching]);
|
|
25
36
|
|
|
26
37
|
React.useEffect(() => {
|
|
27
|
-
if (!searching)
|
|
38
|
+
if (!searching) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
28
41
|
const onKey = (e: KeyboardEvent) => {
|
|
29
42
|
if (e.key === "Escape") {
|
|
30
43
|
setSearching(false);
|
|
@@ -33,18 +46,20 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
33
46
|
}
|
|
34
47
|
};
|
|
35
48
|
window.addEventListener("keydown", onKey);
|
|
36
|
-
return () => {
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener("keydown", onKey);
|
|
51
|
+
};
|
|
37
52
|
}, [searching, onSearchChange]);
|
|
38
53
|
|
|
39
54
|
const variants: Variants = {
|
|
40
55
|
inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
41
|
-
outToLeft:
|
|
42
|
-
inFromLeft:
|
|
43
|
-
outToRight:
|
|
56
|
+
outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
57
|
+
inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
58
|
+
outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
44
59
|
};
|
|
45
60
|
|
|
46
61
|
return (
|
|
47
|
-
<div className={
|
|
62
|
+
<div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
|
|
48
63
|
<div className="flex h-full items-center px-[20px]">
|
|
49
64
|
<AnimatePresence initial={false} mode="wait">
|
|
50
65
|
{!searching ? (
|
|
@@ -64,13 +79,16 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
64
79
|
</div>
|
|
65
80
|
|
|
66
81
|
<div className="flex items-center gap-2">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
{!hideSearch && (
|
|
83
|
+
<button
|
|
84
|
+
title="Search"
|
|
85
|
+
onClick={() => setSearching(true)}
|
|
86
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center cursor-pointer border-none bg-transparent"
|
|
87
|
+
>
|
|
88
|
+
<ChatSearchIcon className="w-5 h-5" />
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
|
|
74
92
|
<button
|
|
75
93
|
title="Close"
|
|
76
94
|
onClick={onClose}
|
|
@@ -95,7 +113,7 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
95
113
|
<span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
|
|
96
114
|
<ChatSearchIcon className="w-5 h-5" />
|
|
97
115
|
</span>
|
|
98
|
-
|
|
116
|
+
|
|
99
117
|
<input
|
|
100
118
|
ref={inputRef}
|
|
101
119
|
value={q}
|
|
@@ -108,13 +126,19 @@ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange })
|
|
|
108
126
|
/>
|
|
109
127
|
</div>
|
|
110
128
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
<div>
|
|
130
|
+
<button
|
|
131
|
+
title="Close search"
|
|
132
|
+
onClick={() => {
|
|
133
|
+
setSearching(false);
|
|
134
|
+
setQ("");
|
|
135
|
+
onSearchChange?.("");
|
|
136
|
+
}}
|
|
137
|
+
className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5 cursor-pointer border-none bg-transparent"
|
|
138
|
+
>
|
|
139
|
+
<ChatXIcon className="w-5 h-5" />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
118
142
|
</div>
|
|
119
143
|
</div>
|
|
120
144
|
</motion.div>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ui/ChatScroll.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
top?: React.ReactNode;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
/** set true if you want short threads anchored at the bottom */
|
|
12
|
+
bottomAlignWhenShort?: boolean;
|
|
13
|
+
/** when this value changes, we auto-scroll to the bottom */
|
|
14
|
+
scrollKey?: string | number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ChatScroll = React.forwardRef<HTMLDivElement, Props>(function ChatScroll(
|
|
18
|
+
{ top, children, className, bottomAlignWhenShort = false, scrollKey },
|
|
19
|
+
_,
|
|
20
|
+
) {
|
|
21
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
const scrollToBottom = React.useCallback(() => {
|
|
24
|
+
const el = ref.current;
|
|
25
|
+
if (!el) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
el.scrollTop = el.scrollHeight;
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
// On mount & when scrollKey changes — useLayoutEffect guarantees synchronous scroll before paint!
|
|
32
|
+
React.useLayoutEffect(() => {
|
|
33
|
+
scrollToBottom();
|
|
34
|
+
}, [scrollKey, scrollToBottom]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
data-chat-scroll
|
|
40
|
+
className={clsx(
|
|
41
|
+
"h-full min-h-0 overflow-y-auto bg-white p-4 custom-scroll-hidden",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{/* This wrapper ensures content is at least as tall as the scroll area */}
|
|
46
|
+
<div
|
|
47
|
+
className={clsx(
|
|
48
|
+
"min-h-full flex flex-col",
|
|
49
|
+
bottomAlignWhenShort ? "justify-end" : "justify-between",
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{top}
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default ChatScroll;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import React from "react";
|
|
5
|
-
import { BlueBadgeIcon } from "
|
|
5
|
+
import { BlueBadgeIcon } from "../icons";
|
|
6
6
|
|
|
7
7
|
export type ChatThreadStatus =
|
|
8
8
|
| { kind: "seen" }
|
|
@@ -16,14 +16,13 @@ type Props = {
|
|
|
16
16
|
verified?: boolean;
|
|
17
17
|
|
|
18
18
|
title: string;
|
|
19
|
-
preview: string;
|
|
20
|
-
time: string;
|
|
19
|
+
preview: string;
|
|
20
|
+
time: string;
|
|
21
21
|
status: ChatThreadStatus;
|
|
22
22
|
|
|
23
|
-
avatarText: string;
|
|
24
|
-
avatarSrc?: string;
|
|
25
|
-
|
|
26
|
-
avatarBg?: string; // default soft peach
|
|
23
|
+
avatarText: string;
|
|
24
|
+
avatarSrc?: string;
|
|
25
|
+
avatarBg?: string;
|
|
27
26
|
className?: string;
|
|
28
27
|
onClick?: () => void;
|
|
29
28
|
};
|
|
@@ -39,28 +38,22 @@ const ChatThreadItem: React.FC<Props> = ({
|
|
|
39
38
|
status,
|
|
40
39
|
avatarText,
|
|
41
40
|
avatarSrc,
|
|
42
|
-
_size = 46,
|
|
43
41
|
avatarBg = "#FFF1EC",
|
|
44
42
|
className,
|
|
45
43
|
onClick,
|
|
46
44
|
}) => {
|
|
47
|
-
const count = status.kind === "new" ? String(Math.max(0, status.count)).padStart(2, "0") : "";
|
|
48
|
-
|
|
49
45
|
const statusEl = (() => {
|
|
50
46
|
switch (status.kind) {
|
|
51
47
|
case "seen":
|
|
52
48
|
return <span className="text-[#0D5EA8]">Seen</span>;
|
|
53
|
-
|
|
54
49
|
case "delivered":
|
|
55
50
|
return <span className="text-[#B7B7B7]">Delivered</span>;
|
|
56
|
-
|
|
57
51
|
case "new":
|
|
58
52
|
return (
|
|
59
53
|
<span className="text-[#E63946]">
|
|
60
|
-
{count} New
|
|
54
|
+
{String(Math.max(0, status.count)).padStart(2, "0")} New
|
|
61
55
|
</span>
|
|
62
56
|
);
|
|
63
|
-
|
|
64
57
|
default:
|
|
65
58
|
return null;
|
|
66
59
|
}
|
|
@@ -81,10 +74,10 @@ const ChatThreadItem: React.FC<Props> = ({
|
|
|
81
74
|
)}
|
|
82
75
|
|
|
83
76
|
<div className="flex items-start gap-3 border-b border-[#f8f8f8] pb-2">
|
|
84
|
-
{/* Avatar + online */}
|
|
77
|
+
{/* Avatar + online dot */}
|
|
85
78
|
<div className="relative mt-[2px]">
|
|
86
79
|
{avatarSrc ? (
|
|
87
|
-
<div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1]
|
|
80
|
+
<div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1]">
|
|
88
81
|
<img
|
|
89
82
|
src={avatarSrc}
|
|
90
83
|
alt={title}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ui/MessageHoverActions.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import React from "react";
|
|
6
|
-
import { MessageReplayIcon, NewLanguageIcon } from "
|
|
6
|
+
import { MessageReplayIcon, NewLanguageIcon } from "../icons";
|
|
7
7
|
|
|
8
8
|
type ItemButton = "replay" | "translate";
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ui/ReplyCard.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import React from "react";
|
|
6
|
-
import { ArrowBackUpIcon, FileIcon, ChatXIcon } from "
|
|
6
|
+
import { ArrowBackUpIcon, FileIcon, ChatXIcon } from "../icons";
|
|
7
7
|
import { scrollToMessageById } from "./scrollToMessage";
|
|
8
8
|
import type { MessageRef } from "./types";
|
|
9
9
|
|
|
@@ -6,7 +6,7 @@ import _Lottie from "lottie-react";
|
|
|
6
6
|
const Lottie = ((_Lottie as any).default ?? _Lottie) as typeof _Lottie;
|
|
7
7
|
|
|
8
8
|
import React from "react";
|
|
9
|
-
import dots from "
|
|
9
|
+
import dots from "../lottie/typingdotanimation2.json";
|
|
10
10
|
|
|
11
11
|
type Props = {
|
|
12
12
|
size?: number;
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
|
|
6
|
-
import { BusinessInfoIcon, ChatMailIcon, ChatPhoneCallIcon, ChatXIcon } from "
|
|
6
|
+
import { BusinessInfoIcon, ChatMailIcon, ChatPhoneCallIcon, ChatXIcon } from "../../icons";
|
|
7
7
|
|
|
8
|
-
import { cn } from "
|
|
8
|
+
import { cn } from "../../utils/cn";
|
|
9
9
|
import type { BusinessCard } from "../types";
|
|
10
10
|
|
|
11
11
|
/* =======================
|
|
@@ -19,6 +19,8 @@ type BusinessCardDropupProps = {
|
|
|
19
19
|
/** Position against this button (like EmojiDropup) */
|
|
20
20
|
anchorRef?: React.RefObject<HTMLElement | null>;
|
|
21
21
|
className?: string;
|
|
22
|
+
/** Called when the Edit button is clicked — opens business info modal */
|
|
23
|
+
onEdit?: () => void;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
/* =======================
|
|
@@ -49,6 +51,7 @@ const BusinessCardDropup = ({
|
|
|
49
51
|
onSend,
|
|
50
52
|
anchorRef,
|
|
51
53
|
className,
|
|
54
|
+
onEdit,
|
|
52
55
|
}: BusinessCardDropupProps) => {
|
|
53
56
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
54
57
|
const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
|
|
@@ -233,7 +236,16 @@ const BusinessCardDropup = ({
|
|
|
233
236
|
|
|
234
237
|
{/* Footer actions */}
|
|
235
238
|
<div className="mt-3 flex justify-end gap-2">
|
|
236
|
-
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={() => {
|
|
242
|
+
onClose();
|
|
243
|
+
onEdit?.();
|
|
244
|
+
}}
|
|
245
|
+
className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
|
|
246
|
+
>
|
|
247
|
+
Edit
|
|
248
|
+
</button>
|
|
237
249
|
<button
|
|
238
250
|
type="button"
|
|
239
251
|
disabled={disabled}
|
|
@@ -98,7 +98,7 @@ const EmojiDropup: React.FC<Props> = ({ open, onClose, onSelect, anchorRef, clas
|
|
|
98
98
|
aria-label="Emoji picker"
|
|
99
99
|
className={clsx("emoji-dropup", className)}
|
|
100
100
|
style={{
|
|
101
|
-
zIndex:
|
|
101
|
+
zIndex: 99999, // Must be above chat panel z-index (10002)
|
|
102
102
|
width: WIDTH,
|
|
103
103
|
left: pos?.left ?? -9999,
|
|
104
104
|
top: pos?.top ?? -9999,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ui/message-items/ChatAddressCard.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import React from "react";
|
|
6
6
|
|
|
7
|
-
import { ChatPhoneCallIcon, BadgeHomeIcon, MapPinIcon } from "
|
|
8
|
-
import { BadgeOfficeIcon } from "
|
|
9
|
-
import { cn } from "
|
|
7
|
+
import { ChatPhoneCallIcon, BadgeHomeIcon, MapPinIcon } from "../../icons";
|
|
8
|
+
import { BadgeOfficeIcon } from "../../icons";
|
|
9
|
+
import { cn } from "../../utils/cn";
|
|
10
10
|
import type { AddressCard } from "../types";
|
|
11
11
|
|
|
12
12
|
type Props = {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ui/message-items/ChatBubbleImages.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import React from "react";
|
|
5
5
|
|
|
6
|
-
import { useGallery } from "
|
|
6
|
+
import { useGallery } from "../../contexts/GalleryContext";
|
|
7
7
|
|
|
8
8
|
type ImgTileProps = {
|
|
9
9
|
src: string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import React from "react";
|
|
5
5
|
|
|
6
|
-
import { BusinessInfoIcon, ChatMailIcon, ChatPhoneCallIcon } from "
|
|
6
|
+
import { BusinessInfoIcon, ChatMailIcon, ChatPhoneCallIcon } from "../../icons";
|
|
7
7
|
import type { BusinessCard } from "../types";
|
|
8
8
|
|
|
9
9
|
// const AVATAR_SIZE = 60; // uncomment if avatar section is re-enabled
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
import type { MessageAudio, MessageFile } from "
|
|
1
|
+
// ui/types.ts
|
|
2
|
+
import type { MessageAudio, MessageFile } from "../types";
|
|
3
3
|
|
|
4
4
|
// Re-export global types so UI components can import from this local file
|
|
5
5
|
export type { MessageAudio as ChatAudio, MessageFile as ChatFile };
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import clsx from "clsx";
|
|
4
|
-
import React, { useState } from "react";
|
|
5
|
-
import { createPortal } from "react-dom";
|
|
6
|
-
|
|
7
|
-
import { ChatInfoIcon } from "../../icons";
|
|
8
|
-
import Button from "../../ui/Button";
|
|
9
|
-
import Select from "../../ui/Select";
|
|
10
|
-
|
|
11
|
-
/* ───────── Types ───────── */
|
|
12
|
-
|
|
13
|
-
export type TranslateSettings = {
|
|
14
|
-
incomingTarget?: string;
|
|
15
|
-
autoIncoming: boolean;
|
|
16
|
-
enableOutgoing: boolean;
|
|
17
|
-
outgoingFrom: string;
|
|
18
|
-
outgoingTo: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type Props = {
|
|
22
|
-
open: boolean;
|
|
23
|
-
onClose: () => void;
|
|
24
|
-
onSave: (settings: TranslateSettings) => void;
|
|
25
|
-
initial?: Partial<TranslateSettings>;
|
|
26
|
-
className?: string;
|
|
27
|
-
/** Controls layout / placement */
|
|
28
|
-
variant?: "single" | "group";
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/* Language options */
|
|
32
|
-
const LANG_OPTIONS = [
|
|
33
|
-
"English",
|
|
34
|
-
"Bangla",
|
|
35
|
-
"Arabic",
|
|
36
|
-
"Chinese",
|
|
37
|
-
"French",
|
|
38
|
-
"German",
|
|
39
|
-
"Hindi",
|
|
40
|
-
"Italian",
|
|
41
|
-
"Japanese",
|
|
42
|
-
"Korean",
|
|
43
|
-
"Portuguese",
|
|
44
|
-
"Russian",
|
|
45
|
-
"Spanish",
|
|
46
|
-
"Turkish",
|
|
47
|
-
"Urdu",
|
|
48
|
-
].map((l) => ({ label: l, value: l }));
|
|
49
|
-
|
|
50
|
-
const ChatTranslateSettingsModal: React.FC<Props> = ({
|
|
51
|
-
open,
|
|
52
|
-
onClose,
|
|
53
|
-
onSave,
|
|
54
|
-
initial,
|
|
55
|
-
className,
|
|
56
|
-
variant = "group",
|
|
57
|
-
}) => {
|
|
58
|
-
const [incomingTarget, setIncomingTarget] = useState(
|
|
59
|
-
initial?.incomingTarget ?? "",
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// ESC to close
|
|
63
|
-
React.useEffect(() => {
|
|
64
|
-
if (!open) return;
|
|
65
|
-
const onKey = (e: KeyboardEvent) => {
|
|
66
|
-
if (e.key === "Escape") onClose();
|
|
67
|
-
};
|
|
68
|
-
window.addEventListener("keydown", onKey);
|
|
69
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
70
|
-
}, [open, onClose]);
|
|
71
|
-
|
|
72
|
-
if (!open) return null;
|
|
73
|
-
|
|
74
|
-
const isSingle = variant === "single";
|
|
75
|
-
|
|
76
|
-
const handleSave = () => {
|
|
77
|
-
onSave({
|
|
78
|
-
incomingTarget,
|
|
79
|
-
autoIncoming: true,
|
|
80
|
-
enableOutgoing: false,
|
|
81
|
-
outgoingFrom: "English",
|
|
82
|
-
outgoingTo: "Bangla",
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const content = (
|
|
87
|
-
<div
|
|
88
|
-
className={clsx(
|
|
89
|
-
isSingle
|
|
90
|
-
? "fixed inset-0 z-9999 flex"
|
|
91
|
-
: "absolute inset-0 z-50 flex items-center justify-center",
|
|
92
|
-
)}
|
|
93
|
-
onClick={() => onClose()}
|
|
94
|
-
>
|
|
95
|
-
{/* Backdrop */}
|
|
96
|
-
<div className={isSingle ? "fixed inset-0 bg-black/30" : "absolute inset-0 bg-black/30"} />
|
|
97
|
-
|
|
98
|
-
<div
|
|
99
|
-
role="dialog"
|
|
100
|
-
aria-modal="true"
|
|
101
|
-
aria-labelledby="translate-settings-title"
|
|
102
|
-
onClick={(e) => e.stopPropagation()}
|
|
103
|
-
className={clsx(
|
|
104
|
-
isSingle
|
|
105
|
-
? "fixed bottom-6 right-6 w-[500px] max-w-[95vw]"
|
|
106
|
-
: "relative w-[500px] max-w-[95vw]",
|
|
107
|
-
"z-10000 overflow-visible rounded-md bg-white shadow-[0_12px_30px_rgba(0,0,0,0.18)]",
|
|
108
|
-
className,
|
|
109
|
-
)}
|
|
110
|
-
>
|
|
111
|
-
{/* Header */}
|
|
112
|
-
<div className="flex h-[44px] w-full items-center rounded-t-md bg-[#f8f8f8] px-6 py-[7px] shadow-[0px_2px_2px_rgba(47,47,47,0.08)]">
|
|
113
|
-
<h2
|
|
114
|
-
id="translate-settings-title"
|
|
115
|
-
className="w-full text-[20px] font-semibold text-black"
|
|
116
|
-
>
|
|
117
|
-
Translation Settings
|
|
118
|
-
</h2>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
{/* Body */}
|
|
122
|
-
<div className="p-4">
|
|
123
|
-
<div className="grid gap-2">
|
|
124
|
-
<span className="text-[12px] font-medium text-black">
|
|
125
|
-
Translate message into
|
|
126
|
-
</span>
|
|
127
|
-
|
|
128
|
-
<Select
|
|
129
|
-
options={LANG_OPTIONS}
|
|
130
|
-
value={incomingTarget}
|
|
131
|
-
onChange={setIncomingTarget}
|
|
132
|
-
placeholder="Select Language"
|
|
133
|
-
size={36}
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div className="mt-6 flex items-start gap-1.5 text-[#FF5300]">
|
|
138
|
-
<ChatInfoIcon className="h-4 w-4 shrink-0" />
|
|
139
|
-
<p className="text-xs leading-relaxed">
|
|
140
|
-
Automatically translate incoming messages. The language you save
|
|
141
|
-
here will be used to display all incoming messages. You can choose
|
|
142
|
-
from Spanish, Russian, French, Arabic, Portuguese, Turkish, Bangla,
|
|
143
|
-
and among others.
|
|
144
|
-
</p>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
{/* Footer */}
|
|
149
|
-
<div className="flex h-[52px] items-center justify-end rounded-b-md bg-[#f8f8f8] px-6">
|
|
150
|
-
<div className="flex items-center gap-3">
|
|
151
|
-
<Button
|
|
152
|
-
onClick={onClose}
|
|
153
|
-
variant="outlined"
|
|
154
|
-
color="black"
|
|
155
|
-
size="34"
|
|
156
|
-
>
|
|
157
|
-
Cancel
|
|
158
|
-
</Button>
|
|
159
|
-
<Button
|
|
160
|
-
onClick={handleSave}
|
|
161
|
-
variant="filled"
|
|
162
|
-
color="black"
|
|
163
|
-
size="34"
|
|
164
|
-
>
|
|
165
|
-
Update
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (isSingle) {
|
|
174
|
-
return createPortal(content, document.body);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return content;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
export default ChatTranslateSettingsModal;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import React from "react";
|
|
3
|
-
|
|
4
|
-
type Props = {
|
|
5
|
-
top?: React.ReactNode;
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
className?: string;
|
|
8
|
-
style?: React.CSSProperties;
|
|
9
|
-
bottomAlignWhenShort?: boolean;
|
|
10
|
-
scrollKey?: string | number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const ChatScroll: React.FC<Props> = ({
|
|
14
|
-
top,
|
|
15
|
-
children,
|
|
16
|
-
className,
|
|
17
|
-
style,
|
|
18
|
-
bottomAlignWhenShort = false,
|
|
19
|
-
scrollKey,
|
|
20
|
-
}) => {
|
|
21
|
-
const ref = React.useRef<HTMLDivElement>(null);
|
|
22
|
-
|
|
23
|
-
const scrollToBottom = React.useCallback(() => {
|
|
24
|
-
const el = ref.current;
|
|
25
|
-
if (!el) return;
|
|
26
|
-
el.scrollTop = el.scrollHeight;
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
React.useEffect(() => {
|
|
30
|
-
scrollToBottom();
|
|
31
|
-
const id = window.setTimeout(scrollToBottom, 0);
|
|
32
|
-
return () => window.clearTimeout(id);
|
|
33
|
-
}, [scrollKey, scrollToBottom]);
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div
|
|
37
|
-
ref={ref}
|
|
38
|
-
data-chat-scroll
|
|
39
|
-
className={`h-full min-h-0 overflow-y-auto bg-white p-4 custom-scroll-hidden${className ? ` ${className}` : ""}`}
|
|
40
|
-
style={style}
|
|
41
|
-
>
|
|
42
|
-
<div
|
|
43
|
-
className={`min-h-full flex flex-col${bottomAlignWhenShort ? " justify-end" : " justify-start"}`}
|
|
44
|
-
>
|
|
45
|
-
{top}
|
|
46
|
-
{children}
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export default ChatScroll;
|
|
File without changes
|
|
File without changes
|