@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,2 @@
1
+ export declare function AutoScrollAnchor(): import("react").JSX.Element;
2
+ //# sourceMappingURL=AutoScrollAnchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AutoScrollAnchor.d.ts","sourceRoot":"","sources":["../../src/components/AutoScrollAnchor.tsx"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,gCAU/B"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ export function AutoScrollAnchor() {
4
+ const anchorRef = useRef(null);
5
+ useEffect(() => {
6
+ if (anchorRef.current) {
7
+ anchorRef.current.scrollIntoView({ behavior: "smooth" });
8
+ }
9
+ });
10
+ return <div ref={anchorRef}/>;
11
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ChatBubble(): import("react").JSX.Element;
2
+ //# sourceMappingURL=ChatBubble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatBubble.d.ts","sourceRoot":"","sources":["../../src/components/ChatBubble.tsx"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,gCAsCzB"}
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { MessageCircle, X } from "lucide-react";
4
+ import { ChatLayout } from "./ChatLayout";
5
+ export function ChatBubble() {
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ return (<>
8
+ {/* Chat Bubble Button */}
9
+ <button onClick={() => setIsOpen(true)} className={`fixed bottom-6 right-6 w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center z-40 ${isOpen ? "scale-0" : "scale-100"}`} aria-label="Open chat">
10
+ <MessageCircle className="w-6 h-6"/>
11
+ </button>
12
+
13
+ {/* Chat Window */}
14
+ {isOpen && (<div className="fixed bottom-6 right-6 w-96 h-[500px] bg-white rounded-lg shadow-2xl border z-50 flex flex-col overflow-hidden">
15
+ {/* Header */}
16
+ <div className="flex items-center justify-between p-4 border-b bg-blue-600 text-white">
17
+ <h3 className="font-semibold">Chat Support</h3>
18
+ <button onClick={() => setIsOpen(false)} className="p-1 hover:bg-blue-700 rounded" aria-label="Close chat">
19
+ <X className="w-4 h-4"/>
20
+ </button>
21
+ </div>
22
+
23
+ {/* Chat Content */}
24
+ <div className="flex-1 overflow-hidden">
25
+ <ChatLayout />
26
+ </div>
27
+ </div>)}
28
+
29
+ {/* Backdrop */}
30
+ {isOpen && <div className="fixed inset-0 bg-black bg-opacity-20 z-30" onClick={() => setIsOpen(false)}/>}
31
+ </>);
32
+ }
@@ -0,0 +1,8 @@
1
+ interface ChatHeaderProps {
2
+ conversationId: string;
3
+ onBackClick?: () => void;
4
+ onMenuClick?: () => void;
5
+ }
6
+ export declare function ChatHeader({ conversationId, onBackClick, onMenuClick }: ChatHeaderProps): import("react").JSX.Element | null;
7
+ export {};
8
+ //# sourceMappingURL=ChatHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatHeader.d.ts","sourceRoot":"","sources":["../../src/components/ChatHeader.tsx"],"names":[],"mappings":"AAGA,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,wBAAgB,UAAU,CAAC,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,EAAE,eAAe,sCAgGvF"}
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { useChatContext } from "../context/ChatContext";
3
+ export function ChatHeader({ conversationId, onBackClick, onMenuClick }) {
4
+ const { state } = useChatContext();
5
+ const conversation = state.conversations.find((c) => c.id === conversationId);
6
+ const otherParticipant = conversation === null || conversation === void 0 ? void 0 : conversation.participants.find((p) => { var _a; return p.id !== ((_a = state.config) === null || _a === void 0 ? void 0 : _a.userId); });
7
+ if (!conversation || !otherParticipant) {
8
+ return null;
9
+ }
10
+ const getStatusText = () => {
11
+ if (otherParticipant.status === "online") {
12
+ return "Online";
13
+ }
14
+ else if (otherParticipant.lastSeen) {
15
+ const now = new Date();
16
+ const diff = now.getTime() - otherParticipant.lastSeen.getTime();
17
+ const minutes = Math.floor(diff / (1000 * 60));
18
+ const hours = Math.floor(minutes / 60);
19
+ const days = Math.floor(hours / 24);
20
+ if (minutes < 1)
21
+ return "Last seen just now";
22
+ if (minutes < 60)
23
+ return `Last seen ${minutes}m ago`;
24
+ if (hours < 24)
25
+ return `Last seen ${hours}h ago`;
26
+ return `Last seen ${days}d ago`;
27
+ }
28
+ return "Offline";
29
+ };
30
+ return (<div className="flex items-center p-3 sm:p-4 border-b border-gray-200 bg-white relative">
31
+ {/* Mobile back button with enhanced touch area */}
32
+ <button onClick={onBackClick} className="md:hidden p-3 mr-1 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 -ml-3 transition-colors" aria-label="Go back to conversations">
33
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7"/>
35
+ </svg>
36
+ </button>
37
+
38
+ <div className="relative">
39
+ <img src={otherParticipant.avatar || "/placeholder.svg?height=40&width=40&query=user"} alt={otherParticipant.name} className="w-8 h-8 sm:w-10 sm:h-10 rounded-full object-cover"/>
40
+ {otherParticipant.status === "online" && (<div className="absolute bottom-0 right-0 w-2.5 h-2.5 sm:w-3 sm:h-3 bg-green-500 rounded-full border-2 border-white"></div>)}
41
+ </div>
42
+
43
+ <div className="ml-3 flex-1 min-w-0">
44
+ <h2 className="text-base sm:text-lg font-semibold text-gray-900 truncate">{otherParticipant.name}</h2>
45
+ <p className="text-xs sm:text-sm text-gray-500 truncate">{getStatusText()}</p>
46
+ </div>
47
+
48
+ {/* Swipe hint for first-time users */}
49
+ <div className="md:hidden absolute top-full left-0 right-0 bg-blue-50 border-b border-blue-100 px-4 py-2 text-xs text-blue-600 text-center animate-pulse swipe-tutorial">
50
+ 💡 Tip: Swipe right anywhere to go back
51
+ </div>
52
+
53
+ <div className="flex items-center space-x-1 sm:space-x-2">
54
+ <button className="p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition-colors">
55
+ <svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
57
+ </svg>
58
+ </button>
59
+ <button className="p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition-colors">
60
+ <svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
62
+ </svg>
63
+ </button>
64
+ {/* Mobile menu button */}
65
+ <button onClick={onMenuClick} className="md:hidden p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition-colors" aria-label="Open menu">
66
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16"/>
68
+ </svg>
69
+ </button>
70
+ </div>
71
+ </div>);
72
+ }
@@ -0,0 +1,4 @@
1
+ import type React from "react";
2
+ import type { ChatInputProps } from "../types/chat";
3
+ export declare function ChatInput({ conversationId, onSendMessage, onEmojiClick, onStickerClick, onFileUpload, onImageUpload, onContactShare, onVoiceRecord, onVoiceMessage, onQuickReact, placeholder, disabled, className, }: ChatInputProps): React.JSX.Element;
4
+ //# sourceMappingURL=ChatInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInput.d.ts","sourceRoot":"","sources":["../../src/components/ChatInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,wBAAgB,SAAS,CAAC,EACxB,cAAc,EACd,aAAa,EACb,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,WAA6B,EAC7B,QAAgB,EAChB,SAAc,GACf,EAAE,cAAc,qBA4lBhB"}
@@ -0,0 +1,444 @@
1
+ "use client";
2
+ import { useState, useCallback, useEffect, useRef } from "react";
3
+ import { Smile, ImageIcon, Paperclip, Mic, Send, Type, Sticker, User, X, Loader2, FileText } from "lucide-react"; // Import FileText icon
4
+ import { useChat } from "../hooks/useChat";
5
+ import { useTyping } from "../hooks/useTyping";
6
+ import { useTextSelection } from "../hooks/useTextSelection";
7
+ import { TextFormattingToolbar } from "./TextFormattingToolbar";
8
+ import { EmojiPicker } from "./EmojiPicker";
9
+ import { StickerPicker } from "./StickerPicker";
10
+ export function ChatInput({ conversationId, onSendMessage, onEmojiClick, onStickerClick, onFileUpload, onImageUpload, onContactShare, onVoiceRecord, onVoiceMessage, onQuickReact, placeholder = "Nhập tin nhắn", disabled = false, className = "", }) {
11
+ const [message, setMessage] = useState("");
12
+ const [isRecording, setIsRecording] = useState(false);
13
+ const [showVoiceWave, setShowVoiceWave] = useState(false);
14
+ const [showTextToolbar, setShowTextToolbar] = useState(false);
15
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
16
+ const [showStickerPicker, setShowStickerPicker] = useState(false);
17
+ const [selectedFormats, setSelectedFormats] = useState([]);
18
+ const [selectedFiles, setSelectedFiles] = useState([]); // State for selected files
19
+ const [isUploading, setIsUploading] = useState(false); // State for uploading status
20
+ const fileInputRef = useRef(null);
21
+ const imageInputRef = useRef(null);
22
+ // Refs for popups
23
+ const textToolbarRef = useRef(null);
24
+ const emojiPickerRef = useRef(null);
25
+ const stickerPickerRef = useRef(null);
26
+ // Refs for toggle buttons
27
+ const textButtonRef = useRef(null);
28
+ const emojiButtonRef = useRef(null);
29
+ const stickerButtonRef = useRef(null);
30
+ // Ref for the relative container that wraps the popups
31
+ const popupWrapperRef = useRef(null);
32
+ // State for dynamic popup styles
33
+ const [textToolbarStyle, setTextToolbarStyle] = useState({});
34
+ const [emojiPickerStyle, setEmojiPickerStyle] = useState({});
35
+ const [stickerPickerStyle, setStickerPickerStyle] = useState({});
36
+ const { sendMessage: sendChatMessage } = useChat(conversationId || "");
37
+ const { startTyping, stopTyping } = useTyping(conversationId || "");
38
+ const { textareaRef, applyFormat } = useTextSelection();
39
+ const handleSubmit = useCallback(async (e) => {
40
+ e.preventDefault();
41
+ if (disabled || isUploading)
42
+ return;
43
+ if (selectedFiles.length > 0) {
44
+ setIsUploading(true);
45
+ // Simulate file upload
46
+ await Promise.all(selectedFiles.map(async (file) => {
47
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate network delay
48
+ if (file.type.startsWith("image/")) {
49
+ onImageUpload === null || onImageUpload === void 0 ? void 0 : onImageUpload(file);
50
+ }
51
+ else {
52
+ onFileUpload === null || onFileUpload === void 0 ? void 0 : onFileUpload(file);
53
+ }
54
+ }));
55
+ setSelectedFiles([]);
56
+ setIsUploading(false);
57
+ }
58
+ if (message.trim()) {
59
+ // Use integrated chat system if available, otherwise use callback
60
+ if (sendChatMessage) {
61
+ sendChatMessage(message.trim());
62
+ }
63
+ else {
64
+ onSendMessage === null || onSendMessage === void 0 ? void 0 : onSendMessage(message.trim());
65
+ }
66
+ setMessage("");
67
+ setSelectedFormats([]);
68
+ stopTyping();
69
+ // Reset textarea height
70
+ if (textareaRef.current) {
71
+ textareaRef.current.style.height = "auto";
72
+ }
73
+ }
74
+ }, [
75
+ message,
76
+ disabled,
77
+ isUploading,
78
+ selectedFiles,
79
+ sendChatMessage,
80
+ onSendMessage,
81
+ onFileUpload,
82
+ onImageUpload,
83
+ stopTyping,
84
+ textareaRef,
85
+ ]);
86
+ const handleInputChange = useCallback((e) => {
87
+ const value = e.target.value;
88
+ setMessage(value);
89
+ // Trigger typing indicator
90
+ if (value.trim()) {
91
+ startTyping();
92
+ }
93
+ else {
94
+ stopTyping();
95
+ }
96
+ // Auto-resize textarea
97
+ const textarea = e.target;
98
+ textarea.style.height = "auto";
99
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
100
+ }, [startTyping, stopTyping]);
101
+ const handleKeyPress = useCallback((e) => {
102
+ if (e.key === "Enter" && !e.shiftKey) {
103
+ e.preventDefault();
104
+ handleSubmit(e);
105
+ }
106
+ // Handle keyboard shortcuts
107
+ if (e.ctrlKey || e.metaKey) {
108
+ switch (e.key) {
109
+ case "b":
110
+ e.preventDefault();
111
+ applyFormat("bold");
112
+ break;
113
+ case "i":
114
+ e.preventDefault();
115
+ applyFormat("italic");
116
+ break;
117
+ case "e":
118
+ e.preventDefault();
119
+ applyFormat("code");
120
+ break;
121
+ case "k":
122
+ e.preventDefault();
123
+ applyFormat("link");
124
+ break;
125
+ }
126
+ }
127
+ }, [handleSubmit, applyFormat]);
128
+ const handleFileClick = useCallback(() => {
129
+ var _a;
130
+ (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
131
+ }, []);
132
+ const handleImageClick = useCallback(() => {
133
+ var _a;
134
+ (_a = imageInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
135
+ }, []);
136
+ const handleFileChange = useCallback((e) => {
137
+ const files = Array.from(e.target.files || []);
138
+ if (files.length > 0) {
139
+ setSelectedFiles((prev) => [...prev, ...files]);
140
+ e.target.value = ""; // Reset input to allow selecting same file again
141
+ }
142
+ }, []);
143
+ const handleImageChange = useCallback((e) => {
144
+ const files = Array.from(e.target.files || []);
145
+ if (files.length > 0) {
146
+ setSelectedFiles((prev) => [...prev, ...files]);
147
+ e.target.value = ""; // Reset input to allow selecting same file again
148
+ }
149
+ }, []);
150
+ const handleRemoveFile = useCallback((fileToRemove) => {
151
+ setSelectedFiles((prev) => prev.filter((file) => file !== fileToRemove));
152
+ }, []);
153
+ const handleVoiceRecord = useCallback(() => {
154
+ setIsRecording(!isRecording);
155
+ onVoiceRecord === null || onVoiceRecord === void 0 ? void 0 : onVoiceRecord();
156
+ }, [isRecording, onVoiceRecord]);
157
+ const handleTextToolbarToggle = useCallback(() => {
158
+ setShowTextToolbar((prev) => !prev);
159
+ setShowEmojiPicker(false);
160
+ setShowStickerPicker(false);
161
+ }, []);
162
+ const handleEmojiToggle = useCallback(() => {
163
+ setShowEmojiPicker((prev) => !prev);
164
+ setShowTextToolbar(false);
165
+ setShowStickerPicker(false);
166
+ }, []);
167
+ const handleStickerToggle = useCallback(() => {
168
+ setShowStickerPicker((prev) => !prev);
169
+ setShowTextToolbar(false);
170
+ setShowEmojiPicker(false);
171
+ }, []);
172
+ const handleFormatSelect = useCallback((format) => {
173
+ applyFormat(format);
174
+ setSelectedFormats((prev) => {
175
+ if (prev.includes(format)) {
176
+ return prev.filter((f) => f !== format);
177
+ }
178
+ else {
179
+ return [...prev, format];
180
+ }
181
+ });
182
+ }, [applyFormat]);
183
+ const handleEmojiSelect = useCallback((emoji) => {
184
+ const textarea = textareaRef.current;
185
+ if (textarea) {
186
+ const start = textarea.selectionStart;
187
+ const end = textarea.selectionEnd;
188
+ const newMessage = message.slice(0, start) + emoji + message.slice(end);
189
+ setMessage(newMessage);
190
+ setTimeout(() => {
191
+ textarea.selectionStart = textarea.selectionEnd = start + emoji.length;
192
+ textarea.focus();
193
+ }, 0);
194
+ }
195
+ onEmojiClick === null || onEmojiClick === void 0 ? void 0 : onEmojiClick(emoji);
196
+ }, [message, onEmojiClick, textareaRef]);
197
+ const handleStickerSelect = useCallback((sticker) => {
198
+ // Send sticker immediately (like a message)
199
+ if (sendChatMessage) {
200
+ sendChatMessage(sticker);
201
+ }
202
+ else {
203
+ onSendMessage === null || onSendMessage === void 0 ? void 0 : onSendMessage(sticker);
204
+ }
205
+ onStickerClick === null || onStickerClick === void 0 ? void 0 : onStickerClick(sticker);
206
+ setShowStickerPicker(false); // Close picker after selection
207
+ }, [sendChatMessage, onSendMessage, onStickerClick]);
208
+ // Click outside logic
209
+ useEffect(() => {
210
+ const handleClickOutside = (event) => {
211
+ const target = event.target;
212
+ // Text Formatting Toolbar
213
+ if (showTextToolbar &&
214
+ textToolbarRef.current &&
215
+ !textToolbarRef.current.contains(target) &&
216
+ textButtonRef.current &&
217
+ !textButtonRef.current.contains(target)) {
218
+ setShowTextToolbar(false);
219
+ }
220
+ // Emoji Picker
221
+ if (showEmojiPicker &&
222
+ emojiPickerRef.current &&
223
+ !emojiPickerRef.current.contains(target) &&
224
+ emojiButtonRef.current &&
225
+ !emojiButtonRef.current.contains(target)) {
226
+ setShowEmojiPicker(false);
227
+ }
228
+ // Sticker Picker
229
+ if (showStickerPicker &&
230
+ stickerPickerRef.current &&
231
+ !stickerPickerRef.current.contains(target) &&
232
+ stickerButtonRef.current &&
233
+ !stickerButtonRef.current.contains(target)) {
234
+ setShowStickerPicker(false);
235
+ }
236
+ };
237
+ document.addEventListener("mousedown", handleClickOutside);
238
+ return () => {
239
+ document.removeEventListener("mousedown", handleClickOutside);
240
+ };
241
+ }, [showTextToolbar, showEmojiPicker, showStickerPicker]);
242
+ // Dynamic positioning logic for popups
243
+ const calculatePopupPosition = useCallback((buttonRef, popupRef, setPopupStyle) => {
244
+ const wrapper = popupWrapperRef.current;
245
+ const button = buttonRef.current;
246
+ const popup = popupRef.current;
247
+ if (!wrapper || !button || !popup)
248
+ return;
249
+ const wrapperRect = wrapper.getBoundingClientRect();
250
+ const buttonRect = button.getBoundingClientRect();
251
+ const popupRect = popup.getBoundingClientRect();
252
+ const viewportWidth = window.innerWidth;
253
+ let desiredLeft = buttonRect.left - wrapperRect.left;
254
+ // Add a small margin (e.g., 8px) from the right edge of the viewport
255
+ const rightViewportMargin = 8;
256
+ if (wrapperRect.left + desiredLeft + popupRect.width > viewportWidth - rightViewportMargin) {
257
+ desiredLeft = viewportWidth - wrapperRect.left - popupRect.width - rightViewportMargin;
258
+ // Ensure it doesn't go off the left edge (e.g., 8px margin from left)
259
+ desiredLeft = Math.max(8, desiredLeft);
260
+ }
261
+ setPopupStyle({ left: `${desiredLeft}px` });
262
+ }, []);
263
+ useEffect(() => {
264
+ if (showTextToolbar) {
265
+ calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle);
266
+ window.addEventListener("resize", () => calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle));
267
+ }
268
+ else {
269
+ setTextToolbarStyle({}); // Reset style when closed
270
+ }
271
+ return () => window.removeEventListener("resize", () => calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle));
272
+ }, [showTextToolbar, calculatePopupPosition]);
273
+ useEffect(() => {
274
+ if (showEmojiPicker) {
275
+ calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle);
276
+ window.addEventListener("resize", () => calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle));
277
+ }
278
+ else {
279
+ setEmojiPickerStyle({}); // Reset style when closed
280
+ }
281
+ return () => window.removeEventListener("resize", () => calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle));
282
+ }, [showEmojiPicker, calculatePopupPosition]);
283
+ useEffect(() => {
284
+ if (showStickerPicker) {
285
+ calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle);
286
+ window.addEventListener("resize", () => calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle));
287
+ }
288
+ else {
289
+ setStickerPickerStyle({}); // Reset style when closed
290
+ }
291
+ return () => window.removeEventListener("resize", () => calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle));
292
+ }, [showStickerPicker, calculatePopupPosition]);
293
+ // Cleanup object URLs when selectedFiles change or component unmounts
294
+ useEffect(() => {
295
+ const objectUrls = [];
296
+ selectedFiles.forEach((file) => {
297
+ if (file.type.startsWith("image/")) {
298
+ objectUrls.push(URL.createObjectURL(file));
299
+ }
300
+ });
301
+ return () => {
302
+ objectUrls.forEach((url) => URL.revokeObjectURL(url));
303
+ };
304
+ }, [selectedFiles]);
305
+ // Small, left-aligned action buttons
306
+ const actionButtons = [
307
+ {
308
+ icon: Type,
309
+ label: "Định dạng văn bản",
310
+ onClick: handleTextToolbarToggle,
311
+ color: showTextToolbar ? "text-blue-500 bg-blue-50" : "text-gray-500 hover:text-blue-500",
312
+ active: showTextToolbar,
313
+ ref: textButtonRef,
314
+ },
315
+ {
316
+ icon: Smile,
317
+ label: "Emoji",
318
+ onClick: handleEmojiToggle,
319
+ color: showEmojiPicker ? "text-yellow-500 bg-yellow-50" : "text-gray-500 hover:text-yellow-500",
320
+ active: showEmojiPicker,
321
+ ref: emojiButtonRef,
322
+ },
323
+ {
324
+ icon: Sticker,
325
+ label: "Sticker",
326
+ onClick: handleStickerToggle,
327
+ color: showStickerPicker ? "text-purple-500 bg-purple-50" : "text-gray-500 hover:text-purple-500",
328
+ active: showStickerPicker,
329
+ ref: stickerButtonRef,
330
+ },
331
+ {
332
+ icon: ImageIcon,
333
+ label: "Hình ảnh",
334
+ onClick: handleImageClick,
335
+ color: "text-gray-500 hover:text-green-500",
336
+ },
337
+ {
338
+ icon: Paperclip,
339
+ label: "Tệp đính kèm",
340
+ onClick: handleFileClick,
341
+ color: "text-gray-500 hover:text-blue-500",
342
+ },
343
+ {
344
+ icon: User,
345
+ label: "Chia sẻ liên hệ",
346
+ onClick: onContactShare,
347
+ color: "text-gray-500 hover:text-indigo-500",
348
+ },
349
+ {
350
+ icon: Mic,
351
+ label: "Ghi âm",
352
+ onClick: handleVoiceRecord,
353
+ color: isRecording ? "text-red-500 bg-red-50" : "text-gray-500 hover:text-red-500",
354
+ active: isRecording,
355
+ },
356
+ ];
357
+ const isSendButtonDisabled = disabled || isUploading || (message.trim() === "" && selectedFiles.length === 0);
358
+ return (<div className={`bg-white border-t border-gray-200 p-3 sm:p-4 ${className}`}>
359
+ {/* Toolbars */}
360
+ <div className="relative" ref={popupWrapperRef}>
361
+ <TextFormattingToolbar ref={textToolbarRef} isOpen={showTextToolbar} onClose={() => setShowTextToolbar(false)} onFormatSelect={handleFormatSelect} selectedFormats={selectedFormats} style={textToolbarStyle}/>
362
+ <EmojiPicker ref={emojiPickerRef} isOpen={showEmojiPicker} onEmojiSelect={handleEmojiSelect} onClose={() => setShowEmojiPicker(false)} style={emojiPickerStyle}/>
363
+ <StickerPicker ref={stickerPickerRef} isOpen={showStickerPicker} onStickerSelect={handleStickerSelect} onClose={() => setShowStickerPicker(false)} style={stickerPickerStyle}/>
364
+ </div>
365
+
366
+ {/* Main Input Container */}
367
+ <div className="bg-gray-50 rounded-2xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200">
368
+ {/* Selected Files Preview */}
369
+ {selectedFiles.length > 0 && (<div className="px-4 pt-3 pb-2 border-b border-gray-100 flex flex-wrap gap-2">
370
+ {selectedFiles.map((file, index) => {
371
+ const isImage = file.type.startsWith("image/");
372
+ const fileUrl = isImage ? URL.createObjectURL(file) : null;
373
+ return (<div key={index} className="flex items-center bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-1 rounded-lg relative overflow-hidden">
374
+ {isImage ? (<img src={fileUrl || ""} alt={file.name} className="h-12 w-12 object-cover rounded-md mr-2"/>) : (<div className="flex items-center space-x-1">
375
+ <FileText className="w-4 h-4 text-blue-600"/> {/* File icon */}
376
+ <span className="truncate max-w-[100px] sm:max-w-[150px]">{file.name}</span>
377
+ </div>)}
378
+ <button type="button" onClick={() => handleRemoveFile(file)} className="ml-1 p-0.5 rounded-full hover:bg-blue-200 text-blue-600 absolute top-0.5 right-0.5 bg-white/70 backdrop-blur-sm" aria-label={`Remove ${file.name}`}>
379
+ <X className="w-3 h-3"/>
380
+ </button>
381
+ </div>);
382
+ })}
383
+ </div>)}
384
+
385
+ {/* Input Field */}
386
+ <div className="px-4 py-3">
387
+ <form onSubmit={handleSubmit} className="flex items-end space-x-3">
388
+ <div className="flex-1 relative">
389
+ <textarea ref={textareaRef} value={message} onChange={handleInputChange} onKeyDown={handleKeyPress} placeholder={placeholder} disabled={disabled || isUploading} className="w-full bg-transparent border-none outline-none resize-none text-sm sm:text-base text-gray-900 placeholder-gray-500 leading-5" rows={1} style={{
390
+ minHeight: "20px",
391
+ maxHeight: "120px",
392
+ }}/>
393
+ </div>
394
+
395
+ {/* Send Button or Quick React */}
396
+ <button type="submit" disabled={isSendButtonDisabled} className="flex-shrink-0 p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200" aria-label={isUploading ? "Đang tải lên" : "Gửi tin nhắn"}>
397
+ {isUploading ? <Loader2 className="w-4 h-4 animate-spin"/> : <Send className="w-4 h-4"/>}
398
+ </button>
399
+ </form>
400
+ </div>
401
+
402
+ {/* Action Icons Row - Small, Left Aligned */}
403
+ <div className="px-4 pb-3 border-t border-gray-100">
404
+ <div className="flex items-center space-x-2 justify-start">
405
+ {actionButtons.map((button, index) => {
406
+ const Icon = button.icon;
407
+ return (<button key={index} ref={button.ref} // Assign ref here
408
+ onClick={button.onClick} disabled={disabled || isUploading} className={`
409
+ p-1.5 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed
410
+ hover:bg-gray-100 active:scale-95
411
+ ${button.color}
412
+ `} aria-label={button.label} title={button.label}>
413
+ <Icon className="w-4 h-4"/>
414
+ </button>);
415
+ })}
416
+ </div>
417
+ </div>
418
+
419
+ {/* Recording Indicator */}
420
+ {isRecording && (<div className="px-4 pb-2">
421
+ <div className="flex items-center space-x-2 text-red-500 text-sm">
422
+ <div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
423
+ <span>Đang ghi âm...</span>
424
+ </div>
425
+ </div>)}
426
+
427
+ {/* Voice Wave Indicator */}
428
+ {showVoiceWave && (<div className="px-4 pb-2">
429
+ <div className="flex items-center space-x-1">
430
+ {[...Array(8)].map((_, i) => (<div key={i} className="w-1 bg-blue-500 rounded-full animate-pulse" style={{
431
+ height: `${Math.random() * 20 + 10}px`,
432
+ animationDelay: `${i * 0.1}s`,
433
+ }}/>))}
434
+ </div>
435
+ </div>)}
436
+ </div>
437
+
438
+ {/* Hidden File Inputs */}
439
+ <input ref={fileInputRef} type="file" onChange={handleFileChange} className="hidden" accept=".pdf,.doc,.docx,.txt,.zip,.rar" multiple // Allow multiple file selection
440
+ />
441
+ <input ref={imageInputRef} type="file" onChange={handleImageChange} className="hidden" accept="image/*" multiple // Allow multiple image selection
442
+ />
443
+ </div>);
444
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ChatInputDemo(): import("react").JSX.Element;
2
+ //# sourceMappingURL=ChatInputDemo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInputDemo.d.ts","sourceRoot":"","sources":["../../src/components/ChatInputDemo.tsx"],"names":[],"mappings":"AAKA,wBAAgB,aAAa,gCA0E5B"}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { ChatInput } from "./ChatInput";
4
+ export function ChatInputDemo() {
5
+ const [messages, setMessages] = useState([]);
6
+ const handleSendMessage = (message) => {
7
+ setMessages((prev) => [...prev, message]);
8
+ console.log("Message sent:", message);
9
+ };
10
+ const handleEmojiClick = (emoji) => {
11
+ console.log("Emoji clicked:", emoji);
12
+ };
13
+ const handleStickerClick = (sticker) => {
14
+ console.log("Sticker clicked:", sticker);
15
+ setMessages((prev) => [...prev, `Sticker: ${sticker}`]);
16
+ };
17
+ const handleFileUpload = (file) => {
18
+ console.log("File uploaded:", file.name);
19
+ setMessages((prev) => [...prev, `📎 File: ${file.name}`]);
20
+ };
21
+ const handleImageUpload = (file) => {
22
+ console.log("Image uploaded:", file.name);
23
+ setMessages((prev) => [...prev, `🖼️ Image: ${file.name}`]);
24
+ };
25
+ const handleContactShare = () => {
26
+ console.log("Contact share clicked");
27
+ setMessages((prev) => [...prev, "👤 Contact shared"]);
28
+ };
29
+ const handleVoiceRecord = () => {
30
+ console.log("Voice record clicked");
31
+ };
32
+ const handleQuickReact = () => {
33
+ console.log("Quick react clicked");
34
+ setMessages((prev) => [...prev, "👍"]);
35
+ };
36
+ return (<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
37
+ {/* Mock Chat Messages */}
38
+ <div className="h-96 p-4 bg-gray-50 overflow-y-auto">
39
+ <div className="space-y-3">
40
+ <div className="text-center text-gray-500 text-sm mb-4">Demo Chat Interface</div>
41
+ {messages.map((message, index) => (<div key={index} className="flex justify-end">
42
+ <div className="bg-blue-500 text-white px-4 py-2 rounded-2xl rounded-tr-md max-w-xs">{message}</div>
43
+ </div>))}
44
+ {messages.length === 0 && (<div className="text-center text-gray-400 text-sm">
45
+ Type a message, select emoji, or send sticker to test the ChatInput component
46
+ </div>)}
47
+ </div>
48
+ </div>
49
+
50
+ {/* Chat Input */}
51
+ <ChatInput onSendMessage={handleSendMessage} onEmojiClick={handleEmojiClick} onStickerClick={handleStickerClick} onFileUpload={handleFileUpload} onImageUpload={handleImageUpload} onContactShare={handleContactShare} onVoiceRecord={handleVoiceRecord} onQuickReact={handleQuickReact} placeholder="Nhập tin nhắn"/>
52
+ </div>);
53
+ }