@banbox/chat 1.0.1 → 1.0.3
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 +566 -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 +488 -499
- package/dist/index.js.map +1 -1
- package/package.json +7 -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 +152 -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 +27 -43
- package/src/ui/chat/drop-up/BusinessCardDropup.tsx +9 -1
- package/src/ui/chat/types.ts +42 -37
|
@@ -1,16 +1,11 @@
|
|
|
1
|
+
// components/chat/ui/chat/AttachmentPreviewStrip.tsx
|
|
1
2
|
"use client";
|
|
2
|
-
|
|
3
|
-
import
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import React from "react";
|
|
4
5
|
import { FileIcon, ChatXIcon } from "../../icons";
|
|
5
|
-
import { cn } from "../../utils/cn";
|
|
6
|
-
|
|
7
|
-
/* =======================
|
|
8
|
-
Helpers
|
|
9
|
-
======================= */
|
|
10
6
|
|
|
11
|
-
const extColor = (ext: string)
|
|
7
|
+
const extColor = (ext: string) => {
|
|
12
8
|
const e = ext.toLowerCase();
|
|
13
|
-
|
|
14
9
|
if (e === "pdf") {
|
|
15
10
|
return "text-[#D93025]";
|
|
16
11
|
}
|
|
@@ -20,143 +15,99 @@ const extColor = (ext: string): string => {
|
|
|
20
15
|
if (e === "doc" || e === "docx") {
|
|
21
16
|
return "text-[#2B579A]";
|
|
22
17
|
}
|
|
23
|
-
|
|
24
18
|
return "text-[#6B7280]";
|
|
25
19
|
};
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
Types
|
|
29
|
-
======================= */
|
|
30
|
-
|
|
31
|
-
export type FilePreview = {
|
|
21
|
+
export const FilePreviewChip: React.FC<{
|
|
32
22
|
name: string;
|
|
33
23
|
sizeMB: number;
|
|
34
24
|
ext: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
onRemove: () => void;
|
|
26
|
+
}> = ({ name, sizeMB, ext, onRemove }) => (
|
|
27
|
+
<div className="mr-2 inline-flex items-center gap-3 whitespace-nowrap rounded-sm border border-[#e1e1e1] bg-white px-3 py-2 h-[65px] max-w-[185px]">
|
|
28
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
29
|
+
<div className="min-w-0">
|
|
30
|
+
<div className="flex items-center gap-1">
|
|
31
|
+
<FileIcon className={clsx("h-[18px] w-[18px]", extColor(ext))} />{" "}
|
|
32
|
+
<div className="truncate text-sm font-normal text-black">{name}</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex items-center gap-2 text-xs text-[#636363] mt-2">
|
|
35
|
+
<span>{sizeMB.toFixed(1)} MB</span>
|
|
36
|
+
<span className="h-3 w-px bg-[#e1e1e1]" />
|
|
37
|
+
<span className="uppercase">{ext}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onRemove}
|
|
44
|
+
className="grid h-8 w-8 place-items-center rounded-full bg-white text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5"
|
|
45
|
+
title="Remove"
|
|
46
|
+
aria-label="Remove file"
|
|
47
|
+
>
|
|
48
|
+
<ChatXIcon className="h-[18px] w-[18px]" />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const ImageThumb: React.FC<{ url: string; onRemove: () => void }> = ({ url, onRemove }) => (
|
|
54
|
+
<div className="relative mr-2 inline-block h-[65px] w-[65px] rounded-sm border border-[#e1e1e1] bg-[#F7F7F7]">
|
|
55
|
+
<img src={url} alt="" className="h-full w-full object-cover rounded-sm" />
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={onRemove}
|
|
59
|
+
className="absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-black/30 text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D]"
|
|
60
|
+
aria-label="Remove image"
|
|
61
|
+
title="Remove image"
|
|
62
|
+
>
|
|
63
|
+
<ChatXIcon className="h-4 w-4 text-white" />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
38
67
|
|
|
39
|
-
type
|
|
68
|
+
export type FilePreview = {
|
|
40
69
|
name: string;
|
|
41
70
|
sizeMB: number;
|
|
42
71
|
ext: string;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
type ImageThumbProps = {
|
|
47
|
-
url: string;
|
|
48
|
-
onRemove: () => void;
|
|
72
|
+
href: string;
|
|
73
|
+
downloadName?: string;
|
|
49
74
|
};
|
|
50
75
|
|
|
51
|
-
type
|
|
76
|
+
type Props = {
|
|
52
77
|
imgPreviews: string[];
|
|
53
78
|
filePreviews: FilePreview[];
|
|
54
79
|
onRemoveImage: (index: number) => void;
|
|
55
80
|
onRemoveFile: (index: number) => void;
|
|
56
81
|
};
|
|
57
82
|
|
|
58
|
-
/* =======================
|
|
59
|
-
Sub Components
|
|
60
|
-
======================= */
|
|
61
|
-
|
|
62
|
-
export const FilePreviewChip: FC<FilePreviewChipProps> = ({ name, sizeMB, ext, onRemove }) => {
|
|
63
|
-
return (
|
|
64
|
-
<div
|
|
65
|
-
className={cn(
|
|
66
|
-
"mr-2 inline-flex h-[65px] max-w-[185px] items-center gap-3 whitespace-nowrap rounded-sm",
|
|
67
|
-
"border border-[#e1e1e1] bg-white px-3 py-2",
|
|
68
|
-
)}
|
|
69
|
-
>
|
|
70
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
71
|
-
<div className="min-w-0">
|
|
72
|
-
<div className="flex items-center gap-1">
|
|
73
|
-
<FileIcon className={cn("h-[18px] w-[18px]", extColor(ext))} />
|
|
74
|
-
<div className="truncate text-[13px] font-normal text-black">{name}</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div className="mt-2 flex items-center gap-2 text-xs text-[#636363]">
|
|
78
|
-
<span>{sizeMB.toFixed(1)} MB</span>
|
|
79
|
-
<span className="h-3 w-px bg-[#e1e1e1]" />
|
|
80
|
-
<span className="uppercase">{ext}</span>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
onClick={onRemove}
|
|
88
|
-
title="Remove"
|
|
89
|
-
aria-label="Remove file"
|
|
90
|
-
className={cn(
|
|
91
|
-
"grid h-8 w-8 place-items-center rounded-full bg-white text-[#3D3D3D]",
|
|
92
|
-
"shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5",
|
|
93
|
-
)}
|
|
94
|
-
>
|
|
95
|
-
<ChatXIcon className="h-[18px] w-[18px]" />
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const ImageThumb: FC<ImageThumbProps> = ({ url, onRemove }) => {
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
className={cn(
|
|
105
|
-
"relative mr-2 inline-block h-[65px] w-[65px] rounded-sm",
|
|
106
|
-
"border border-[#e1e1e1] bg-[#F7F7F7]",
|
|
107
|
-
)}
|
|
108
|
-
>
|
|
109
|
-
<img src={url} alt="" className="h-full w-full rounded-sm object-cover" loading="lazy" />
|
|
110
|
-
|
|
111
|
-
<button
|
|
112
|
-
type="button"
|
|
113
|
-
onClick={onRemove}
|
|
114
|
-
aria-label="Remove image"
|
|
115
|
-
title="Remove image"
|
|
116
|
-
className={cn(
|
|
117
|
-
"absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center",
|
|
118
|
-
"rounded-full bg-black/30 text-white",
|
|
119
|
-
"shadow-[0px_2px_4px_0px_#A5A3AE4D]",
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
<ChatXIcon className="h-4 w-4 text-white" />
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/* =======================
|
|
129
|
-
Component
|
|
130
|
-
======================= */
|
|
131
|
-
|
|
132
83
|
/** Single-row, horizontally scrollable preview strip */
|
|
133
|
-
const AttachmentPreviewStrip: FC<
|
|
84
|
+
const AttachmentPreviewStrip: React.FC<Props> = ({
|
|
134
85
|
imgPreviews,
|
|
135
86
|
filePreviews,
|
|
136
87
|
onRemoveFile,
|
|
137
88
|
onRemoveImage,
|
|
138
89
|
}) => {
|
|
139
90
|
const has = imgPreviews.length > 0 || filePreviews.length > 0;
|
|
140
|
-
|
|
141
91
|
if (!has) {
|
|
142
92
|
return null;
|
|
143
93
|
}
|
|
144
94
|
|
|
145
95
|
return (
|
|
146
|
-
<div className=
|
|
96
|
+
<div className="mb-2 max-w-[410px] mx-auto overflow-x-auto custom-scroll">
|
|
147
97
|
<div className="flex items-start whitespace-nowrap">
|
|
148
|
-
{filePreviews.map((
|
|
98
|
+
{filePreviews.map((f, i) => (
|
|
149
99
|
<FilePreviewChip
|
|
150
|
-
key={`${
|
|
151
|
-
name={
|
|
152
|
-
sizeMB={
|
|
153
|
-
ext={
|
|
154
|
-
onRemove={() => onRemoveFile(
|
|
100
|
+
key={`${f.name}-${i}`}
|
|
101
|
+
name={f.name}
|
|
102
|
+
sizeMB={f.sizeMB}
|
|
103
|
+
ext={f.ext}
|
|
104
|
+
onRemove={() => onRemoveFile(i)}
|
|
155
105
|
/>
|
|
156
106
|
))}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
107
|
+
{imgPreviews.map((u, i) => (
|
|
108
|
+
<div key={`${u}-${i}`}>
|
|
109
|
+
<ImageThumb url={u} onRemove={() => onRemoveImage(i)} />
|
|
110
|
+
</div>
|
|
160
111
|
))}
|
|
161
112
|
</div>
|
|
162
113
|
</div>
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
+
// components/chat/ui/chat/ChatComposerBar.tsx
|
|
1
2
|
"use client";
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
-
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import React, { useRef, useState } from "react";
|
|
5
6
|
import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../../icons";
|
|
6
|
-
import { cn } from "../../utils/cn";
|
|
7
|
-
|
|
8
|
-
/* =======================
|
|
9
|
-
Types
|
|
10
|
-
======================= */
|
|
11
7
|
|
|
12
|
-
type
|
|
8
|
+
type Props = {
|
|
13
9
|
recording: boolean;
|
|
14
10
|
seconds: number;
|
|
15
11
|
|
|
@@ -34,21 +30,14 @@ type ChatComposerBarProps = {
|
|
|
34
30
|
fmtTime: (s: number) => string;
|
|
35
31
|
};
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
Gradients
|
|
39
|
-
======================= */
|
|
40
|
-
|
|
33
|
+
// gradients
|
|
41
34
|
const idleGradient =
|
|
42
35
|
"linear-gradient(90.85deg, rgba(51, 201, 212, 0.5) 0%, rgba(39, 83, 251, 0.5) 29.98%, rgba(39, 83, 251, 0.5) 49.97%, rgba(39, 83, 251, 0.5) 64.96%, rgba(235, 67, 255, 0.5) 99.94%)";
|
|
43
36
|
|
|
44
37
|
const activeGradient =
|
|
45
38
|
"linear-gradient(90.85deg, #33C9D4 0%, #2753FB 29.98%, #2753FB 49.97%, #2753FB 64.96%, #EB43FF 99.94%)";
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
Component
|
|
49
|
-
======================= */
|
|
50
|
-
|
|
51
|
-
const ChatComposerBar = ({
|
|
40
|
+
const ChatComposerBar: React.FC<Props> = ({
|
|
52
41
|
recording,
|
|
53
42
|
seconds,
|
|
54
43
|
isTyping,
|
|
@@ -63,10 +52,11 @@ const ChatComposerBar = ({
|
|
|
63
52
|
sendText,
|
|
64
53
|
sendAttachments,
|
|
65
54
|
fmtTime,
|
|
66
|
-
}
|
|
55
|
+
}) => {
|
|
67
56
|
const composingRef = useRef(false);
|
|
68
57
|
const [isFocused, setIsFocused] = useState(false);
|
|
69
58
|
|
|
59
|
+
// active border when focused or recording
|
|
70
60
|
const isActiveBorder = isFocused || recording;
|
|
71
61
|
|
|
72
62
|
if (!recording) {
|
|
@@ -84,7 +74,7 @@ const ChatComposerBar = ({
|
|
|
84
74
|
<button
|
|
85
75
|
type="button"
|
|
86
76
|
onClick={startRecording}
|
|
87
|
-
className="grid h-[
|
|
77
|
+
className="grid h-[44px] w-[44px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
88
78
|
title="Record voice"
|
|
89
79
|
aria-label="Record voice"
|
|
90
80
|
>
|
|
@@ -95,9 +85,9 @@ const ChatComposerBar = ({
|
|
|
95
85
|
<textarea
|
|
96
86
|
ref={textRef}
|
|
97
87
|
rows={1}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
className="
|
|
88
|
+
placeholder="Type a message…"
|
|
89
|
+
autoFocus={true}
|
|
90
|
+
className="max-h-[200px] flex-1 resize-none bg-transparent px-3 py-2 outline-none custom-scroll-hidden"
|
|
101
91
|
value={text}
|
|
102
92
|
onChange={(e) => onTextChange(e.target.value)}
|
|
103
93
|
onInput={onAutoGrow}
|
|
@@ -127,9 +117,7 @@ const ChatComposerBar = ({
|
|
|
127
117
|
/>
|
|
128
118
|
</div>
|
|
129
119
|
|
|
130
|
-
{!canSendArrow
|
|
131
|
-
<div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />
|
|
132
|
-
) : null}
|
|
120
|
+
{!canSendArrow && <div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />}
|
|
133
121
|
|
|
134
122
|
<div className="px-2">
|
|
135
123
|
{isTyping ? (
|
|
@@ -138,9 +126,8 @@ const ChatComposerBar = ({
|
|
|
138
126
|
<button
|
|
139
127
|
type="button"
|
|
140
128
|
onClick={sendText}
|
|
141
|
-
className={
|
|
142
|
-
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full",
|
|
143
|
-
"text-[#ff5301] hover:bg-[#f8f8f8]",
|
|
129
|
+
className={clsx(
|
|
130
|
+
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full text-[#ff5301] hover:bg-[#f8f8f8]",
|
|
144
131
|
)}
|
|
145
132
|
title={hasAttachments ? "Send attachments" : "Send"}
|
|
146
133
|
aria-label="Send"
|
|
@@ -155,7 +142,7 @@ const ChatComposerBar = ({
|
|
|
155
142
|
type="button"
|
|
156
143
|
onClick={sendAttachments}
|
|
157
144
|
disabled={!hasAttachments}
|
|
158
|
-
className={
|
|
145
|
+
className={clsx(
|
|
159
146
|
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full hover:bg-[#f8f8f8]",
|
|
160
147
|
hasAttachments ? "text-[#ff5301]" : "text-[#B9C3D4]",
|
|
161
148
|
)}
|
|
@@ -173,22 +160,19 @@ const ChatComposerBar = ({
|
|
|
173
160
|
);
|
|
174
161
|
}
|
|
175
162
|
|
|
176
|
-
|
|
177
|
-
Recording state UI
|
|
178
|
-
======================= */
|
|
179
|
-
|
|
163
|
+
// Recording state UI
|
|
180
164
|
return (
|
|
181
165
|
<div className="flex w-full items-stretch gap-2">
|
|
182
166
|
<div className="w-full rounded-sm p-px" style={{ background: activeGradient }}>
|
|
183
|
-
<div className="flex h-[
|
|
167
|
+
<div className="flex min-h-[50px] w-full items-center justify-between rounded-[3px] bg-white">
|
|
184
168
|
<button
|
|
185
169
|
type="button"
|
|
186
|
-
className="ms-[3px]
|
|
170
|
+
className="grid ms-[3px] h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
187
171
|
aria-label="Recording"
|
|
188
172
|
title="Recording"
|
|
189
173
|
>
|
|
190
174
|
<RecordMicIcon
|
|
191
|
-
className={
|
|
175
|
+
className={clsx(
|
|
192
176
|
"h-6 w-6",
|
|
193
177
|
seconds % 2 === 0
|
|
194
178
|
? "text-[#929292]"
|
|
@@ -197,7 +181,7 @@ const ChatComposerBar = ({
|
|
|
197
181
|
/>
|
|
198
182
|
</button>
|
|
199
183
|
|
|
200
|
-
<div className="px-3 text-
|
|
184
|
+
<div className="px-3 text-sm">{fmtTime(seconds)}</div>
|
|
201
185
|
|
|
202
186
|
<div className="ml-auto flex items-center gap-3 pr-2">
|
|
203
187
|
<button
|
|
@@ -216,7 +200,7 @@ const ChatComposerBar = ({
|
|
|
216
200
|
type="button"
|
|
217
201
|
onClick={() => stopRecording(true)}
|
|
218
202
|
className="grid h-10 w-[40px] place-items-center rounded-full text-[#ff5301]"
|
|
219
|
-
title=
|
|
203
|
+
title="Send"
|
|
220
204
|
aria-label="Send"
|
|
221
205
|
>
|
|
222
206
|
<ArrowSendAngleIcon className="h-6 w-6" />
|
|
@@ -11,7 +11,7 @@ import ChatComposerBar from "./ChatComposerBar";
|
|
|
11
11
|
import BusinessCardDropup from "./drop-up/BusinessCardDropup";
|
|
12
12
|
import EmojiDropup from "./drop-up/EmojiDropup";
|
|
13
13
|
import ReplyCard from "./ReplyCard";
|
|
14
|
-
import type { MessageRef } from "
|
|
14
|
+
import type { MessageRef } from "./types";
|
|
15
15
|
import type { SendPayload } from "../../types";
|
|
16
16
|
|
|
17
17
|
/* Simple tooltip wrapper */
|