@droppii-org/chat-sdk 0.0.1

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 (100) hide show
  1. package/dist/components/AutoScrollAnchor.d.ts +2 -0
  2. package/dist/components/AutoScrollAnchor.d.ts.map +1 -0
  3. package/dist/components/AutoScrollAnchor.jsx +11 -0
  4. package/dist/components/ChatBubble.d.ts +2 -0
  5. package/dist/components/ChatBubble.d.ts.map +1 -0
  6. package/dist/components/ChatBubble.jsx +32 -0
  7. package/dist/components/ChatHeader.d.ts +8 -0
  8. package/dist/components/ChatHeader.d.ts.map +1 -0
  9. package/dist/components/ChatHeader.jsx +72 -0
  10. package/dist/components/ChatInput.d.ts +4 -0
  11. package/dist/components/ChatInput.d.ts.map +1 -0
  12. package/dist/components/ChatInput.jsx +444 -0
  13. package/dist/components/ChatInputDemo.d.ts +2 -0
  14. package/dist/components/ChatInputDemo.d.ts.map +1 -0
  15. package/dist/components/ChatInputDemo.jsx +53 -0
  16. package/dist/components/ChatInputWithCustomIcon.d.ts +17 -0
  17. package/dist/components/ChatInputWithCustomIcon.d.ts.map +1 -0
  18. package/dist/components/ChatInputWithCustomIcon.jsx +167 -0
  19. package/dist/components/ChatLayout.d.ts +6 -0
  20. package/dist/components/ChatLayout.d.ts.map +1 -0
  21. package/dist/components/ChatLayout.jsx +122 -0
  22. package/dist/components/ConversationItem.d.ts +9 -0
  23. package/dist/components/ConversationItem.d.ts.map +1 -0
  24. package/dist/components/ConversationItem.jsx +51 -0
  25. package/dist/components/ConversationList.d.ts +8 -0
  26. package/dist/components/ConversationList.d.ts.map +1 -0
  27. package/dist/components/ConversationList.jsx +22 -0
  28. package/dist/components/DateDivider.d.ts +7 -0
  29. package/dist/components/DateDivider.d.ts.map +1 -0
  30. package/dist/components/DateDivider.jsx +28 -0
  31. package/dist/components/EmojiPicker.d.ts +4 -0
  32. package/dist/components/EmojiPicker.d.ts.map +1 -0
  33. package/dist/components/EmojiPicker.jsx +229 -0
  34. package/dist/components/ImageLightbox.d.ts +8 -0
  35. package/dist/components/ImageLightbox.d.ts.map +1 -0
  36. package/dist/components/ImageLightbox.jsx +16 -0
  37. package/dist/components/ImagePreviewModal.d.ts +12 -0
  38. package/dist/components/ImagePreviewModal.d.ts.map +1 -0
  39. package/dist/components/ImagePreviewModal.jsx +84 -0
  40. package/dist/components/MessageItem.d.ts +3 -0
  41. package/dist/components/MessageItem.d.ts.map +1 -0
  42. package/dist/components/MessageItem.jsx +99 -0
  43. package/dist/components/MessageItemDemo.d.ts +2 -0
  44. package/dist/components/MessageItemDemo.d.ts.map +1 -0
  45. package/dist/components/MessageItemDemo.jsx +179 -0
  46. package/dist/components/MessageList.d.ts +15 -0
  47. package/dist/components/MessageList.d.ts.map +1 -0
  48. package/dist/components/MessageList.jsx +306 -0
  49. package/dist/components/MessageListDemo.d.ts +2 -0
  50. package/dist/components/MessageListDemo.d.ts.map +1 -0
  51. package/dist/components/MessageListDemo.jsx +183 -0
  52. package/dist/components/StickerPicker.d.ts +4 -0
  53. package/dist/components/StickerPicker.d.ts.map +1 -0
  54. package/dist/components/StickerPicker.jsx +106 -0
  55. package/dist/components/SwipeIndicator.d.ts +9 -0
  56. package/dist/components/SwipeIndicator.d.ts.map +1 -0
  57. package/dist/components/SwipeIndicator.jsx +28 -0
  58. package/dist/components/TextFormattingToolbar.d.ts +4 -0
  59. package/dist/components/TextFormattingToolbar.d.ts.map +1 -0
  60. package/dist/components/TextFormattingToolbar.jsx +52 -0
  61. package/dist/components/TypingIndicator.d.ts +6 -0
  62. package/dist/components/TypingIndicator.d.ts.map +1 -0
  63. package/dist/components/TypingIndicator.jsx +27 -0
  64. package/dist/components/VoiceWaveIcon.d.ts +7 -0
  65. package/dist/components/VoiceWaveIcon.d.ts.map +1 -0
  66. package/dist/components/VoiceWaveIcon.jsx +11 -0
  67. package/dist/context/ChatContext.d.ts +72 -0
  68. package/dist/context/ChatContext.d.ts.map +1 -0
  69. package/dist/context/ChatContext.jsx +346 -0
  70. package/dist/hooks/useChat.d.ts +5 -0
  71. package/dist/hooks/useChat.d.ts.map +1 -0
  72. package/dist/hooks/useChat.js +73 -0
  73. package/dist/hooks/useConversationList.d.ts +5 -0
  74. package/dist/hooks/useConversationList.d.ts.map +1 -0
  75. package/dist/hooks/useConversationList.js +9 -0
  76. package/dist/hooks/useMessages.d.ts +5 -0
  77. package/dist/hooks/useMessages.d.ts.map +1 -0
  78. package/dist/hooks/useMessages.js +192 -0
  79. package/dist/hooks/useSocket.d.ts +7 -0
  80. package/dist/hooks/useSocket.d.ts.map +1 -0
  81. package/dist/hooks/useSocket.js +120 -0
  82. package/dist/hooks/useSwipeGesture.d.ts +11 -0
  83. package/dist/hooks/useSwipeGesture.d.ts.map +1 -0
  84. package/dist/hooks/useSwipeGesture.js +54 -0
  85. package/dist/hooks/useTextSelection.d.ts +13 -0
  86. package/dist/hooks/useTextSelection.d.ts.map +1 -0
  87. package/dist/hooks/useTextSelection.js +132 -0
  88. package/dist/hooks/useTyping.d.ts +7 -0
  89. package/dist/hooks/useTyping.d.ts.map +1 -0
  90. package/dist/hooks/useTyping.js +64 -0
  91. package/dist/index.d.ts +28 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +29 -0
  94. package/dist/types/chat.d.ts +39 -0
  95. package/dist/types/chat.d.ts.map +1 -0
  96. package/dist/types/chat.js +1 -0
  97. package/dist/types/index.d.ts +86 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +1 -0
  100. package/package.json +60 -0
