@droppii-org/chat-sdk 0.0.4 → 0.0.6

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.
Files changed (153) hide show
  1. package/dist/assets/droppiiFontSelection.json +14521 -0
  2. package/dist/components/ChatBubble.d.ts +9 -1
  3. package/dist/components/ChatBubble.d.ts.map +1 -1
  4. package/dist/components/ChatBubble.js +23 -15
  5. package/dist/components/chat-bubble/ChatBubble.d.ts +9 -0
  6. package/dist/components/chat-bubble/ChatBubble.d.ts.map +1 -0
  7. package/dist/components/chat-bubble/ChatBubble.js +27 -0
  8. package/dist/components/conversation/DeskConversationList.d.ts +8 -0
  9. package/dist/components/conversation/DeskConversationList.d.ts.map +1 -0
  10. package/dist/components/conversation/DeskConversationList.js +168 -0
  11. package/dist/components/icon/index.d.ts +11 -0
  12. package/dist/components/icon/index.d.ts.map +1 -0
  13. package/dist/components/icon/index.js +18 -0
  14. package/dist/components/message/MessageList.d.ts +10 -0
  15. package/dist/components/message/MessageList.d.ts.map +1 -0
  16. package/dist/components/message/MessageList.js +91 -0
  17. package/dist/components/session/AssignedSessionFilter.d.ts +7 -0
  18. package/dist/components/session/AssignedSessionFilter.d.ts.map +1 -0
  19. package/dist/components/session/AssignedSessionFilter.js +90 -0
  20. package/dist/context/ChatContext.d.ts +4 -71
  21. package/dist/context/ChatContext.d.ts.map +1 -1
  22. package/dist/context/ChatContext.js +33 -344
  23. package/dist/hooks/conversation/useConversation.d.ts +11 -0
  24. package/dist/hooks/conversation/useConversation.d.ts.map +1 -0
  25. package/dist/hooks/conversation/useConversation.js +51 -0
  26. package/dist/hooks/message/useMessage.d.ts +9 -0
  27. package/dist/hooks/message/useMessage.d.ts.map +1 -0
  28. package/dist/hooks/message/useMessage.js +46 -0
  29. package/dist/hooks/message/useSendMessage.d.ts +10 -0
  30. package/dist/hooks/message/useSendMessage.d.ts.map +1 -0
  31. package/dist/hooks/message/useSendMessage.js +42 -0
  32. package/dist/index.d.ts +9 -26
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +10 -27
  35. package/dist/screens/desk-message/index.d.ts +3 -0
  36. package/dist/screens/desk-message/index.d.ts.map +1 -0
  37. package/dist/screens/desk-message/index.js +14 -0
  38. package/dist/types/chat.d.ts +6 -36
  39. package/dist/types/chat.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +0 -85
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/index.js +1 -1
  43. package/dist/types/sdk.d.ts +1 -0
  44. package/dist/types/sdk.d.ts.map +1 -0
  45. package/dist/types/sdk.js +1 -0
  46. package/package.json +19 -3
  47. package/dist/components/AutoScrollAnchor.d.ts +0 -2
  48. package/dist/components/AutoScrollAnchor.d.ts.map +0 -1
  49. package/dist/components/AutoScrollAnchor.js +0 -12
  50. package/dist/components/AutoScrollAnchor.jsx +0 -11
  51. package/dist/components/ChatBubble.jsx +0 -80
  52. package/dist/components/ChatHeader.d.ts +0 -8
  53. package/dist/components/ChatHeader.d.ts.map +0 -1
  54. package/dist/components/ChatHeader.js +0 -32
  55. package/dist/components/ChatHeader.jsx +0 -72
  56. package/dist/components/ChatInput.d.ts +0 -3
  57. package/dist/components/ChatInput.d.ts.map +0 -1
  58. package/dist/components/ChatInput.js +0 -379
  59. package/dist/components/ChatInput.jsx +0 -444
  60. package/dist/components/ChatInputDemo.d.ts +0 -2
  61. package/dist/components/ChatInputDemo.d.ts.map +0 -1
  62. package/dist/components/ChatInputDemo.js +0 -38
  63. package/dist/components/ChatInputDemo.jsx +0 -53
  64. package/dist/components/ChatInputWithCustomIcon.d.ts +0 -16
  65. package/dist/components/ChatInputWithCustomIcon.d.ts.map +0 -1
  66. package/dist/components/ChatInputWithCustomIcon.js +0 -85
  67. package/dist/components/ChatInputWithCustomIcon.jsx +0 -167
  68. package/dist/components/ChatLayout.d.ts +0 -6
  69. package/dist/components/ChatLayout.d.ts.map +0 -1
  70. package/dist/components/ChatLayout.js +0 -48
  71. package/dist/components/ChatLayout.jsx +0 -122
  72. package/dist/components/ConversationItem.d.ts +0 -9
  73. package/dist/components/ConversationItem.d.ts.map +0 -1
  74. package/dist/components/ConversationItem.js +0 -27
  75. package/dist/components/ConversationItem.jsx +0 -51
  76. package/dist/components/ConversationList.d.ts +0 -8
  77. package/dist/components/ConversationList.d.ts.map +0 -1
  78. package/dist/components/ConversationList.js +0 -11
  79. package/dist/components/ConversationList.jsx +0 -22
  80. package/dist/components/DateDivider.d.ts +0 -7
  81. package/dist/components/DateDivider.d.ts.map +0 -1
  82. package/dist/components/DateDivider.js +0 -27
  83. package/dist/components/DateDivider.jsx +0 -28
  84. package/dist/components/EmojiPicker.d.ts +0 -4
  85. package/dist/components/EmojiPicker.d.ts.map +0 -1
  86. package/dist/components/EmojiPicker.js +0 -191
  87. package/dist/components/EmojiPicker.jsx +0 -229
  88. package/dist/components/ImageLightbox.d.ts +0 -8
  89. package/dist/components/ImageLightbox.d.ts.map +0 -1
  90. package/dist/components/ImageLightbox.js +0 -8
  91. package/dist/components/ImageLightbox.jsx +0 -16
  92. package/dist/components/ImagePreviewModal.d.ts +0 -12
  93. package/dist/components/ImagePreviewModal.d.ts.map +0 -1
  94. package/dist/components/ImagePreviewModal.js +0 -55
  95. package/dist/components/ImagePreviewModal.jsx +0 -84
  96. package/dist/components/MessageItem.d.ts +0 -3
  97. package/dist/components/MessageItem.d.ts.map +0 -1
  98. package/dist/components/MessageItem.js +0 -38
  99. package/dist/components/MessageItem.jsx +0 -99
  100. package/dist/components/MessageItemDemo.d.ts +0 -2
  101. package/dist/components/MessageItemDemo.d.ts.map +0 -1
  102. package/dist/components/MessageItemDemo.js +0 -166
  103. package/dist/components/MessageItemDemo.jsx +0 -179
  104. package/dist/components/MessageList.d.ts +0 -15
  105. package/dist/components/MessageList.d.ts.map +0 -1
  106. package/dist/components/MessageList.js +0 -243
  107. package/dist/components/MessageList.jsx +0 -306
  108. package/dist/components/MessageListDemo.d.ts +0 -2
  109. package/dist/components/MessageListDemo.d.ts.map +0 -1
  110. package/dist/components/MessageListDemo.js +0 -165
  111. package/dist/components/MessageListDemo.jsx +0 -183
  112. package/dist/components/StickerPicker.d.ts +0 -4
  113. package/dist/components/StickerPicker.d.ts.map +0 -1
  114. package/dist/components/StickerPicker.js +0 -68
  115. package/dist/components/StickerPicker.jsx +0 -106
  116. package/dist/components/SwipeIndicator.d.ts +0 -9
  117. package/dist/components/SwipeIndicator.d.ts.map +0 -1
  118. package/dist/components/SwipeIndicator.js +0 -24
  119. package/dist/components/SwipeIndicator.jsx +0 -28
  120. package/dist/components/TextFormattingToolbar.d.ts +0 -4
  121. package/dist/components/TextFormattingToolbar.d.ts.map +0 -1
  122. package/dist/components/TextFormattingToolbar.js +0 -29
  123. package/dist/components/TextFormattingToolbar.jsx +0 -52
  124. package/dist/components/TypingIndicator.d.ts +0 -6
  125. package/dist/components/TypingIndicator.d.ts.map +0 -1
  126. package/dist/components/TypingIndicator.js +0 -21
  127. package/dist/components/TypingIndicator.jsx +0 -27
  128. package/dist/components/VoiceWaveIcon.d.ts +0 -7
  129. package/dist/components/VoiceWaveIcon.d.ts.map +0 -1
  130. package/dist/components/VoiceWaveIcon.js +0 -5
  131. package/dist/components/VoiceWaveIcon.jsx +0 -11
  132. package/dist/context/ChatContext.jsx +0 -346
  133. package/dist/hooks/useChat.d.ts +0 -5
  134. package/dist/hooks/useChat.d.ts.map +0 -1
  135. package/dist/hooks/useChat.js +0 -73
  136. package/dist/hooks/useConversationList.d.ts +0 -5
  137. package/dist/hooks/useConversationList.d.ts.map +0 -1
  138. package/dist/hooks/useConversationList.js +0 -9
  139. package/dist/hooks/useMessages.d.ts +0 -5
  140. package/dist/hooks/useMessages.d.ts.map +0 -1
  141. package/dist/hooks/useMessages.js +0 -192
  142. package/dist/hooks/useSocket.d.ts +0 -7
  143. package/dist/hooks/useSocket.d.ts.map +0 -1
  144. package/dist/hooks/useSocket.js +0 -120
  145. package/dist/hooks/useSwipeGesture.d.ts +0 -11
  146. package/dist/hooks/useSwipeGesture.d.ts.map +0 -1
  147. package/dist/hooks/useSwipeGesture.js +0 -54
  148. package/dist/hooks/useTextSelection.d.ts +0 -13
  149. package/dist/hooks/useTextSelection.d.ts.map +0 -1
  150. package/dist/hooks/useTextSelection.js +0 -132
  151. package/dist/hooks/useTyping.d.ts +0 -7
  152. package/dist/hooks/useTyping.d.ts.map +0 -1
  153. package/dist/hooks/useTyping.js +0 -64
