@droppii-org/chat-sdk 0.0.2 → 0.0.4
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/components/AutoScrollAnchor.d.ts +1 -1
- package/dist/components/AutoScrollAnchor.d.ts.map +1 -1
- package/dist/components/AutoScrollAnchor.js +12 -0
- package/dist/components/ChatBubble.d.ts +1 -1
- package/dist/components/ChatBubble.d.ts.map +1 -1
- package/dist/components/ChatBubble.js +18 -0
- package/dist/components/ChatHeader.d.ts +1 -1
- package/dist/components/ChatHeader.d.ts.map +1 -1
- package/dist/components/ChatHeader.js +32 -0
- package/dist/components/ChatInput.d.ts +1 -2
- package/dist/components/ChatInput.d.ts.map +1 -1
- package/dist/components/ChatInput.js +379 -0
- package/dist/components/ChatInputDemo.d.ts +1 -1
- package/dist/components/ChatInputDemo.d.ts.map +1 -1
- package/dist/components/ChatInputDemo.js +38 -0
- package/dist/components/ChatInputWithCustomIcon.d.ts +1 -2
- package/dist/components/ChatInputWithCustomIcon.d.ts.map +1 -1
- package/dist/components/ChatInputWithCustomIcon.js +85 -0
- package/dist/components/ChatLayout.d.ts +1 -1
- package/dist/components/ChatLayout.d.ts.map +1 -1
- package/dist/components/ChatLayout.js +48 -0
- package/dist/components/ConversationItem.d.ts +1 -1
- package/dist/components/ConversationItem.d.ts.map +1 -1
- package/dist/components/ConversationItem.js +27 -0
- package/dist/components/ConversationList.d.ts +1 -1
- package/dist/components/ConversationList.d.ts.map +1 -1
- package/dist/components/ConversationList.js +11 -0
- package/dist/components/DateDivider.d.ts +1 -1
- package/dist/components/DateDivider.d.ts.map +1 -1
- package/dist/components/DateDivider.js +27 -0
- package/dist/components/EmojiPicker.js +191 -0
- package/dist/components/ImageLightbox.d.ts +1 -1
- package/dist/components/ImageLightbox.d.ts.map +1 -1
- package/dist/components/ImageLightbox.js +8 -0
- package/dist/components/ImagePreviewModal.d.ts +1 -1
- package/dist/components/ImagePreviewModal.d.ts.map +1 -1
- package/dist/components/ImagePreviewModal.js +55 -0
- package/dist/components/MessageItem.d.ts +1 -1
- package/dist/components/MessageItem.d.ts.map +1 -1
- package/dist/components/MessageItem.js +38 -0
- package/dist/components/MessageItemDemo.d.ts +1 -1
- package/dist/components/MessageItemDemo.d.ts.map +1 -1
- package/dist/components/MessageItemDemo.js +166 -0
- package/dist/components/MessageList.d.ts +1 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +243 -0
- package/dist/components/MessageListDemo.d.ts +1 -1
- package/dist/components/MessageListDemo.d.ts.map +1 -1
- package/dist/components/MessageListDemo.js +165 -0
- package/dist/components/StickerPicker.js +68 -0
- package/dist/components/SwipeIndicator.d.ts +1 -1
- package/dist/components/SwipeIndicator.d.ts.map +1 -1
- package/dist/components/SwipeIndicator.js +24 -0
- package/dist/components/TextFormattingToolbar.js +29 -0
- package/dist/components/TypingIndicator.d.ts +1 -1
- package/dist/components/TypingIndicator.d.ts.map +1 -1
- package/dist/components/TypingIndicator.js +21 -0
- package/dist/components/VoiceWaveIcon.d.ts +1 -1
- package/dist/components/VoiceWaveIcon.d.ts.map +1 -1
- package/dist/components/VoiceWaveIcon.js +5 -0
- package/dist/context/ChatContext.d.ts +1 -1
- package/dist/context/ChatContext.d.ts.map +1 -1
- package/dist/context/ChatContext.js +347 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useChatContext } from "../context/ChatContext";
|
|
4
|
+
import { FileText, Download } from "lucide-react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
export function MessageItem({ message, isGrouped, onImageClick }) {
|
|
7
|
+
const { state } = useChatContext();
|
|
8
|
+
const isOwnMessage = message.isMine;
|
|
9
|
+
const sender = state.users[message.senderId];
|
|
10
|
+
const formatFileSize = (bytes) => {
|
|
11
|
+
if (bytes === 0)
|
|
12
|
+
return "0 Bytes";
|
|
13
|
+
const k = 1024;
|
|
14
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
15
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
16
|
+
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
17
|
+
};
|
|
18
|
+
const renderTextMessage = (msg) => {
|
|
19
|
+
const content = msg.text || "";
|
|
20
|
+
// Simple regex to detect URLs and make them clickable
|
|
21
|
+
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
|
22
|
+
const parts = content.split(linkRegex);
|
|
23
|
+
return (_jsx("div", { className: `px-3 py-2 sm:px-4 sm:py-2 rounded-2xl max-w-full break-words ${isOwnMessage ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-900"} ${isGrouped ? (isOwnMessage ? "rounded-tr-md" : "rounded-tl-md") : ""}`, children: _jsx("p", { className: "text-sm sm:text-base whitespace-pre-wrap", children: parts.map((part, index) => linkRegex.test(part) ? (_jsx("a", { href: part, target: "_blank", rel: "noopener noreferrer", className: "underline text-blue-200 hover:text-blue-100", children: part }, index)) : (part)) }) }));
|
|
24
|
+
};
|
|
25
|
+
const renderMediaMessage = (msg) => {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
const imageAttachments = ((_a = msg.attachments) === null || _a === void 0 ? void 0 : _a.filter((att) => att.type === "image")) || [];
|
|
28
|
+
return (_jsxs("div", { className: `flex flex-col gap-2 ${isOwnMessage ? "items-end" : "items-start"}`, children: [msg.text && (_jsx("div", { className: `px-3 py-2 sm:px-4 sm:py-2 rounded-2xl max-w-full break-words ${isOwnMessage ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-900"} ${isGrouped ? (isOwnMessage ? "rounded-tr-md" : "rounded-tl-md") : ""}`, children: _jsx("p", { className: "text-sm sm:text-base whitespace-pre-wrap", children: msg.text }) })), _jsx("div", { className: `grid gap-2 ${imageAttachments.length > 1 ? "grid-cols-2" : "grid-cols-1"}`, children: (_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.map((attachment, idx) => (_jsx("div", { className: `relative rounded-lg overflow-hidden ${isOwnMessage ? "bg-blue-100" : "bg-gray-100"}`, children: attachment.type === "image" ? (_jsxs(_Fragment, { children: [_jsx(Image, { src: attachment.url || "/placeholder.svg", alt: attachment.name || "Attached image", width: 200, height: 150, className: "w-full h-auto object-cover cursor-pointer", onClick: () => onImageClick === null || onImageClick === void 0 ? void 0 : onImageClick(attachment.id, imageAttachments.map((img) => ({ id: img.id, url: img.url, name: img.name }))) }), _jsx("div", { className: "absolute inset-0 bg-black/10 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity", children: _jsx("span", { className: "text-white text-xs font-medium p-1 rounded bg-black/50", children: "Xem \u1EA3nh" }) })] })) : (_jsxs("a", { href: attachment.url, download: attachment.name, className: "flex items-center p-3 space-x-2 text-gray-800 hover:bg-gray-200 transition-colors", children: [_jsx(FileText, { className: "w-5 h-5 flex-shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block text-sm font-medium truncate", children: attachment.name || "File" }), _jsx("span", { className: "block text-xs text-gray-500", children: attachment.size ? formatFileSize(attachment.size) : "Unknown size" })] }), _jsx(Download, { className: "w-4 h-4 flex-shrink-0 text-gray-600" })] })) }, idx))) })] }));
|
|
29
|
+
};
|
|
30
|
+
const renderPromoMessage = (msg) => {
|
|
31
|
+
if (!msg.promoData)
|
|
32
|
+
return null;
|
|
33
|
+
const { imageUrl, title, description, buttonText, buttonUrl } = msg.promoData;
|
|
34
|
+
return (_jsxs("div", { className: "w-full max-w-xs mx-auto my-2 bg-white shadow-md rounded-lg overflow-hidden border border-gray-200", children: [_jsx("div", { className: "relative w-full h-32 bg-gray-200", children: _jsx(Image, { src: imageUrl || "/placeholder.svg", alt: title, fill: true, style: { objectFit: "cover" }, className: "rounded-t-lg" }) }), _jsxs("div", { className: "p-4", children: [_jsx("h3", { className: "text-base font-semibold text-gray-900 mb-1", children: title }), _jsx("p", { className: "text-sm text-gray-600 mb-3", children: description }), _jsx("a", { href: buttonUrl, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center justify-center w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors", children: buttonText })] })] }));
|
|
35
|
+
};
|
|
36
|
+
return (_jsx("div", { className: `flex ${isOwnMessage ? "justify-end" : "justify-start"} ${isGrouped ? "mt-0.5 sm:mt-1" : "mt-3 sm:mt-4"}`, children: _jsxs("div", { className: `flex items-end space-x-2 max-w-[85%] sm:max-w-xs lg:max-w-md ${isOwnMessage ? "flex-row-reverse space-x-reverse" : ""}`, style: message.type === "promo" ? { maxWidth: "none" } : {}, children: [message.type !== "promo" && !isOwnMessage && !isGrouped && (_jsx("img", { src: (sender === null || sender === void 0 ? void 0 : sender.avatar) || "/placeholder.svg?height=32&width=32&query=user", alt: (sender === null || sender === void 0 ? void 0 : sender.name) || "User", className: "w-6 h-6 sm:w-8 sm:h-8 rounded-full object-cover flex-shrink-0" })), message.type !== "promo" && !isOwnMessage && isGrouped && (_jsx("div", { className: "w-6 sm:w-8 flex-shrink-0" }) // Spacer for grouped messages
|
|
37
|
+
), _jsxs("div", { className: `flex flex-col ${isOwnMessage ? "items-end" : "items-start"}`, children: [message.type !== "promo" && !isGrouped && !isOwnMessage && (_jsx("span", { className: "text-xs text-gray-500 mb-1 px-3", children: (sender === null || sender === void 0 ? void 0 : sender.name) || "Unknown User" })), message.type === "text" && renderTextMessage(message), message.type === "media" && renderMediaMessage(message), message.type === "promo" && renderPromoMessage(message), message.type !== "promo" && (_jsx("div", { className: `flex items-center space-x-1 mt-1 px-2 ${isOwnMessage ? "flex-row-reverse space-x-reverse" : ""}`, children: isOwnMessage && (_jsx("svg", { className: "w-3 h-3 sm:w-4 sm:h-4 text-blue-500", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }) })) }))] })] }) }));
|
|
38
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function MessageItemDemo(): import("react").JSX.Element;
|
|
1
|
+
export declare function MessageItemDemo(): import("react/jsx-runtime").JSX.Element;
|
|
2
2
|
//# sourceMappingURL=MessageItemDemo.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItemDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageItemDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,
|
|
1
|
+
{"version":3,"file":"MessageItemDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageItemDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,4CA2L9B"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { MessageItem } from "./MessageItem";
|
|
4
|
+
import { DateDivider } from "./DateDivider";
|
|
5
|
+
export function MessageItemDemo() {
|
|
6
|
+
// Demo messages showcasing all message types
|
|
7
|
+
const demoMessages = [
|
|
8
|
+
// Text message from other user
|
|
9
|
+
{
|
|
10
|
+
id: "demo-1",
|
|
11
|
+
senderId: "user-2",
|
|
12
|
+
type: "text",
|
|
13
|
+
text: "Hey! How are you doing today? 😊",
|
|
14
|
+
createdAt: new Date(Date.now() - 3600000).toISOString(),
|
|
15
|
+
isMine: false,
|
|
16
|
+
},
|
|
17
|
+
// Text message from current user
|
|
18
|
+
{
|
|
19
|
+
id: "demo-2",
|
|
20
|
+
senderId: "current-user",
|
|
21
|
+
type: "text",
|
|
22
|
+
text: "I'm doing great! Thanks for asking. Check out this link: https://example.com",
|
|
23
|
+
createdAt: new Date(Date.now() - 3000000).toISOString(),
|
|
24
|
+
isMine: true,
|
|
25
|
+
},
|
|
26
|
+
// Media message with text and image from other user
|
|
27
|
+
{
|
|
28
|
+
id: "demo-3",
|
|
29
|
+
senderId: "user-2",
|
|
30
|
+
type: "media",
|
|
31
|
+
text: "Look at this beautiful sunset I captured! 📸",
|
|
32
|
+
attachments: [
|
|
33
|
+
{
|
|
34
|
+
id: "demo-3-1",
|
|
35
|
+
type: "image",
|
|
36
|
+
url: "/placeholder.svg?height=300&width=400",
|
|
37
|
+
name: "sunset.jpg",
|
|
38
|
+
size: 245760,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
createdAt: new Date(Date.now() - 2400000).toISOString(),
|
|
42
|
+
isMine: false,
|
|
43
|
+
},
|
|
44
|
+
// Media message with multiple images from current user
|
|
45
|
+
{
|
|
46
|
+
id: "demo-4",
|
|
47
|
+
senderId: "current-user",
|
|
48
|
+
type: "media",
|
|
49
|
+
text: "My vacation photos! 🏖️",
|
|
50
|
+
attachments: [
|
|
51
|
+
{
|
|
52
|
+
id: "demo-4-1",
|
|
53
|
+
type: "image",
|
|
54
|
+
url: "/placeholder.svg?height=200&width=300",
|
|
55
|
+
name: "beach1.jpg",
|
|
56
|
+
size: 180000,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "demo-4-2",
|
|
60
|
+
type: "image",
|
|
61
|
+
url: "/placeholder.svg?height=200&width=300",
|
|
62
|
+
name: "beach2.jpg",
|
|
63
|
+
size: 195000,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
createdAt: new Date(Date.now() - 1800000).toISOString(),
|
|
67
|
+
isMine: true,
|
|
68
|
+
},
|
|
69
|
+
// Media message with file from other user
|
|
70
|
+
{
|
|
71
|
+
id: "demo-5",
|
|
72
|
+
senderId: "user-2",
|
|
73
|
+
type: "media",
|
|
74
|
+
text: "Here's the document you requested",
|
|
75
|
+
attachments: [
|
|
76
|
+
{
|
|
77
|
+
id: "demo-5-1",
|
|
78
|
+
type: "file",
|
|
79
|
+
url: "/placeholder.svg?height=200&width=200",
|
|
80
|
+
name: "project-proposal.pdf",
|
|
81
|
+
size: 1024000,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
createdAt: new Date(Date.now() - 1200000).toISOString(),
|
|
85
|
+
isMine: false,
|
|
86
|
+
},
|
|
87
|
+
// Media message with mixed attachments from current user
|
|
88
|
+
{
|
|
89
|
+
id: "demo-6",
|
|
90
|
+
senderId: "current-user",
|
|
91
|
+
type: "media",
|
|
92
|
+
text: "Screenshot and the related document",
|
|
93
|
+
attachments: [
|
|
94
|
+
{
|
|
95
|
+
id: "demo-6-1",
|
|
96
|
+
type: "image",
|
|
97
|
+
url: "/placeholder.svg?height=400&width=300",
|
|
98
|
+
name: "screenshot.png",
|
|
99
|
+
size: 156000,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "demo-6-2",
|
|
103
|
+
type: "file",
|
|
104
|
+
url: "/placeholder.svg?height=200&width=200",
|
|
105
|
+
name: "instructions.docx",
|
|
106
|
+
size: 45000,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
createdAt: new Date(Date.now() - 600000).toISOString(),
|
|
110
|
+
isMine: true,
|
|
111
|
+
},
|
|
112
|
+
// Promotional message (system message)
|
|
113
|
+
{
|
|
114
|
+
id: "demo-7",
|
|
115
|
+
senderId: "system",
|
|
116
|
+
type: "promo",
|
|
117
|
+
promoData: {
|
|
118
|
+
imageUrl: "/placeholder.svg?height=200&width=400",
|
|
119
|
+
title: "Giảm 30% đơn hàng hôm nay!",
|
|
120
|
+
description: "Khuyến mãi đặc biệt chỉ trong hôm nay. Đừng bỏ lỡ cơ hội tiết kiệm!",
|
|
121
|
+
buttonText: "Đặt ngay",
|
|
122
|
+
buttonUrl: "https://example.com/sale",
|
|
123
|
+
},
|
|
124
|
+
createdAt: new Date(Date.now() - 300000).toISOString(),
|
|
125
|
+
isMine: false,
|
|
126
|
+
},
|
|
127
|
+
// Recent text message from other user
|
|
128
|
+
{
|
|
129
|
+
id: "demo-8",
|
|
130
|
+
senderId: "user-2",
|
|
131
|
+
type: "text",
|
|
132
|
+
text: "Thanks for sharing! Those photos are amazing 🤩",
|
|
133
|
+
createdAt: new Date(Date.now() - 60000).toISOString(),
|
|
134
|
+
isMine: false,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
const groupMessagesByDate = () => {
|
|
138
|
+
const groups = [];
|
|
139
|
+
let currentDate = "";
|
|
140
|
+
let currentGroup = [];
|
|
141
|
+
demoMessages.forEach((message) => {
|
|
142
|
+
const messageDate = new Date(message.createdAt).toDateString();
|
|
143
|
+
if (messageDate !== currentDate) {
|
|
144
|
+
if (currentGroup.length > 0) {
|
|
145
|
+
groups.push({ date: currentDate, messages: currentGroup });
|
|
146
|
+
}
|
|
147
|
+
currentDate = messageDate;
|
|
148
|
+
currentGroup = [message];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
currentGroup.push(message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (currentGroup.length > 0) {
|
|
155
|
+
groups.push({ date: currentDate, messages: currentGroup });
|
|
156
|
+
}
|
|
157
|
+
return groups;
|
|
158
|
+
};
|
|
159
|
+
const messageGroups = groupMessagesByDate();
|
|
160
|
+
return (_jsxs("div", { className: "max-w-2xl mx-auto bg-white rounded-lg shadow-lg overflow-hidden", children: [_jsxs("div", { className: "bg-gray-50 p-4 border-b", children: [_jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "MessageItem Demo" }), _jsx("p", { className: "text-sm text-gray-600", children: "Showcasing all message types: text, media, and promotional" })] }), _jsx("div", { className: "h-96 overflow-y-auto p-4 space-y-4", children: messageGroups.map((group, groupIndex) => (_jsxs("div", { children: [_jsx(DateDivider, { date: new Date(group.date) }), _jsx("div", { className: "space-y-2", children: group.messages.map((message, messageIndex) => {
|
|
161
|
+
const prevMessage = messageIndex > 0 ? group.messages[messageIndex - 1] : null;
|
|
162
|
+
const isGrouped = (prevMessage === null || prevMessage === void 0 ? void 0 : prevMessage.senderId) === message.senderId &&
|
|
163
|
+
new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() < 300000; // 5 minutes
|
|
164
|
+
return _jsx(MessageItem, { message: message, isGrouped: isGrouped }, message.id);
|
|
165
|
+
}) })] }, group.date))) })] }));
|
|
166
|
+
}
|
|
@@ -10,6 +10,6 @@ interface MessageListProps {
|
|
|
10
10
|
onSwipeBack?: () => void;
|
|
11
11
|
}
|
|
12
12
|
export declare function MessageList({ messages, // Add default empty array
|
|
13
|
-
isLoadingMore, onLoadMore, hasMore, currentUserId, conversationId, className, onSwipeBack, }: MessageListProps): import("react").JSX.Element;
|
|
13
|
+
isLoadingMore, onLoadMore, hasMore, currentUserId, conversationId, className, onSwipeBack, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
14
14
|
export {};
|
|
15
15
|
//# sourceMappingURL=MessageList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/components/MessageList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,UAAU,CAAA;AAGvD,UAAU,gBAAgB;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,wBAAgB,WAAW,CAAC,EAC1B,QAAa,EAAE,0BAA0B;AACzC,aAAqB,EACrB,UAAU,EACV,OAAe,EACf,aAAa,EACb,cAAc,EACd,SAAc,EACd,WAAW,GACZ,EAAE,gBAAgB
|
|
1
|
+
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/components/MessageList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,UAAU,CAAA;AAGvD,UAAU,gBAAgB;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,wBAAgB,WAAW,CAAC,EAC1B,QAAa,EAAE,0BAA0B;AACzC,aAAqB,EACrB,UAAU,EACV,OAAe,EACf,aAAa,EACb,cAAc,EACd,SAAc,EACd,WAAW,GACZ,EAAE,gBAAgB,2CA8WlB"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
4
|
+
import { MessageItem } from "./MessageItem";
|
|
5
|
+
import { DateDivider } from "./DateDivider";
|
|
6
|
+
import { TypingIndicator } from "./TypingIndicator";
|
|
7
|
+
// import { AutoScrollAnchor } from "./AutoScrollAnchor" // Removed
|
|
8
|
+
import { useSwipeGesture } from "../hooks/useSwipeGesture";
|
|
9
|
+
import { useChatContext } from "../context/ChatContext";
|
|
10
|
+
import { ImagePreviewModal } from "./ImagePreviewModal"; // Import the new modal
|
|
11
|
+
export function MessageList({ messages = [], // Add default empty array
|
|
12
|
+
isLoadingMore = false, onLoadMore, hasMore = false, currentUserId, conversationId, className = "", onSwipeBack, }) {
|
|
13
|
+
const { state } = useChatContext();
|
|
14
|
+
const scrollRef = useRef(null);
|
|
15
|
+
const shouldScrollToBottomRef = useRef(true); // New ref to control auto-scrolling
|
|
16
|
+
const lastMessageCountRef = useRef((messages === null || messages === void 0 ? void 0 : messages.length) || 0); // Add null check
|
|
17
|
+
const [showSwipeHint, setShowSwipeHint] = useState(false);
|
|
18
|
+
const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false); // State for button visibility
|
|
19
|
+
// State for ImagePreviewModal
|
|
20
|
+
const [isImagePreviewModalOpen, setIsImagePreviewModalOpen] = useState(false);
|
|
21
|
+
const [previewImages, setPreviewImages] = useState([]);
|
|
22
|
+
const [initialPreviewImageId, setInitialPreviewImageId] = useState("");
|
|
23
|
+
// Convert internal Message format to DisplayMessage format
|
|
24
|
+
const convertToDisplayMessages = useCallback(() => {
|
|
25
|
+
if (!messages || !Array.isArray(messages))
|
|
26
|
+
return []; // Add safety check
|
|
27
|
+
return messages.map((msg) => {
|
|
28
|
+
const isCurrentUser = msg.senderId === currentUserId;
|
|
29
|
+
// Handle different message types
|
|
30
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
id: msg.id,
|
|
33
|
+
senderId: msg.senderId,
|
|
34
|
+
type: "media",
|
|
35
|
+
text: msg.content || undefined,
|
|
36
|
+
attachments: msg.attachments.map((att) => ({
|
|
37
|
+
id: att.id, // Ensure ID is passed
|
|
38
|
+
type: att.type.startsWith("image/") ? "image" : "file",
|
|
39
|
+
url: att.url,
|
|
40
|
+
name: att.name,
|
|
41
|
+
size: att.size,
|
|
42
|
+
})),
|
|
43
|
+
createdAt: msg.timestamp.toISOString(),
|
|
44
|
+
isMine: isCurrentUser,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Promotional message type (assuming senderId 'system' or similar for demo)
|
|
48
|
+
if (msg.type === "promo" && msg.promoData) {
|
|
49
|
+
return {
|
|
50
|
+
id: msg.id,
|
|
51
|
+
senderId: msg.senderId,
|
|
52
|
+
type: "promo",
|
|
53
|
+
promoData: msg.promoData,
|
|
54
|
+
createdAt: msg.timestamp.toISOString(),
|
|
55
|
+
isMine: isCurrentUser, // Can be false for system messages
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Regular text message
|
|
59
|
+
return {
|
|
60
|
+
id: msg.id,
|
|
61
|
+
senderId: msg.senderId,
|
|
62
|
+
type: "text",
|
|
63
|
+
text: msg.content,
|
|
64
|
+
createdAt: msg.timestamp.toISOString(),
|
|
65
|
+
isMine: isCurrentUser,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}, [messages, currentUserId]);
|
|
69
|
+
const displayMessages = convertToDisplayMessages();
|
|
70
|
+
// Swipe gesture for going back (secondary swipe area)
|
|
71
|
+
const messageSwipeRef = useSwipeGesture({
|
|
72
|
+
onSwipeRight: () => {
|
|
73
|
+
if (window.innerWidth < 768) {
|
|
74
|
+
setShowSwipeHint(true);
|
|
75
|
+
setTimeout(() => setShowSwipeHint(false), 1500);
|
|
76
|
+
onSwipeBack === null || onSwipeBack === void 0 ? void 0 : onSwipeBack();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
threshold: 80,
|
|
80
|
+
restraint: 120,
|
|
81
|
+
allowedTime: 400,
|
|
82
|
+
enabled: true,
|
|
83
|
+
});
|
|
84
|
+
// Auto-scroll to bottom logic
|
|
85
|
+
const scrollToBottom = useCallback((force = false) => {
|
|
86
|
+
if (scrollRef.current) {
|
|
87
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
88
|
+
if (force) {
|
|
89
|
+
shouldScrollToBottomRef.current = true; // If forced, ensure auto-scroll is re-enabled
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
// Handle scroll events to manage auto-scroll behavior and button visibility
|
|
94
|
+
const handleScroll = useCallback(() => {
|
|
95
|
+
if (!scrollRef.current)
|
|
96
|
+
return;
|
|
97
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
98
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
99
|
+
const SCROLL_UP_THRESHOLD = 200; // If user scrolls up more than 200px from bottom, disable auto-scroll
|
|
100
|
+
const SCROLL_DOWN_THRESHOLD = 5; // If user scrolls within 5px of bottom, re-enable auto-scroll
|
|
101
|
+
if (distanceFromBottom > SCROLL_UP_THRESHOLD) {
|
|
102
|
+
shouldScrollToBottomRef.current = false;
|
|
103
|
+
}
|
|
104
|
+
else if (distanceFromBottom <= SCROLL_DOWN_THRESHOLD) {
|
|
105
|
+
shouldScrollToBottomRef.current = true;
|
|
106
|
+
}
|
|
107
|
+
// Show button if not at bottom AND auto-scroll is disabled
|
|
108
|
+
setShowScrollToBottomButton(distanceFromBottom > SCROLL_DOWN_THRESHOLD && !shouldScrollToBottomRef.current);
|
|
109
|
+
}, []);
|
|
110
|
+
// Handle new messages
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const currentMessageCount = (messages === null || messages === void 0 ? void 0 : messages.length) || 0;
|
|
113
|
+
const previousMessageCount = lastMessageCountRef.current;
|
|
114
|
+
if (currentMessageCount > previousMessageCount) {
|
|
115
|
+
const newMessages = messages.slice(previousMessageCount);
|
|
116
|
+
const hasNewMessageFromCurrentUser = newMessages.some((msg) => msg.senderId === currentUserId);
|
|
117
|
+
// If current user sent a message, always scroll to bottom
|
|
118
|
+
// If another user sent a message, only scroll if shouldScrollToBottomRef is true (user is already at bottom)
|
|
119
|
+
if (hasNewMessageFromCurrentUser) {
|
|
120
|
+
setTimeout(() => scrollToBottom(true), 50); // Force scroll for own messages
|
|
121
|
+
}
|
|
122
|
+
else if (shouldScrollToBottomRef.current) {
|
|
123
|
+
setTimeout(() => scrollToBottom(), 50); // Scroll if auto-scroll is enabled for others' messages
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lastMessageCountRef.current = currentMessageCount;
|
|
127
|
+
}, [messages, currentUserId, scrollToBottom]); // scrollToBottom is a dependency because it's called here
|
|
128
|
+
// Attach and detach scroll listener
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const currentScrollRef = scrollRef.current;
|
|
131
|
+
if (currentScrollRef) {
|
|
132
|
+
currentScrollRef.addEventListener("scroll", handleScroll);
|
|
133
|
+
// Initial check for button visibility
|
|
134
|
+
handleScroll();
|
|
135
|
+
}
|
|
136
|
+
return () => {
|
|
137
|
+
if (currentScrollRef) {
|
|
138
|
+
currentScrollRef.removeEventListener("scroll", handleScroll);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}, [handleScroll]); // Re-attach if handleScroll changes (due to useCallback dependencies)
|
|
142
|
+
// Preserve scroll position when loading more messages
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (isLoadingMore)
|
|
145
|
+
return;
|
|
146
|
+
const scrollContainer = scrollRef.current;
|
|
147
|
+
if (!scrollContainer)
|
|
148
|
+
return;
|
|
149
|
+
// Save current scroll position
|
|
150
|
+
const previousScrollHeight = scrollContainer.scrollHeight;
|
|
151
|
+
const previousScrollTop = scrollContainer.scrollTop;
|
|
152
|
+
// After new messages are loaded, adjust scroll position
|
|
153
|
+
const adjustScrollPosition = () => {
|
|
154
|
+
const newScrollHeight = scrollContainer.scrollHeight;
|
|
155
|
+
const heightDifference = newScrollHeight - previousScrollHeight;
|
|
156
|
+
if (heightDifference > 0) {
|
|
157
|
+
scrollContainer.scrollTop = previousScrollTop + heightDifference;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// Use setTimeout to ensure DOM has updated
|
|
161
|
+
setTimeout(adjustScrollPosition, 0);
|
|
162
|
+
}, [isLoadingMore]);
|
|
163
|
+
// Show swipe hint on first load for mobile users
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const hasSeenHint = localStorage.getItem("chat-swipe-hint-seen");
|
|
166
|
+
if (!hasSeenHint && window.innerWidth < 768) {
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
setShowSwipeHint(true);
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
setShowSwipeHint(false);
|
|
171
|
+
localStorage.setItem("chat-swipe-hint-seen", "true");
|
|
172
|
+
}, 3000);
|
|
173
|
+
}, 1000);
|
|
174
|
+
}
|
|
175
|
+
}, []);
|
|
176
|
+
// Group messages by date
|
|
177
|
+
const groupMessagesByDate = useCallback(() => {
|
|
178
|
+
const groups = [];
|
|
179
|
+
let currentDate = "";
|
|
180
|
+
let currentGroup = [];
|
|
181
|
+
displayMessages.forEach((message) => {
|
|
182
|
+
const messageDate = new Date(message.createdAt).toDateString();
|
|
183
|
+
if (messageDate !== currentDate) {
|
|
184
|
+
if (currentGroup.length > 0) {
|
|
185
|
+
groups.push({ date: currentDate, messages: currentGroup });
|
|
186
|
+
}
|
|
187
|
+
currentDate = messageDate;
|
|
188
|
+
currentGroup = [message];
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
currentGroup.push(message);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (currentGroup.length > 0) {
|
|
195
|
+
groups.push({ date: currentDate, messages: currentGroup });
|
|
196
|
+
}
|
|
197
|
+
return groups;
|
|
198
|
+
}, [displayMessages]);
|
|
199
|
+
const messageGroups = groupMessagesByDate();
|
|
200
|
+
// Format date labels in Vietnamese
|
|
201
|
+
const formatDateLabel = (date) => {
|
|
202
|
+
const now = new Date();
|
|
203
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
204
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
205
|
+
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
206
|
+
if (messageDate.getTime() === today.getTime()) {
|
|
207
|
+
return "Hôm nay";
|
|
208
|
+
}
|
|
209
|
+
else if (messageDate.getTime() === yesterday.getTime()) {
|
|
210
|
+
return "Hôm qua";
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
return date.toLocaleDateString("vi-VN", {
|
|
214
|
+
weekday: "long",
|
|
215
|
+
year: "numeric",
|
|
216
|
+
month: "long",
|
|
217
|
+
day: "numeric",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
// Handler for image clicks from MessageItem
|
|
222
|
+
const handleImageClick = useCallback((imageId, images) => {
|
|
223
|
+
setPreviewImages(images);
|
|
224
|
+
setInitialPreviewImageId(imageId);
|
|
225
|
+
setIsImagePreviewModalOpen(true);
|
|
226
|
+
}, []);
|
|
227
|
+
if (!messages || messages.length === 0) {
|
|
228
|
+
return (_jsx("div", { className: `flex items-center justify-center h-full ${className}`, children: _jsxs("div", { className: "text-center text-gray-500", children: [_jsx("div", { className: "w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center", children: _jsx("svg", { className: "w-8 h-8 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" }) }) }), _jsx("p", { className: "text-sm", children: "Ch\u01B0a c\u00F3 tin nh\u1EAFn n\u00E0o" }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "H\u00E3y b\u1EAFt \u0111\u1EA7u cu\u1ED9c tr\u00F2 chuy\u1EC7n!" })] }) }));
|
|
229
|
+
}
|
|
230
|
+
return (_jsxs("div", { className: `relative h-full ${className}`, children: [showSwipeHint && (_jsx("div", { className: "absolute top-4 left-1/2 transform -translate-x-1/2 z-20 md:hidden", children: _jsxs("div", { className: "bg-black bg-opacity-80 text-white text-sm px-4 py-2 rounded-full flex items-center space-x-2 animate-pulse", children: [_jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), _jsx("span", { children: "Vu\u1ED1t ph\u1EA3i \u0111\u1EC3 quay l\u1EA1i" })] }) })), showScrollToBottomButton && (_jsx("div", { className: "absolute bottom-20 right-4 z-10", children: _jsx("button", { onClick: () => scrollToBottom(true), className: "bg-blue-500 text-white p-3 rounded-full shadow-lg hover:bg-blue-600 transition-colors", "aria-label": "Scroll to bottom", children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }) }) })), _jsxs("div", { ref: (el) => {
|
|
231
|
+
scrollRef.current = el;
|
|
232
|
+
if (messageSwipeRef.current !== el) {
|
|
233
|
+
messageSwipeRef.current = el;
|
|
234
|
+
}
|
|
235
|
+
}, className: "h-full overflow-y-auto p-3 sm:p-4", style: {
|
|
236
|
+
WebkitOverflowScrolling: "touch", // Smooth scrolling on iOS
|
|
237
|
+
}, onScroll: handleScroll, children: [isLoadingMore && (_jsx("div", { className: "flex justify-center py-4", children: _jsxs("div", { className: "flex items-center space-x-2 text-gray-500", children: [_jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500" }), _jsx("span", { className: "text-sm", children: "\u0110ang t\u1EA3i th\u00EAm tin nh\u1EAFn..." })] }) })), hasMore && !isLoadingMore && (_jsx("div", { className: "flex justify-center py-4", children: _jsx("button", { onClick: onLoadMore, className: "text-blue-500 hover:text-blue-600 text-sm font-medium", children: "T\u1EA3i th\u00EAm tin nh\u1EAFn" }) })), _jsx("div", { className: "space-y-3 sm:space-y-4", children: messageGroups.map((group, groupIndex) => (_jsxs("div", { children: [_jsx(DateDivider, { date: new Date(group.date), customLabel: formatDateLabel(new Date(group.date)) }), _jsx("div", { className: "space-y-1 sm:space-y-2", children: group.messages.map((message, messageIndex) => {
|
|
238
|
+
const prevMessage = messageIndex > 0 ? group.messages[messageIndex - 1] : null;
|
|
239
|
+
const isGrouped = (prevMessage === null || prevMessage === void 0 ? void 0 : prevMessage.senderId) === message.senderId &&
|
|
240
|
+
new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() < 300000; // 5 minutes
|
|
241
|
+
return (_jsx(MessageItem, { message: message, isGrouped: isGrouped, onImageClick: handleImageClick }, message.id));
|
|
242
|
+
}) })] }, group.date))) }), conversationId && _jsx(TypingIndicator, { conversationId: conversationId })] }), isImagePreviewModalOpen && (_jsx(ImagePreviewModal, { images: previewImages, initialImageId: initialPreviewImageId, onClose: () => setIsImagePreviewModalOpen(false) }))] }));
|
|
243
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function MessageListDemo(): import("react").JSX.Element;
|
|
1
|
+
export declare function MessageListDemo(): import("react/jsx-runtime").JSX.Element;
|
|
2
2
|
//# sourceMappingURL=MessageListDemo.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageListDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageListDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,
|
|
1
|
+
{"version":3,"file":"MessageListDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageListDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,4CAmN9B"}
|