@@ -0,0 +1,229 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { useState } from "react";
4
+ import { X, Search, Clock, Smile, Heart, Hand, Car, Lightbulb, Coffee, Flag } from "lucide-react";
5
+ const emojiCategories = {
6
+ recent: {
7
+ label: "Thường xuyên sử dụng",
8
+ icon: Clock,
9
+ emojis: ["😊", "😂", "❤️", "👍", "😢", "😮", "😡", "🎉", "👏"],
10
+ },
11
+ smileys: {
12
+ label: "Mặt cười & con người",
13
+ icon: Smile,
14
+ emojis: [
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
+ "😓",
74
+ "🤗",
75
+ "🤔",
76
+ "🤭",
77
+ "🤫",
78
+ "🤥",
79
+ "😶",
80
+ "😐",
81
+ "😑",
82
+ "😬",
83
+ "🙄",
84
+ "😯",
85
+ "😦",
86
+ "😧",
87
+ "😮",
88
+ "😲",
89
+ "🥱",
90
+ "😴",
91
+ "🤤",
92
+ "😪",
93
+ "😵",
94
+ "🤐",
95
+ ],
96
+ },
97
+ gestures: {
98
+ label: "Cử chỉ",
99
+ icon: Hand,
100
+ emojis: [
101
+ "👍",
102
+ "👎",
103
+ "👌",
104
+ "✌️",
105
+ "🤞",
106
+ "🤟",
107
+ "🤘",
108
+ "🤙",
109
+ "👈",
110
+ "👉",
111
+ "👆",
112
+ "🖕",
113
+ "👇",
114
+ "☝️",
115
+ "👏",
116
+ "🙌",
117
+ "👐",
118
+ "🤲",
119
+ "🤝",
120
+ "🙏",
121
+ ],
122
+ },
123
+ hearts: {
124
+ label: "Trái tim",
125
+ icon: Heart,
126
+ emojis: ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝"],
127
+ },
128
+ objects: {
129
+ label: "Đồ vật",
130
+ icon: Coffee,
131
+ emojis: ["📱", "💻", "⌨️", "🖥️", "🖨️", "📷", "📹", "🎥", "📞", "☎️", "📠", "📺", "📻", "🎵", "🎶", "🎤", "🎧", "📢"],
132
+ },
133
+ travel: {
134
+ label: "Du lịch",
135
+ icon: Car,
136
+ emojis: ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🏍️", "🛵", "🚲", "🛴"],
137
+ },
138
+ activities: {
139
+ label: "Hoạt động",
140
+ icon: Lightbulb,
141
+ emojis: [
142
+ "⚽",
143
+ "🏀",
144
+ "🏈",
145
+ "⚾",
146
+ "🥎",
147
+ "🎾",
148
+ "🏐",
149
+ "🏉",
150
+ "🥏",
151
+ "🎱",
152
+ "🪀",
153
+ "🏓",
154
+ "🏸",
155
+ "🏒",
156
+ "🏑",
157
+ "🥍",
158
+ "🏏",
159
+ "🪃",
160
+ ],
161
+ },
162
+ flags: {
163
+ label: "Cờ",
164
+ icon: Flag,
165
+ emojis: ["🏳️", "🏴", "🏁", "🚩", "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇻🇳", "🇺🇸", "🇬🇧", "🇫🇷", "🇩🇪", "🇯🇵", "🇰🇷", "🇨🇳", "🇮🇳"],
166
+ },
167
+ };
168
+ export const EmojiPicker = React.forwardRef(({ onEmojiSelect, onClose, isOpen = false, style }, ref) => {
169
+ const [activeCategory, setActiveCategory] = useState("recent");
170
+ const [searchTerm, setSearchTerm] = useState("");
171
+ if (!isOpen)
172
+ return null;
173
+ const handleEmojiClick = (emoji) => {
174
+ onEmojiSelect === null || onEmojiSelect === void 0 ? void 0 : onEmojiSelect(emoji);
175
+ };
176
+ const filteredEmojis = searchTerm
177
+ ? emojiCategories[activeCategory].emojis.filter((emoji) =>
178
+ // Simple search - in a real app you'd have emoji names/keywords
179
+ emoji.includes(searchTerm))
180
+ : emojiCategories[activeCategory].emojis;
181
+ return (<div ref={ref} className="absolute bottom-full mb-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-full sm:max-w-xs md:w-80" style={style} // Apply dynamic style here
182
+ >
183
+ <div className="p-2">
184
+ {/* Category Icons Row - Compact */}
185
+ <div className="flex items-center justify-between mb-2 pb-1 border-b border-gray-100">
186
+ {Object.entries(emojiCategories).map(([key, category]) => {
187
+ const Icon = category.icon;
188
+ const isActive = activeCategory === key;
189
+ return (<button key={key} onClick={() => setActiveCategory(key)} className={`
190
+ p-1.5 rounded-full transition-all duration-200 hover:bg-gray-100
191
+ ${isActive ? "text-blue-500 bg-blue-50" : "text-gray-500"}
192
+ `} title={category.label}>
193
+ <Icon className="w-4 h-4"/>
194
+ </button>);
195
+ })}
196
+
197
+ {/* Close button */}
198
+ <button onClick={onClose} className="p-1.5 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100">
199
+ <X className="w-4 h-4"/>
200
+ </button>
201
+ </div>
202
+
203
+ {/* Search Bar - Compact */}
204
+ <div className="relative mb-2">
205
+ <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-3 h-3 text-gray-400"/>
206
+ <input type="text" placeholder="Tìm kiếm" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-7 pr-3 py-1.5 text-xs bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent"/>
207
+ </div>
208
+
209
+ {/* Category Title - Compact */}
210
+ <div className="mb-2">
211
+ <h3 className="text-xs font-medium text-gray-900">{emojiCategories[activeCategory].label}</h3>
212
+ </div>
213
+
214
+ {/* Emoji Grid - Compact */}
215
+ <div className="grid grid-cols-9 gap-0.5 max-h-48 overflow-y-auto">
216
+ {filteredEmojis.map((emoji, index) => (<button key={index} onClick={() => handleEmojiClick(emoji)} className="p-1.5 text-lg hover:bg-gray-100 rounded transition-all duration-200 hover:scale-110 active:scale-95" title={emoji}>
217
+ {emoji}
218
+ </button>))}
219
+ </div>
220
+
221
+ {/* Empty state */}
222
+ {filteredEmojis.length === 0 && (<div className="text-center py-4 text-gray-500">
223
+ <div className="text-lg mb-1">🔍</div>
224
+ <p className="text-xs">Không tìm thấy emoji nào</p>
225
+ </div>)}
226
+ </div>
227
+ </div>);
228
+ });
229
+ EmojiPicker.displayName = "EmojiPicker";
@@ -0,0 +1,8 @@
1
+ interface ImageLightboxProps {
2
+ src: string;
3
+ alt: string;
4
+ onClose: () => void;
5
+ }
6
+ export declare function ImageLightbox({ src, alt, onClose }: ImageLightboxProps): import("react").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=ImageLightbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageLightbox.d.ts","sourceRoot":"","sources":["../../src/components/ImageLightbox.tsx"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,kBAAkB,+BA0BtE"}
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { X } from "lucide-react";
3
+ import Image from "next/image";
4
+ export function ImageLightbox({ src, alt, onClose }) {
5
+ return (<div className="fixed inset-0 z-[9999] bg-black bg-opacity-80 flex items-center justify-center p-4" onClick={onClose}>
6
+ <button onClick={onClose} className="absolute top-4 right-4 text-white p-2 rounded-full bg-gray-800/50 hover:bg-gray-700/70 transition-colors z-50" aria-label="Close image">
7
+ <X className="w-6 h-6"/>
8
+ </button>
9
+ <div className="relative max-w-full max-h-full" onClick={(e) => e.stopPropagation()}>
10
+ <Image src={src || "/placeholder.svg"} alt={alt} width={1200} // Max width for lightbox image
11
+ height={800} // Max height for lightbox image
12
+ style={{ width: "auto", height: "auto", maxWidth: "90vw", maxHeight: "90vh" }} className="rounded-lg shadow-xl object-contain" priority // Load immediately
13
+ />
14
+ </div>
15
+ </div>);
16
+ }
@@ -0,0 +1,12 @@
1
+ interface ImagePreviewModalProps {
2
+ images: {
3
+ id: string;
4
+ url: string;
5
+ name?: string;
6
+ }[];
7
+ initialImageId: string;
8
+ onClose: () => void;
9
+ }
10
+ export declare function ImagePreviewModal({ images, initialImageId, onClose }: ImagePreviewModalProps): import("react").JSX.Element | null;
11
+ export {};
12
+ //# sourceMappingURL=ImagePreviewModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImagePreviewModal.d.ts","sourceRoot":"","sources":["../../src/components/ImagePreviewModal.tsx"],"names":[],"mappings":"AAOA,UAAU,sBAAsB;IAC9B,MAAM,EAAE;QACN,EAAE,EAAE,MAAM,CAAA;QACV,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,EAAE,CAAA;IACH,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,sBAAsB,sCAmH5F"}
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { useState, useEffect, useCallback } from "react";
3
+ import Image from "next/image";
4
+ import { X, ChevronLeft, ChevronRight } from "lucide-react";
5
+ export function ImagePreviewModal({ images, initialImageId, onClose }) {
6
+ // Find the initial index based on initialImageId
7
+ const initialIndex = images.findIndex((img) => img.id === initialImageId);
8
+ const [currentImageIndex, setCurrentImageIndex] = useState(initialIndex !== -1 ? initialIndex : 0);
9
+ // Ensure currentImageIndex is valid if initialImageId wasn't found or images array is empty
10
+ useEffect(() => {
11
+ if (initialIndex === -1 && images.length > 0) {
12
+ setCurrentImageIndex(0);
13
+ }
14
+ else if (images.length === 0) {
15
+ onClose(); // Close if no images are provided
16
+ }
17
+ }, [initialImageId, images, initialIndex, onClose]);
18
+ const currentImage = images[currentImageIndex];
19
+ const handlePrev = useCallback(() => {
20
+ setCurrentImageIndex((prevIndex) => Math.max(0, prevIndex - 1));
21
+ }, []);
22
+ const handleNext = useCallback(() => {
23
+ setCurrentImageIndex((prevIndex) => Math.min(images.length - 1, prevIndex + 1));
24
+ }, [images.length]);
25
+ const handleKeyDown = useCallback((event) => {
26
+ if (event.key === "Escape") {
27
+ onClose();
28
+ }
29
+ else if (event.key === "ArrowLeft") {
30
+ handlePrev();
31
+ }
32
+ else if (event.key === "ArrowRight") {
33
+ handleNext();
34
+ }
35
+ }, [onClose, handlePrev, handleNext]);
36
+ // Add and remove keyboard event listener
37
+ useEffect(() => {
38
+ document.addEventListener("keydown", handleKeyDown);
39
+ return () => {
40
+ document.removeEventListener("keydown", handleKeyDown);
41
+ };
42
+ }, [handleKeyDown]);
43
+ if (!currentImage) {
44
+ return null; // Or render a loading/error state
45
+ }
46
+ const isFirstImage = currentImageIndex === 0;
47
+ const isLastImage = currentImageIndex === images.length - 1;
48
+ return (<div className="fixed inset-0 z-[9999] bg-black bg-opacity-90 flex items-center justify-center p-4 sm:p-8" onClick={onClose} // Close when clicking on the overlay
49
+ >
50
+ {/* Close Button */}
51
+ <button onClick={onClose} className="absolute top-4 right-4 text-white p-2 rounded-full bg-gray-800/50 hover:bg-gray-700/70 transition-colors z-50" aria-label="Đóng" title="Đóng (Esc)">
52
+ <X className="w-6 h-6"/>
53
+ </button>
54
+
55
+ {/* Image Container */}
56
+ <div className="relative flex items-center justify-center w-full h-full max-w-screen-xl max-h-screen-xl" onClick={(e) => e.stopPropagation()} // Prevent closing when clicking on the image itself
57
+ >
58
+ {/* Previous Button */}
59
+ <button onClick={handlePrev} disabled={isFirstImage} className={`absolute left-2 sm:left-4 p-3 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/70 transition-colors z-40
60
+ ${isFirstImage ? "opacity-50 cursor-not-allowed" : ""}`} aria-label="Ảnh trước" title="Ảnh trước (Mũi tên trái)">
61
+ <ChevronLeft className="w-6 h-6 sm:w-8 sm:h-8"/>
62
+ </button>
63
+
64
+ {/* Image Display */}
65
+ <div className="relative w-full h-full flex items-center justify-center">
66
+ <Image src={currentImage.url || "/placeholder.svg"} alt={currentImage.name || "Xem trước hình ảnh"} layout="fill" // Use fill to make it responsive within its parent
67
+ objectFit="contain" // Ensure the image fits within the container without cropping
68
+ className="rounded-lg shadow-xl" priority // Load immediately for better UX
69
+ />
70
+ </div>
71
+
72
+ {/* Next Button */}
73
+ <button onClick={handleNext} disabled={isLastImage} className={`absolute right-2 sm:right-4 p-3 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/70 transition-colors z-40
74
+ ${isLastImage ? "opacity-50 cursor-not-allowed" : ""}`} aria-label="Ảnh tiếp theo" title="Ảnh tiếp theo (Mũi tên phải)">
75
+ <ChevronRight className="w-6 h-6 sm:w-8 sm:h-8"/>
76
+ </button>
77
+ </div>
78
+
79
+ {/* Image Name/Counter (Optional) */}
80
+ <div className="absolute bottom-4 text-white text-sm sm:text-base bg-black/50 px-4 py-2 rounded-full">
81
+ {currentImage.name || "Hình ảnh"} ({currentImageIndex + 1} / {images.length})
82
+ </div>
83
+ </div>);
84
+ }
@@ -0,0 +1,3 @@
1
+ import type { MessageItemProps } from "../types";
2
+ export declare function MessageItem({ message, isGrouped, onImageClick }: MessageItemProps): import("react").JSX.Element;
3
+ //# sourceMappingURL=MessageItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageItem.d.ts","sourceRoot":"","sources":["../../src/components/MessageItem.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,UAAU,CAAA;AAIhE,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,gBAAgB,+BAwLjF"}
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ import { useChatContext } from "../context/ChatContext";
3
+ import { FileText, Download } from "lucide-react";
4
+ import Image from "next/image";
5
+ export function MessageItem({ message, isGrouped, onImageClick }) {
6
+ const { state } = useChatContext();
7
+ const isOwnMessage = message.isMine;
8
+ const sender = state.users[message.senderId];
9
+ const formatFileSize = (bytes) => {
10
+ if (bytes === 0)
11
+ return "0 Bytes";
12
+ const k = 1024;
13
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
14
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
15
+ return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
16
+ };
17
+ const renderTextMessage = (msg) => {
18
+ const content = msg.text || "";
19
+ // Simple regex to detect URLs and make them clickable
20
+ const linkRegex = /(https?:\/\/[^\s]+)/g;
21
+ const parts = content.split(linkRegex);
22
+ return (<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") : ""}`}>
23
+ <p className="text-sm sm:text-base whitespace-pre-wrap">
24
+ {parts.map((part, index) => linkRegex.test(part) ? (<a key={index} href={part} target="_blank" rel="noopener noreferrer" className="underline text-blue-200 hover:text-blue-100">
25
+ {part}
26
+ </a>) : (part))}
27
+ </p>
28
+ </div>);
29
+ };
30
+ const renderMediaMessage = (msg) => {
31
+ var _a, _b;
32
+ const imageAttachments = ((_a = msg.attachments) === null || _a === void 0 ? void 0 : _a.filter((att) => att.type === "image")) || [];
33
+ return (<div className={`flex flex-col gap-2 ${isOwnMessage ? "items-end" : "items-start"}`}>
34
+ {msg.text && (<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") : ""}`}>
35
+ <p className="text-sm sm:text-base whitespace-pre-wrap">{msg.text}</p>
36
+ </div>)}
37
+ <div className={`grid gap-2 ${imageAttachments.length > 1 ? "grid-cols-2" : "grid-cols-1"}`}>
38
+ {(_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.map((attachment, idx) => (<div key={idx} className={`relative rounded-lg overflow-hidden ${isOwnMessage ? "bg-blue-100" : "bg-gray-100"}`}>
39
+ {attachment.type === "image" ? (<>
40
+ <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 })))}/>
41
+ <div className="absolute inset-0 bg-black/10 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
42
+ <span className="text-white text-xs font-medium p-1 rounded bg-black/50">Xem ảnh</span>
43
+ </div>
44
+ </>) : (<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">
45
+ <FileText className="w-5 h-5 flex-shrink-0"/>
46
+ <div className="flex-1 min-w-0">
47
+ <span className="block text-sm font-medium truncate">{attachment.name || "File"}</span>
48
+ <span className="block text-xs text-gray-500">
49
+ {attachment.size ? formatFileSize(attachment.size) : "Unknown size"}
50
+ </span>
51
+ </div>
52
+ <Download className="w-4 h-4 flex-shrink-0 text-gray-600"/>
53
+ </a>)}
54
+ </div>))}
55
+ </div>
56
+ </div>);
57
+ };
58
+ const renderPromoMessage = (msg) => {
59
+ if (!msg.promoData)
60
+ return null;
61
+ const { imageUrl, title, description, buttonText, buttonUrl } = msg.promoData;
62
+ return (<div className="w-full max-w-xs mx-auto my-2 bg-white shadow-md rounded-lg overflow-hidden border border-gray-200">
63
+ <div className="relative w-full h-32 bg-gray-200">
64
+ <Image src={imageUrl || "/placeholder.svg"} alt={title} fill style={{ objectFit: "cover" }} className="rounded-t-lg"/>
65
+ </div>
66
+ <div className="p-4">
67
+ <h3 className="text-base font-semibold text-gray-900 mb-1">{title}</h3>
68
+ <p className="text-sm text-gray-600 mb-3">{description}</p>
69
+ <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">
70
+ {buttonText}
71
+ </a>
72
+ </div>
73
+ </div>);
74
+ };
75
+ return (<div className={`flex ${isOwnMessage ? "justify-end" : "justify-start"} ${isGrouped ? "mt-0.5 sm:mt-1" : "mt-3 sm:mt-4"}`}>
76
+ <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" } : {}} // Allow promo messages to take full width if needed
77
+ >
78
+ {message.type !== "promo" && !isOwnMessage && !isGrouped && (<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"/>)}
79
+
80
+ {message.type !== "promo" && !isOwnMessage && isGrouped && (<div className="w-6 sm:w-8 flex-shrink-0"/> // Spacer for grouped messages
81
+ )}
82
+
83
+ <div className={`flex flex-col ${isOwnMessage ? "items-end" : "items-start"}`}>
84
+ {message.type !== "promo" && !isGrouped && !isOwnMessage && (<span className="text-xs text-gray-500 mb-1 px-3">{(sender === null || sender === void 0 ? void 0 : sender.name) || "Unknown User"}</span>)}
85
+
86
+ {message.type === "text" && renderTextMessage(message)}
87
+ {message.type === "media" && renderMediaMessage(message)}
88
+ {message.type === "promo" && renderPromoMessage(message)}
89
+
90
+ {message.type !== "promo" && (<div className={`flex items-center space-x-1 mt-1 px-2 ${isOwnMessage ? "flex-row-reverse space-x-reverse" : ""}`}>
91
+ {/* Status icon logic (from previous implementation) */}
92
+ {isOwnMessage && (<svg className="w-3 h-3 sm:w-4 sm:h-4 text-blue-500" fill="currentColor" viewBox="0 0 24 24">
93
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>
94
+ </svg>)}
95
+ </div>)}
96
+ </div>
97
+ </div>
98
+ </div>);
99
+ }
@@ -0,0 +1,2 @@
1
+ export declare function MessageItemDemo(): import("react").JSX.Element;
2
+ //# sourceMappingURL=MessageItemDemo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageItemDemo.d.ts","sourceRoot":"","sources":["../../src/components/MessageItemDemo.tsx"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,gCA2L9B"}
@@ -0,0 +1,179 @@
1
+ "use client";
2
+ import { MessageItem } from "./MessageItem";
3
+ import { DateDivider } from "./DateDivider";
4
+ export function MessageItemDemo() {
5
+ // Demo messages showcasing all message types
6
+ const demoMessages = [
7
+ // Text message from other user
8
+ {
9
+ id: "demo-1",
10
+ senderId: "user-2",
11
+ type: "text",
12
+ text: "Hey! How are you doing today? 😊",
13
+ createdAt: new Date(Date.now() - 3600000).toISOString(),
14
+ isMine: false,
15
+ },
16
+ // Text message from current user
17
+ {
18
+ id: "demo-2",
19
+ senderId: "current-user",
20
+ type: "text",
21
+ text: "I'm doing great! Thanks for asking. Check out this link: https://example.com",
22
+ createdAt: new Date(Date.now() - 3000000).toISOString(),
23
+ isMine: true,
24
+ },
25
+ // Media message with text and image from other user
26
+ {
27
+ id: "demo-3",
28
+ senderId: "user-2",
29
+ type: "media",
30
+ text: "Look at this beautiful sunset I captured! 📸",
31
+ attachments: [
32
+ {
33
+ id: "demo-3-1",
34
+ type: "image",
35
+ url: "/placeholder.svg?height=300&width=400",
36
+ name: "sunset.jpg",
37
+ size: 245760,
38
+ },
39
+ ],
40
+ createdAt: new Date(Date.now() - 2400000).toISOString(),
41
+ isMine: false,
42
+ },
43
+ // Media message with multiple images from current user
44
+ {
45
+ id: "demo-4",
46
+ senderId: "current-user",
47
+ type: "media",
48
+ text: "My vacation photos! 🏖️",
49
+ attachments: [
50
+ {
51
+ id: "demo-4-1",
52
+ type: "image",
53
+ url: "/placeholder.svg?height=200&width=300",
54
+ name: "beach1.jpg",
55
+ size: 180000,
56
+ },
57
+ {
58
+ id: "demo-4-2",
59
+ type: "image",
60
+ url: "/placeholder.svg?height=200&width=300",
61
+ name: "beach2.jpg",
62
+ size: 195000,
63
+ },
64
+ ],
65
+ createdAt: new Date(Date.now() - 1800000).toISOString(),
66
+ isMine: true,
67
+ },
68
+ // Media message with file from other user
69
+ {
70
+ id: "demo-5",
71
+ senderId: "user-2",
72
+ type: "media",
73
+ text: "Here's the document you requested",
74
+ attachments: [
75
+ {
76
+ id: "demo-5-1",
77
+ type: "file",
78
+ url: "/placeholder.svg?height=200&width=200",
79
+ name: "project-proposal.pdf",
80
+ size: 1024000,
81
+ },
82
+ ],
83
+ createdAt: new Date(Date.now() - 1200000).toISOString(),
84
+ isMine: false,
85
+ },
86
+ // Media message with mixed attachments from current user
87
+ {
88
+ id: "demo-6",
89
+ senderId: "current-user",
90
+ type: "media",
91
+ text: "Screenshot and the related document",
92
+ attachments: [
93
+ {
94
+ id: "demo-6-1",
95
+ type: "image",
96
+ url: "/placeholder.svg?height=400&width=300",
97
+ name: "screenshot.png",
98
+ size: 156000,
99
+ },
100
+ {
101
+ id: "demo-6-2",
102
+ type: "file",
103
+ url: "/placeholder.svg?height=200&width=200",
104
+ name: "instructions.docx",
105
+ size: 45000,
106
+ },
107
+ ],
108
+ createdAt: new Date(Date.now() - 600000).toISOString(),
109
+ isMine: true,
110
+ },
111
+ // Promotional message (system message)
112
+ {
113
+ id: "demo-7",
114
+ senderId: "system",
115
+ type: "promo",
116
+ promoData: {
117
+ imageUrl: "/placeholder.svg?height=200&width=400",
118
+ title: "Giảm 30% đơn hàng hôm nay!",
119
+ description: "Khuyến mãi đặc biệt chỉ trong hôm nay. Đừng bỏ lỡ cơ hội tiết kiệm!",
120
+ buttonText: "Đặt ngay",
121
+ buttonUrl: "https://example.com/sale",
122
+ },
123
+ createdAt: new Date(Date.now() - 300000).toISOString(),
124
+ isMine: false,
125
+ },
126
+ // Recent text message from other user
127
+ {
128
+ id: "demo-8",
129
+ senderId: "user-2",
130
+ type: "text",
131
+ text: "Thanks for sharing! Those photos are amazing 🤩",
132
+ createdAt: new Date(Date.now() - 60000).toISOString(),
133
+ isMine: false,
134
+ },
135
+ ];
136
+ const groupMessagesByDate = () => {
137
+ const groups = [];
138
+ let currentDate = "";
139
+ let currentGroup = [];
140
+ demoMessages.forEach((message) => {
141
+ const messageDate = new Date(message.createdAt).toDateString();
142
+ if (messageDate !== currentDate) {
143
+ if (currentGroup.length > 0) {
144
+ groups.push({ date: currentDate, messages: currentGroup });
145
+ }
146
+ currentDate = messageDate;
147
+ currentGroup = [message];
148
+ }
149
+ else {
150
+ currentGroup.push(message);
151
+ }
152
+ });
153
+ if (currentGroup.length > 0) {
154
+ groups.push({ date: currentDate, messages: currentGroup });
155
+ }
156
+ return groups;
157
+ };
158
+ const messageGroups = groupMessagesByDate();
159
+ return (<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
160
+ <div className="bg-gray-50 p-4 border-b">
161
+ <h2 className="text-lg font-semibold text-gray-900">MessageItem Demo</h2>
162
+ <p className="text-sm text-gray-600">Showcasing all message types: text, media, and promotional</p>
163
+ </div>
164
+
165
+ <div className="h-96 overflow-y-auto p-4 space-y-4">
166
+ {messageGroups.map((group, groupIndex) => (<div key={group.date}>
167
+ <DateDivider date={new Date(group.date)}/>
168
+ <div className="space-y-2">
169
+ {group.messages.map((message, messageIndex) => {
170
+ const prevMessage = messageIndex > 0 ? group.messages[messageIndex - 1] : null;
171
+ const isGrouped = (prevMessage === null || prevMessage === void 0 ? void 0 : prevMessage.senderId) === message.senderId &&
172
+ new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() < 300000; // 5 minutes
173
+ return <MessageItem key={message.id} message={message} isGrouped={isGrouped}/>;
174
+ })}
175
+ </div>
176
+ </div>))}
177
+ </div>
178
+ </div>);
179
+ }