@@ -1,243 +0,0 @@
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,306 +0,0 @@
1
- "use client";
2
- import { useEffect, useRef, useState, useCallback } from "react";
3
- import { MessageItem } from "./MessageItem";
4
- import { DateDivider } from "./DateDivider";
5
- import { TypingIndicator } from "./TypingIndicator";
6
- // import { AutoScrollAnchor } from "./AutoScrollAnchor" // Removed
7
- import { useSwipeGesture } from "../hooks/useSwipeGesture";
8
- import { useChatContext } from "../context/ChatContext";
9
- import { ImagePreviewModal } from "./ImagePreviewModal"; // Import the new modal
10
- export function MessageList({ messages = [], // Add default empty array
11
- isLoadingMore = false, onLoadMore, hasMore = false, currentUserId, conversationId, className = "", onSwipeBack, }) {
12
- const { state } = useChatContext();
13
- const scrollRef = useRef(null);
14
- const shouldScrollToBottomRef = useRef(true); // New ref to control auto-scrolling
15
- const lastMessageCountRef = useRef((messages === null || messages === void 0 ? void 0 : messages.length) || 0); // Add null check
16
- const [showSwipeHint, setShowSwipeHint] = useState(false);
17
- const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false); // State for button visibility
18
- // State for ImagePreviewModal
19
- const [isImagePreviewModalOpen, setIsImagePreviewModalOpen] = useState(false);
20
- const [previewImages, setPreviewImages] = useState([]);
21
- const [initialPreviewImageId, setInitialPreviewImageId] = useState("");
22
- // Convert internal Message format to DisplayMessage format
23
- const convertToDisplayMessages = useCallback(() => {
24
- if (!messages || !Array.isArray(messages))
25
- return []; // Add safety check
26
- return messages.map((msg) => {
27
- const isCurrentUser = msg.senderId === currentUserId;
28
- // Handle different message types
29
- if (msg.attachments && msg.attachments.length > 0) {
30
- return {
31
- id: msg.id,
32
- senderId: msg.senderId,
33
- type: "media",
34
- text: msg.content || undefined,
35
- attachments: msg.attachments.map((att) => ({
36
- id: att.id, // Ensure ID is passed
37
- type: att.type.startsWith("image/") ? "image" : "file",
38
- url: att.url,
39
- name: att.name,
40
- size: att.size,
41
- })),
42
- createdAt: msg.timestamp.toISOString(),
43
- isMine: isCurrentUser,
44
- };
45
- }
46
- // Promotional message type (assuming senderId 'system' or similar for demo)
47
- if (msg.type === "promo" && msg.promoData) {
48
- return {
49
- id: msg.id,
50
- senderId: msg.senderId,
51
- type: "promo",
52
- promoData: msg.promoData,
53
- createdAt: msg.timestamp.toISOString(),
54
- isMine: isCurrentUser, // Can be false for system messages
55
- };
56
- }
57
- // Regular text message
58
- return {
59
- id: msg.id,
60
- senderId: msg.senderId,
61
- type: "text",
62
- text: msg.content,
63
- createdAt: msg.timestamp.toISOString(),
64
- isMine: isCurrentUser,
65
- };
66
- });
67
- }, [messages, currentUserId]);
68
- const displayMessages = convertToDisplayMessages();
69
- // Swipe gesture for going back (secondary swipe area)
70
- const messageSwipeRef = useSwipeGesture({
71
- onSwipeRight: () => {
72
- if (window.innerWidth < 768) {
73
- setShowSwipeHint(true);
74
- setTimeout(() => setShowSwipeHint(false), 1500);
75
- onSwipeBack === null || onSwipeBack === void 0 ? void 0 : onSwipeBack();
76
- }
77
- },
78
- threshold: 80,
79
- restraint: 120,
80
- allowedTime: 400,
81
- enabled: true,
82
- });
83
- // Auto-scroll to bottom logic
84
- const scrollToBottom = useCallback((force = false) => {
85
- if (scrollRef.current) {
86
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
87
- if (force) {
88
- shouldScrollToBottomRef.current = true; // If forced, ensure auto-scroll is re-enabled
89
- }
90
- }
91
- }, []);
92
- // Handle scroll events to manage auto-scroll behavior and button visibility
93
- const handleScroll = useCallback(() => {
94
- if (!scrollRef.current)
95
- return;
96
- const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
97
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
98
- const SCROLL_UP_THRESHOLD = 200; // If user scrolls up more than 200px from bottom, disable auto-scroll
99
- const SCROLL_DOWN_THRESHOLD = 5; // If user scrolls within 5px of bottom, re-enable auto-scroll
100
- if (distanceFromBottom > SCROLL_UP_THRESHOLD) {
101
- shouldScrollToBottomRef.current = false;
102
- }
103
- else if (distanceFromBottom <= SCROLL_DOWN_THRESHOLD) {
104
- shouldScrollToBottomRef.current = true;
105
- }
106
- // Show button if not at bottom AND auto-scroll is disabled
107
- setShowScrollToBottomButton(distanceFromBottom > SCROLL_DOWN_THRESHOLD && !shouldScrollToBottomRef.current);
108
- }, []);
109
- // Handle new messages
110
- useEffect(() => {
111
- const currentMessageCount = (messages === null || messages === void 0 ? void 0 : messages.length) || 0;
112
- const previousMessageCount = lastMessageCountRef.current;
113
- if (currentMessageCount > previousMessageCount) {
114
- const newMessages = messages.slice(previousMessageCount);
115
- const hasNewMessageFromCurrentUser = newMessages.some((msg) => msg.senderId === currentUserId);
116
- // If current user sent a message, always scroll to bottom
117
- // If another user sent a message, only scroll if shouldScrollToBottomRef is true (user is already at bottom)
118
- if (hasNewMessageFromCurrentUser) {
119
- setTimeout(() => scrollToBottom(true), 50); // Force scroll for own messages
120
- }
121
- else if (shouldScrollToBottomRef.current) {
122
- setTimeout(() => scrollToBottom(), 50); // Scroll if auto-scroll is enabled for others' messages
123
- }
124
- }
125
- lastMessageCountRef.current = currentMessageCount;
126
- }, [messages, currentUserId, scrollToBottom]); // scrollToBottom is a dependency because it's called here
127
- // Attach and detach scroll listener
128
- useEffect(() => {
129
- const currentScrollRef = scrollRef.current;
130
- if (currentScrollRef) {
131
- currentScrollRef.addEventListener("scroll", handleScroll);
132
- // Initial check for button visibility
133
- handleScroll();
134
- }
135
- return () => {
136
- if (currentScrollRef) {
137
- currentScrollRef.removeEventListener("scroll", handleScroll);
138
- }
139
- };
140
- }, [handleScroll]); // Re-attach if handleScroll changes (due to useCallback dependencies)
141
- // Preserve scroll position when loading more messages
142
- useEffect(() => {
143
- if (isLoadingMore)
144
- return;
145
- const scrollContainer = scrollRef.current;
146
- if (!scrollContainer)
147
- return;
148
- // Save current scroll position
149
- const previousScrollHeight = scrollContainer.scrollHeight;
150
- const previousScrollTop = scrollContainer.scrollTop;
151
- // After new messages are loaded, adjust scroll position
152
- const adjustScrollPosition = () => {
153
- const newScrollHeight = scrollContainer.scrollHeight;
154
- const heightDifference = newScrollHeight - previousScrollHeight;
155
- if (heightDifference > 0) {
156
- scrollContainer.scrollTop = previousScrollTop + heightDifference;
157
- }
158
- };
159
- // Use setTimeout to ensure DOM has updated
160
- setTimeout(adjustScrollPosition, 0);
161
- }, [isLoadingMore]);
162
- // Show swipe hint on first load for mobile users
163
- useEffect(() => {
164
- const hasSeenHint = localStorage.getItem("chat-swipe-hint-seen");
165
- if (!hasSeenHint && window.innerWidth < 768) {
166
- setTimeout(() => {
167
- setShowSwipeHint(true);
168
- setTimeout(() => {
169
- setShowSwipeHint(false);
170
- localStorage.setItem("chat-swipe-hint-seen", "true");
171
- }, 3000);
172
- }, 1000);
173
- }
174
- }, []);
175
- // Group messages by date
176
- const groupMessagesByDate = useCallback(() => {
177
- const groups = [];
178
- let currentDate = "";
179
- let currentGroup = [];
180
- displayMessages.forEach((message) => {
181
- const messageDate = new Date(message.createdAt).toDateString();
182
- if (messageDate !== currentDate) {
183
- if (currentGroup.length > 0) {
184
- groups.push({ date: currentDate, messages: currentGroup });
185
- }
186
- currentDate = messageDate;
187
- currentGroup = [message];
188
- }
189
- else {
190
- currentGroup.push(message);
191
- }
192
- });
193
- if (currentGroup.length > 0) {
194
- groups.push({ date: currentDate, messages: currentGroup });
195
- }
196
- return groups;
197
- }, [displayMessages]);
198
- const messageGroups = groupMessagesByDate();
199
- // Format date labels in Vietnamese
200
- const formatDateLabel = (date) => {
201
- const now = new Date();
202
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
203
- const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
204
- const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
205
- if (messageDate.getTime() === today.getTime()) {
206
- return "Hôm nay";
207
- }
208
- else if (messageDate.getTime() === yesterday.getTime()) {
209
- return "Hôm qua";
210
- }
211
- else {
212
- return date.toLocaleDateString("vi-VN", {
213
- weekday: "long",
214
- year: "numeric",
215
- month: "long",
216
- day: "numeric",
217
- });
218
- }
219
- };
220
- // Handler for image clicks from MessageItem
221
- const handleImageClick = useCallback((imageId, images) => {
222
- setPreviewImages(images);
223
- setInitialPreviewImageId(imageId);
224
- setIsImagePreviewModalOpen(true);
225
- }, []);
226
- if (!messages || messages.length === 0) {
227
- return (<div className={`flex items-center justify-center h-full ${className}`}>
228
- <div className="text-center text-gray-500">
229
- <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
230
- <svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
231
- <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"/>
232
- </svg>
233
- </div>
234
- <p className="text-sm">Chưa có tin nhắn nào</p>
235
- <p className="text-xs text-gray-400 mt-1">Hãy bắt đầu cuộc trò chuyện!</p>
236
- </div>
237
- </div>);
238
- }
239
- return (<div className={`relative h-full ${className}`}>
240
- {/* Swipe hint overlay */}
241
- {showSwipeHint && (<div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-20 md:hidden">
242
- <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">
243
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
244
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7"/>
245
- </svg>
246
- <span>Vuốt phải để quay lại</span>
247
- </div>
248
- </div>)}
249
-
250
- {/* Scroll to bottom button */}
251
- {showScrollToBottomButton && (<div className="absolute bottom-20 right-4 z-10">
252
- <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">
253
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
255
- </svg>
256
- </button>
257
- </div>)}
258
-
259
- {/* Scrollable message container */}
260
- <div ref={(el) => {
261
- scrollRef.current = el;
262
- if (messageSwipeRef.current !== el) {
263
- messageSwipeRef.current = el;
264
- }
265
- }} className="h-full overflow-y-auto p-3 sm:p-4" style={{
266
- WebkitOverflowScrolling: "touch", // Smooth scrolling on iOS
267
- }} onScroll={handleScroll}>
268
- {/* Loading more indicator */}
269
- {isLoadingMore && (<div className="flex justify-center py-4">
270
- <div className="flex items-center space-x-2 text-gray-500">
271
- <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
272
- <span className="text-sm">Đang tải thêm tin nhắn...</span>
273
- </div>
274
- </div>)}
275
-
276
- {/* Load more button */}
277
- {hasMore && !isLoadingMore && (<div className="flex justify-center py-4">
278
- <button onClick={onLoadMore} className="text-blue-500 hover:text-blue-600 text-sm font-medium">
279
- Tải thêm tin nhắn
280
- </button>
281
- </div>)}
282
-
283
- {/* Message groups */}
284
- <div className="space-y-3 sm:space-y-4">
285
- {messageGroups.map((group, groupIndex) => (<div key={group.date}>
286
- <DateDivider date={new Date(group.date)} customLabel={formatDateLabel(new Date(group.date))}/>
287
- <div className="space-y-1 sm:space-y-2">
288
- {group.messages.map((message, messageIndex) => {
289
- const prevMessage = messageIndex > 0 ? group.messages[messageIndex - 1] : null;
290
- const isGrouped = (prevMessage === null || prevMessage === void 0 ? void 0 : prevMessage.senderId) === message.senderId &&
291
- new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() < 300000; // 5 minutes
292
- return (<MessageItem key={message.id} message={message} isGrouped={isGrouped} onImageClick={handleImageClick} // Pass the handler down
293
- />);
294
- })}
295
- </div>
296
- </div>))}
297
- </div>
298
-
299
- {/* Typing indicator */}
300
- {conversationId && <TypingIndicator conversationId={conversationId}/>}
301
- </div>
302
-
303
- {/* Image Preview Modal */}
304
- {isImagePreviewModalOpen && (<ImagePreviewModal images={previewImages} initialImageId={initialPreviewImageId} onClose={() => setIsImagePreviewModalOpen(false)}/>)}
305
- </div>);
306
- }
@@ -1,2 +0,0 @@
1
- export declare function MessageListDemo(): import("react/jsx-runtime").JSX.Element;
2
- //# sourceMappingURL=MessageListDemo.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"MessageListDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageListDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,4CAmN9B"}