@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.
- package/dist/index.cjs +564 -576
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -41
- package/dist/index.d.ts +43 -41
- package/dist/index.js +486 -499
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/chat/InboxPopup.tsx +51 -46
- package/src/chat/SinglePopup.tsx +47 -58
- package/src/lottie/banbox-chat-globe.json +1 -0
- package/src/ui/chat/AttachmentPreviewStrip.tsx +63 -112
- package/src/ui/chat/ChatComposerBar.tsx +22 -38
- package/src/ui/chat/ChatFooter.tsx +1 -1
- package/src/ui/chat/ChatIdentity.tsx +148 -145
- package/src/ui/chat/ChatListHeader.tsx +60 -83
- package/src/ui/chat/ChatMessageItem.tsx +193 -214
- package/src/ui/chat/ChatThreadItem.tsx +133 -140
- package/src/ui/chat/MessageHoverActions.tsx +136 -120
- package/src/ui/chat/TypingIndicator.tsx +23 -43
- package/src/ui/chat/drop-up/BusinessCardDropup.tsx +9 -1
- package/src/ui/chat/types.ts +42 -37
|
@@ -1,145 +1,148 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
className=
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{props.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import Lottie from "lottie-react";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { BlueBadgeIcon } from "../../icons";
|
|
7
|
+
import globe from "../../lottie/banbox-chat-globe.json";
|
|
8
|
+
|
|
9
|
+
type SubtitleVariant = "live" | "muted";
|
|
10
|
+
|
|
11
|
+
type BaseProps = {
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
subtitleVariant?: SubtitleVariant;
|
|
15
|
+
online?: boolean;
|
|
16
|
+
verified?: boolean;
|
|
17
|
+
/** Circle size in px (default 46) */
|
|
18
|
+
size?: number;
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AvatarVariant = BaseProps & {
|
|
23
|
+
variant: "avatar";
|
|
24
|
+
src: string; // person photo
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type InitialVariant = BaseProps & {
|
|
28
|
+
variant: "initial";
|
|
29
|
+
initial: string; // e.g. "K"
|
|
30
|
+
initialSrc?: string;
|
|
31
|
+
bg?: string; // circle background
|
|
32
|
+
textClassName?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type LogoVariant = BaseProps & {
|
|
36
|
+
variant: "logo";
|
|
37
|
+
src: string; // brand logo
|
|
38
|
+
ringColor?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ChatIdentityProps = AvatarVariant | InitialVariant | LogoVariant;
|
|
42
|
+
|
|
43
|
+
const ChatIdentity: React.FC<ChatIdentityProps> = (props) => {
|
|
44
|
+
const {
|
|
45
|
+
title,
|
|
46
|
+
subtitle,
|
|
47
|
+
subtitleVariant = "muted",
|
|
48
|
+
online = false,
|
|
49
|
+
verified = false,
|
|
50
|
+
size = 46,
|
|
51
|
+
className,
|
|
52
|
+
} = props;
|
|
53
|
+
|
|
54
|
+
const subtitleClass = clsx(
|
|
55
|
+
"text-[10px] font-medium",
|
|
56
|
+
subtitleVariant === "live" ? "text-[#1E9E6A]" : "text-[#929292]",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={clsx("flex items-start gap-3", className)}>
|
|
61
|
+
<div className="relative" style={{ width: size, height: size }}>
|
|
62
|
+
{/* Leading visual by variant */}
|
|
63
|
+
{props.variant === "avatar" && (
|
|
64
|
+
<img
|
|
65
|
+
src={props.src}
|
|
66
|
+
alt={title}
|
|
67
|
+
className="h-full w-full rounded-xs object-cover border border-[#f1f1f1]"
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{props.variant === "initial" && (
|
|
72
|
+
<div
|
|
73
|
+
className={clsx(
|
|
74
|
+
"grid h-full w-full place-items-center rounded-xs text-[15px] font-semibold text-[#2c2c2c]",
|
|
75
|
+
props.textClassName,
|
|
76
|
+
)}
|
|
77
|
+
style={{ backgroundColor: props.bg ?? "#FFE7DB" }}
|
|
78
|
+
>
|
|
79
|
+
{props.initial}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{props.variant === "logo" && (
|
|
84
|
+
<div
|
|
85
|
+
className="grid h-full w-full place-items-center rounded-xs"
|
|
86
|
+
style={{ boxShadow: `0 0 0 1px ${props.ringColor ?? "#EDEDED"} inset` }}
|
|
87
|
+
>
|
|
88
|
+
<img
|
|
89
|
+
src={props.src}
|
|
90
|
+
alt={title}
|
|
91
|
+
className="h-full w-full rounded-xs object-cover"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Online dot — RIGHT SIDE */}
|
|
97
|
+
{online ? (
|
|
98
|
+
<span
|
|
99
|
+
className="absolute rounded-full bg-[#1E8E3E] ring-1 ring-white"
|
|
100
|
+
style={{
|
|
101
|
+
width: "15px",
|
|
102
|
+
height: "15px",
|
|
103
|
+
right: 0,
|
|
104
|
+
bottom: 0,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
) : (
|
|
108
|
+
<span
|
|
109
|
+
className="absolute rounded-full bg-[#eb2127] ring-1 ring-white"
|
|
110
|
+
style={{
|
|
111
|
+
width: "15px",
|
|
112
|
+
height: "15px",
|
|
113
|
+
right: 0,
|
|
114
|
+
bottom: 0,
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div>
|
|
121
|
+
<div className="text-[16px] font-semibold text-[#2c2c2c] flex items-center gap-1">
|
|
122
|
+
<span className="max-w-[300px] truncate">{title}</span>
|
|
123
|
+
{verified && (
|
|
124
|
+
<span>
|
|
125
|
+
<BlueBadgeIcon />
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
{subtitle &&
|
|
130
|
+
(title === "banbox.com" ? (
|
|
131
|
+
<div className="flex items-center -ml-[5px]">
|
|
132
|
+
<Lottie
|
|
133
|
+
animationData={globe}
|
|
134
|
+
loop={true}
|
|
135
|
+
autoplay={true}
|
|
136
|
+
style={{ width: "22px", marginBottom: "1.5px" }}
|
|
137
|
+
/>
|
|
138
|
+
<div className={subtitleClass}>{subtitle}</div>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className={subtitleClass}>{subtitle}</div>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default ChatIdentity;
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
// components/ui/chat/ChatListHeader.tsx
|
|
2
1
|
"use client";
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import clsx from "clsx";
|
|
5
4
|
import type { Variants } from "framer-motion";
|
|
6
5
|
import { AnimatePresence, motion } from "framer-motion";
|
|
7
|
-
|
|
6
|
+
import React from "react";
|
|
8
7
|
import { MessageIcon, ChatSearchIcon, ChatXIcon } from "../../icons";
|
|
9
|
-
import { cn } from "../../utils/cn";
|
|
10
|
-
|
|
11
|
-
/* =======================
|
|
12
|
-
Types
|
|
13
|
-
======================= */
|
|
14
8
|
|
|
15
9
|
type Props = {
|
|
16
10
|
className?: string;
|
|
@@ -18,27 +12,27 @@ type Props = {
|
|
|
18
12
|
onSearchChange?: (value: string) => void;
|
|
19
13
|
};
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange }) => {
|
|
16
|
+
const [searching, setSearching] = React.useState(false);
|
|
17
|
+
const [q, setQ] = React.useState("");
|
|
18
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
// AnimatePresence mode="wait" delays mounting; wait for enter animation
|
|
22
|
+
const timer = searching
|
|
23
|
+
? setTimeout(() => {
|
|
24
|
+
inputRef.current?.focus();
|
|
25
|
+
}, 220)
|
|
26
|
+
: undefined;
|
|
27
|
+
return () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
};
|
|
35
30
|
}, [searching]);
|
|
36
31
|
|
|
37
|
-
useEffect(() => {
|
|
32
|
+
React.useEffect(() => {
|
|
38
33
|
if (!searching) {
|
|
39
34
|
return;
|
|
40
35
|
}
|
|
41
|
-
|
|
42
36
|
const onKey = (e: KeyboardEvent) => {
|
|
43
37
|
if (e.key === "Escape") {
|
|
44
38
|
setSearching(false);
|
|
@@ -46,48 +40,32 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
|
|
|
46
40
|
onSearchChange?.("");
|
|
47
41
|
}
|
|
48
42
|
};
|
|
49
|
-
|
|
50
43
|
window.addEventListener("keydown", onKey);
|
|
51
|
-
return () =>
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener("keydown", onKey);
|
|
46
|
+
};
|
|
52
47
|
}, [searching, onSearchChange]);
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
const _clearInside = () => {
|
|
50
|
+
setQ("");
|
|
51
|
+
onSearchChange?.("");
|
|
52
|
+
inputRef.current?.focus();
|
|
53
|
+
};
|
|
59
54
|
|
|
55
|
+
// Use cubic-bezier tuples to satisfy the Transition type
|
|
60
56
|
const variants: Variants = {
|
|
61
|
-
inFromRight: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
outToLeft: {
|
|
67
|
-
opacity: 0,
|
|
68
|
-
x: -24,
|
|
69
|
-
transition: { duration: 0.16, ease: [0.4, 0, 1, 1] },
|
|
70
|
-
},
|
|
71
|
-
inFromLeft: {
|
|
72
|
-
opacity: 1,
|
|
73
|
-
x: 0,
|
|
74
|
-
transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] },
|
|
75
|
-
},
|
|
76
|
-
outToRight: {
|
|
77
|
-
opacity: 0,
|
|
78
|
-
x: 24,
|
|
79
|
-
transition: { duration: 0.16, ease: [0.4, 0, 1, 1] },
|
|
80
|
-
},
|
|
57
|
+
inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
58
|
+
outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
59
|
+
inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
|
|
60
|
+
outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
81
61
|
};
|
|
82
62
|
|
|
83
63
|
return (
|
|
84
|
-
<div className={
|
|
64
|
+
<div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
|
|
85
65
|
<div className="flex h-full items-center px-[20px]">
|
|
86
66
|
<AnimatePresence initial={false} mode="wait">
|
|
87
67
|
{!searching ? (
|
|
88
|
-
|
|
89
|
-
Normal header
|
|
90
|
-
======================= */
|
|
68
|
+
// NORMAL (title) — appears from left when closing search
|
|
91
69
|
<motion.div
|
|
92
70
|
key="normal"
|
|
93
71
|
className="flex w-full items-center justify-between"
|
|
@@ -97,36 +75,34 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
|
|
|
97
75
|
variants={variants}
|
|
98
76
|
>
|
|
99
77
|
<div className="flex items-center gap-3">
|
|
100
|
-
<div className="flex items-center gap-2
|
|
101
|
-
<MessageIcon className="
|
|
102
|
-
<span className="text-[22px] font-semibold">
|
|
78
|
+
<div className="text-[#2c2c2c] flex items-center gap-2">
|
|
79
|
+
<MessageIcon className="w-6 h-6" />
|
|
80
|
+
<span className="text-[22px] font-semibold">
|
|
81
|
+
Messenger
|
|
82
|
+
</span>
|
|
103
83
|
</div>
|
|
104
84
|
</div>
|
|
105
85
|
|
|
106
86
|
<div className="flex items-center gap-2">
|
|
107
87
|
<button
|
|
108
|
-
type="button"
|
|
109
88
|
title="Search"
|
|
110
89
|
onClick={() => setSearching(true)}
|
|
111
|
-
className="
|
|
90
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
|
|
112
91
|
>
|
|
113
|
-
<ChatSearchIcon className="
|
|
92
|
+
<ChatSearchIcon className="w-5 h-5" />
|
|
114
93
|
</button>
|
|
115
94
|
|
|
116
95
|
<button
|
|
117
|
-
type="button"
|
|
118
96
|
title="Close"
|
|
119
97
|
onClick={onClose}
|
|
120
|
-
className="
|
|
98
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
|
|
121
99
|
>
|
|
122
|
-
<ChatXIcon className="
|
|
100
|
+
<ChatXIcon className="w-6 h-6" />
|
|
123
101
|
</button>
|
|
124
102
|
</div>
|
|
125
103
|
</motion.div>
|
|
126
104
|
) : (
|
|
127
|
-
|
|
128
|
-
Search header
|
|
129
|
-
======================= */
|
|
105
|
+
// SEARCH — enters from right, exits to right
|
|
130
106
|
<motion.div
|
|
131
107
|
key="search"
|
|
132
108
|
className="flex w-full items-center gap-3"
|
|
@@ -136,10 +112,10 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
|
|
|
136
112
|
variants={variants}
|
|
137
113
|
>
|
|
138
114
|
<div className="relative flex-1">
|
|
139
|
-
<div className="flex
|
|
140
|
-
<div className="ms-[12px]
|
|
141
|
-
<span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text
|
|
142
|
-
<ChatSearchIcon className="
|
|
115
|
+
<div className="flex h-10 w-full items-center rounded-full border border-[#6A6A6A] bg-white px-[4px] gap-1.5">
|
|
116
|
+
<div className="flex items-center ms-[12px] w-full">
|
|
117
|
+
<span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
|
|
118
|
+
<ChatSearchIcon className="w-5 h-5" />
|
|
143
119
|
</span>
|
|
144
120
|
<span className="mr-2 h-6 w-px shrink-0 bg-[#e1e1e1]" />
|
|
145
121
|
<input
|
|
@@ -154,18 +130,19 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
|
|
|
154
130
|
/>
|
|
155
131
|
</div>
|
|
156
132
|
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
133
|
+
<div>
|
|
134
|
+
<button
|
|
135
|
+
title="Close search"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
setSearching(false);
|
|
138
|
+
setQ("");
|
|
139
|
+
onSearchChange?.("");
|
|
140
|
+
}}
|
|
141
|
+
className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5"
|
|
142
|
+
>
|
|
143
|
+
<ChatXIcon className="w-5 h-5" />
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
169
146
|
</div>
|
|
170
147
|
</div>
|
|
171
148
|
</motion.div>
|