@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.
- package/dist/assets/droppiiFontSelection.json +14521 -0
- package/dist/components/ChatBubble.d.ts +9 -1
- package/dist/components/ChatBubble.d.ts.map +1 -1
- package/dist/components/ChatBubble.js +23 -15
- package/dist/components/chat-bubble/ChatBubble.d.ts +9 -0
- package/dist/components/chat-bubble/ChatBubble.d.ts.map +1 -0
- package/dist/components/chat-bubble/ChatBubble.js +27 -0
- package/dist/components/conversation/DeskConversationList.d.ts +8 -0
- package/dist/components/conversation/DeskConversationList.d.ts.map +1 -0
- package/dist/components/conversation/DeskConversationList.js +168 -0
- package/dist/components/icon/index.d.ts +11 -0
- package/dist/components/icon/index.d.ts.map +1 -0
- package/dist/components/icon/index.js +18 -0
- package/dist/components/message/MessageList.d.ts +10 -0
- package/dist/components/message/MessageList.d.ts.map +1 -0
- package/dist/components/message/MessageList.js +91 -0
- package/dist/components/session/AssignedSessionFilter.d.ts +7 -0
- package/dist/components/session/AssignedSessionFilter.d.ts.map +1 -0
- package/dist/components/session/AssignedSessionFilter.js +90 -0
- package/dist/context/ChatContext.d.ts +4 -71
- package/dist/context/ChatContext.d.ts.map +1 -1
- package/dist/context/ChatContext.js +33 -344
- package/dist/hooks/conversation/useConversation.d.ts +11 -0
- package/dist/hooks/conversation/useConversation.d.ts.map +1 -0
- package/dist/hooks/conversation/useConversation.js +51 -0
- package/dist/hooks/message/useMessage.d.ts +9 -0
- package/dist/hooks/message/useMessage.d.ts.map +1 -0
- package/dist/hooks/message/useMessage.js +46 -0
- package/dist/hooks/message/useSendMessage.d.ts +10 -0
- package/dist/hooks/message/useSendMessage.d.ts.map +1 -0
- package/dist/hooks/message/useSendMessage.js +42 -0
- package/dist/index.d.ts +9 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -27
- package/dist/screens/desk-message/index.d.ts +3 -0
- package/dist/screens/desk-message/index.d.ts.map +1 -0
- package/dist/screens/desk-message/index.js +14 -0
- package/dist/types/chat.d.ts +6 -36
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -85
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/sdk.d.ts.map +1 -0
- package/dist/types/sdk.js +1 -0
- package/package.json +19 -3
- package/dist/components/AutoScrollAnchor.d.ts +0 -2
- package/dist/components/AutoScrollAnchor.d.ts.map +0 -1
- package/dist/components/AutoScrollAnchor.js +0 -12
- package/dist/components/AutoScrollAnchor.jsx +0 -11
- package/dist/components/ChatBubble.jsx +0 -80
- package/dist/components/ChatHeader.d.ts +0 -8
- package/dist/components/ChatHeader.d.ts.map +0 -1
- package/dist/components/ChatHeader.js +0 -32
- package/dist/components/ChatHeader.jsx +0 -72
- package/dist/components/ChatInput.d.ts +0 -3
- package/dist/components/ChatInput.d.ts.map +0 -1
- package/dist/components/ChatInput.js +0 -379
- package/dist/components/ChatInput.jsx +0 -444
- package/dist/components/ChatInputDemo.d.ts +0 -2
- package/dist/components/ChatInputDemo.d.ts.map +0 -1
- package/dist/components/ChatInputDemo.js +0 -38
- package/dist/components/ChatInputDemo.jsx +0 -53
- package/dist/components/ChatInputWithCustomIcon.d.ts +0 -16
- package/dist/components/ChatInputWithCustomIcon.d.ts.map +0 -1
- package/dist/components/ChatInputWithCustomIcon.js +0 -85
- package/dist/components/ChatInputWithCustomIcon.jsx +0 -167
- package/dist/components/ChatLayout.d.ts +0 -6
- package/dist/components/ChatLayout.d.ts.map +0 -1
- package/dist/components/ChatLayout.js +0 -48
- package/dist/components/ChatLayout.jsx +0 -122
- package/dist/components/ConversationItem.d.ts +0 -9
- package/dist/components/ConversationItem.d.ts.map +0 -1
- package/dist/components/ConversationItem.js +0 -27
- package/dist/components/ConversationItem.jsx +0 -51
- package/dist/components/ConversationList.d.ts +0 -8
- package/dist/components/ConversationList.d.ts.map +0 -1
- package/dist/components/ConversationList.js +0 -11
- package/dist/components/ConversationList.jsx +0 -22
- package/dist/components/DateDivider.d.ts +0 -7
- package/dist/components/DateDivider.d.ts.map +0 -1
- package/dist/components/DateDivider.js +0 -27
- package/dist/components/DateDivider.jsx +0 -28
- package/dist/components/EmojiPicker.d.ts +0 -4
- package/dist/components/EmojiPicker.d.ts.map +0 -1
- package/dist/components/EmojiPicker.js +0 -191
- package/dist/components/EmojiPicker.jsx +0 -229
- package/dist/components/ImageLightbox.d.ts +0 -8
- package/dist/components/ImageLightbox.d.ts.map +0 -1
- package/dist/components/ImageLightbox.js +0 -8
- package/dist/components/ImageLightbox.jsx +0 -16
- package/dist/components/ImagePreviewModal.d.ts +0 -12
- package/dist/components/ImagePreviewModal.d.ts.map +0 -1
- package/dist/components/ImagePreviewModal.js +0 -55
- package/dist/components/ImagePreviewModal.jsx +0 -84
- package/dist/components/MessageItem.d.ts +0 -3
- package/dist/components/MessageItem.d.ts.map +0 -1
- package/dist/components/MessageItem.js +0 -38
- package/dist/components/MessageItem.jsx +0 -99
- package/dist/components/MessageItemDemo.d.ts +0 -2
- package/dist/components/MessageItemDemo.d.ts.map +0 -1
- package/dist/components/MessageItemDemo.js +0 -166
- package/dist/components/MessageItemDemo.jsx +0 -179
- package/dist/components/MessageList.d.ts +0 -15
- package/dist/components/MessageList.d.ts.map +0 -1
- package/dist/components/MessageList.js +0 -243
- package/dist/components/MessageList.jsx +0 -306
- package/dist/components/MessageListDemo.d.ts +0 -2
- package/dist/components/MessageListDemo.d.ts.map +0 -1
- package/dist/components/MessageListDemo.js +0 -165
- package/dist/components/MessageListDemo.jsx +0 -183
- package/dist/components/StickerPicker.d.ts +0 -4
- package/dist/components/StickerPicker.d.ts.map +0 -1
- package/dist/components/StickerPicker.js +0 -68
- package/dist/components/StickerPicker.jsx +0 -106
- package/dist/components/SwipeIndicator.d.ts +0 -9
- package/dist/components/SwipeIndicator.d.ts.map +0 -1
- package/dist/components/SwipeIndicator.js +0 -24
- package/dist/components/SwipeIndicator.jsx +0 -28
- package/dist/components/TextFormattingToolbar.d.ts +0 -4
- package/dist/components/TextFormattingToolbar.d.ts.map +0 -1
- package/dist/components/TextFormattingToolbar.js +0 -29
- package/dist/components/TextFormattingToolbar.jsx +0 -52
- package/dist/components/TypingIndicator.d.ts +0 -6
- package/dist/components/TypingIndicator.d.ts.map +0 -1
- package/dist/components/TypingIndicator.js +0 -21
- package/dist/components/TypingIndicator.jsx +0 -27
- package/dist/components/VoiceWaveIcon.d.ts +0 -7
- package/dist/components/VoiceWaveIcon.d.ts.map +0 -1
- package/dist/components/VoiceWaveIcon.js +0 -5
- package/dist/components/VoiceWaveIcon.jsx +0 -11
- package/dist/context/ChatContext.jsx +0 -346
- package/dist/hooks/useChat.d.ts +0 -5
- package/dist/hooks/useChat.d.ts.map +0 -1
- package/dist/hooks/useChat.js +0 -73
- package/dist/hooks/useConversationList.d.ts +0 -5
- package/dist/hooks/useConversationList.d.ts.map +0 -1
- package/dist/hooks/useConversationList.js +0 -9
- package/dist/hooks/useMessages.d.ts +0 -5
- package/dist/hooks/useMessages.d.ts.map +0 -1
- package/dist/hooks/useMessages.js +0 -192
- package/dist/hooks/useSocket.d.ts +0 -7
- package/dist/hooks/useSocket.d.ts.map +0 -1
- package/dist/hooks/useSocket.js +0 -120
- package/dist/hooks/useSwipeGesture.d.ts +0 -11
- package/dist/hooks/useSwipeGesture.d.ts.map +0 -1
- package/dist/hooks/useSwipeGesture.js +0 -54
- package/dist/hooks/useTextSelection.d.ts +0 -13
- package/dist/hooks/useTextSelection.d.ts.map +0 -1
- package/dist/hooks/useTextSelection.js +0 -132
- package/dist/hooks/useTyping.d.ts +0 -7
- package/dist/hooks/useTyping.d.ts.map +0 -1
- package/dist/hooks/useTyping.js +0 -64
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useCallback, useEffect, useRef } from "react";
|
|
4
|
-
import { Smile, ImageIcon, Paperclip, Mic, Send, Type, Sticker, User, X, Loader2, FileText } from "lucide-react"; // Import FileText icon
|
|
5
|
-
import { useChat } from "../hooks/useChat";
|
|
6
|
-
import { useTyping } from "../hooks/useTyping";
|
|
7
|
-
import { useTextSelection } from "../hooks/useTextSelection";
|
|
8
|
-
import { TextFormattingToolbar } from "./TextFormattingToolbar";
|
|
9
|
-
import { EmojiPicker } from "./EmojiPicker";
|
|
10
|
-
import { StickerPicker } from "./StickerPicker";
|
|
11
|
-
export function ChatInput({ conversationId, onSendMessage, onEmojiClick, onStickerClick, onFileUpload, onImageUpload, onContactShare, onVoiceRecord, onVoiceMessage, onQuickReact, placeholder = "Nhập tin nhắn", disabled = false, className = "", }) {
|
|
12
|
-
const [message, setMessage] = useState("");
|
|
13
|
-
const [isRecording, setIsRecording] = useState(false);
|
|
14
|
-
const [showVoiceWave, setShowVoiceWave] = useState(false);
|
|
15
|
-
const [showTextToolbar, setShowTextToolbar] = useState(false);
|
|
16
|
-
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
17
|
-
const [showStickerPicker, setShowStickerPicker] = useState(false);
|
|
18
|
-
const [selectedFormats, setSelectedFormats] = useState([]);
|
|
19
|
-
const [selectedFiles, setSelectedFiles] = useState([]); // State for selected files
|
|
20
|
-
const [isUploading, setIsUploading] = useState(false); // State for uploading status
|
|
21
|
-
const fileInputRef = useRef(null);
|
|
22
|
-
const imageInputRef = useRef(null);
|
|
23
|
-
// Refs for popups
|
|
24
|
-
const textToolbarRef = useRef(null);
|
|
25
|
-
const emojiPickerRef = useRef(null);
|
|
26
|
-
const stickerPickerRef = useRef(null);
|
|
27
|
-
// Refs for toggle buttons
|
|
28
|
-
const textButtonRef = useRef(null);
|
|
29
|
-
const emojiButtonRef = useRef(null);
|
|
30
|
-
const stickerButtonRef = useRef(null);
|
|
31
|
-
// Ref for the relative container that wraps the popups
|
|
32
|
-
const popupWrapperRef = useRef(null);
|
|
33
|
-
// State for dynamic popup styles
|
|
34
|
-
const [textToolbarStyle, setTextToolbarStyle] = useState({});
|
|
35
|
-
const [emojiPickerStyle, setEmojiPickerStyle] = useState({});
|
|
36
|
-
const [stickerPickerStyle, setStickerPickerStyle] = useState({});
|
|
37
|
-
const { sendMessage: sendChatMessage } = useChat(conversationId || "");
|
|
38
|
-
const { startTyping, stopTyping } = useTyping(conversationId || "");
|
|
39
|
-
const { textareaRef, applyFormat } = useTextSelection();
|
|
40
|
-
const handleSubmit = useCallback(async (e) => {
|
|
41
|
-
e.preventDefault();
|
|
42
|
-
if (disabled || isUploading)
|
|
43
|
-
return;
|
|
44
|
-
if (selectedFiles.length > 0) {
|
|
45
|
-
setIsUploading(true);
|
|
46
|
-
// Simulate file upload
|
|
47
|
-
await Promise.all(selectedFiles.map(async (file) => {
|
|
48
|
-
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate network delay
|
|
49
|
-
if (file.type.startsWith("image/")) {
|
|
50
|
-
onImageUpload === null || onImageUpload === void 0 ? void 0 : onImageUpload(file);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
onFileUpload === null || onFileUpload === void 0 ? void 0 : onFileUpload(file);
|
|
54
|
-
}
|
|
55
|
-
}));
|
|
56
|
-
setSelectedFiles([]);
|
|
57
|
-
setIsUploading(false);
|
|
58
|
-
}
|
|
59
|
-
if (message.trim()) {
|
|
60
|
-
// Use integrated chat system if available, otherwise use callback
|
|
61
|
-
if (sendChatMessage) {
|
|
62
|
-
sendChatMessage(message.trim());
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
onSendMessage === null || onSendMessage === void 0 ? void 0 : onSendMessage(message.trim());
|
|
66
|
-
}
|
|
67
|
-
setMessage("");
|
|
68
|
-
setSelectedFormats([]);
|
|
69
|
-
stopTyping();
|
|
70
|
-
// Reset textarea height
|
|
71
|
-
if (textareaRef.current) {
|
|
72
|
-
textareaRef.current.style.height = "auto";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}, [
|
|
76
|
-
message,
|
|
77
|
-
disabled,
|
|
78
|
-
isUploading,
|
|
79
|
-
selectedFiles,
|
|
80
|
-
sendChatMessage,
|
|
81
|
-
onSendMessage,
|
|
82
|
-
onFileUpload,
|
|
83
|
-
onImageUpload,
|
|
84
|
-
stopTyping,
|
|
85
|
-
textareaRef,
|
|
86
|
-
]);
|
|
87
|
-
const handleInputChange = useCallback((e) => {
|
|
88
|
-
const value = e.target.value;
|
|
89
|
-
setMessage(value);
|
|
90
|
-
// Trigger typing indicator
|
|
91
|
-
if (value.trim()) {
|
|
92
|
-
startTyping();
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
stopTyping();
|
|
96
|
-
}
|
|
97
|
-
// Auto-resize textarea
|
|
98
|
-
const textarea = e.target;
|
|
99
|
-
textarea.style.height = "auto";
|
|
100
|
-
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
|
|
101
|
-
}, [startTyping, stopTyping]);
|
|
102
|
-
const handleKeyPress = useCallback((e) => {
|
|
103
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
handleSubmit(e);
|
|
106
|
-
}
|
|
107
|
-
// Handle keyboard shortcuts
|
|
108
|
-
if (e.ctrlKey || e.metaKey) {
|
|
109
|
-
switch (e.key) {
|
|
110
|
-
case "b":
|
|
111
|
-
e.preventDefault();
|
|
112
|
-
applyFormat("bold");
|
|
113
|
-
break;
|
|
114
|
-
case "i":
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
applyFormat("italic");
|
|
117
|
-
break;
|
|
118
|
-
case "e":
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
applyFormat("code");
|
|
121
|
-
break;
|
|
122
|
-
case "k":
|
|
123
|
-
e.preventDefault();
|
|
124
|
-
applyFormat("link");
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}, [handleSubmit, applyFormat]);
|
|
129
|
-
const handleFileClick = useCallback(() => {
|
|
130
|
-
var _a;
|
|
131
|
-
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
132
|
-
}, []);
|
|
133
|
-
const handleImageClick = useCallback(() => {
|
|
134
|
-
var _a;
|
|
135
|
-
(_a = imageInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
136
|
-
}, []);
|
|
137
|
-
const handleFileChange = useCallback((e) => {
|
|
138
|
-
const files = Array.from(e.target.files || []);
|
|
139
|
-
if (files.length > 0) {
|
|
140
|
-
setSelectedFiles((prev) => [...prev, ...files]);
|
|
141
|
-
e.target.value = ""; // Reset input to allow selecting same file again
|
|
142
|
-
}
|
|
143
|
-
}, []);
|
|
144
|
-
const handleImageChange = useCallback((e) => {
|
|
145
|
-
const files = Array.from(e.target.files || []);
|
|
146
|
-
if (files.length > 0) {
|
|
147
|
-
setSelectedFiles((prev) => [...prev, ...files]);
|
|
148
|
-
e.target.value = ""; // Reset input to allow selecting same file again
|
|
149
|
-
}
|
|
150
|
-
}, []);
|
|
151
|
-
const handleRemoveFile = useCallback((fileToRemove) => {
|
|
152
|
-
setSelectedFiles((prev) => prev.filter((file) => file !== fileToRemove));
|
|
153
|
-
}, []);
|
|
154
|
-
const handleVoiceRecord = useCallback(() => {
|
|
155
|
-
setIsRecording(!isRecording);
|
|
156
|
-
onVoiceRecord === null || onVoiceRecord === void 0 ? void 0 : onVoiceRecord();
|
|
157
|
-
}, [isRecording, onVoiceRecord]);
|
|
158
|
-
const handleTextToolbarToggle = useCallback(() => {
|
|
159
|
-
setShowTextToolbar((prev) => !prev);
|
|
160
|
-
setShowEmojiPicker(false);
|
|
161
|
-
setShowStickerPicker(false);
|
|
162
|
-
}, []);
|
|
163
|
-
const handleEmojiToggle = useCallback(() => {
|
|
164
|
-
setShowEmojiPicker((prev) => !prev);
|
|
165
|
-
setShowTextToolbar(false);
|
|
166
|
-
setShowStickerPicker(false);
|
|
167
|
-
}, []);
|
|
168
|
-
const handleStickerToggle = useCallback(() => {
|
|
169
|
-
setShowStickerPicker((prev) => !prev);
|
|
170
|
-
setShowTextToolbar(false);
|
|
171
|
-
setShowEmojiPicker(false);
|
|
172
|
-
}, []);
|
|
173
|
-
const handleFormatSelect = useCallback((format) => {
|
|
174
|
-
applyFormat(format);
|
|
175
|
-
setSelectedFormats((prev) => {
|
|
176
|
-
if (prev.includes(format)) {
|
|
177
|
-
return prev.filter((f) => f !== format);
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
return [...prev, format];
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}, [applyFormat]);
|
|
184
|
-
const handleEmojiSelect = useCallback((emoji) => {
|
|
185
|
-
const textarea = textareaRef.current;
|
|
186
|
-
if (textarea) {
|
|
187
|
-
const start = textarea.selectionStart;
|
|
188
|
-
const end = textarea.selectionEnd;
|
|
189
|
-
const newMessage = message.slice(0, start) + emoji + message.slice(end);
|
|
190
|
-
setMessage(newMessage);
|
|
191
|
-
setTimeout(() => {
|
|
192
|
-
textarea.selectionStart = textarea.selectionEnd = start + emoji.length;
|
|
193
|
-
textarea.focus();
|
|
194
|
-
}, 0);
|
|
195
|
-
}
|
|
196
|
-
onEmojiClick === null || onEmojiClick === void 0 ? void 0 : onEmojiClick(emoji);
|
|
197
|
-
}, [message, onEmojiClick, textareaRef]);
|
|
198
|
-
const handleStickerSelect = useCallback((sticker) => {
|
|
199
|
-
// Send sticker immediately (like a message)
|
|
200
|
-
if (sendChatMessage) {
|
|
201
|
-
sendChatMessage(sticker);
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
onSendMessage === null || onSendMessage === void 0 ? void 0 : onSendMessage(sticker);
|
|
205
|
-
}
|
|
206
|
-
onStickerClick === null || onStickerClick === void 0 ? void 0 : onStickerClick(sticker);
|
|
207
|
-
setShowStickerPicker(false); // Close picker after selection
|
|
208
|
-
}, [sendChatMessage, onSendMessage, onStickerClick]);
|
|
209
|
-
// Click outside logic
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
const handleClickOutside = (event) => {
|
|
212
|
-
const target = event.target;
|
|
213
|
-
// Text Formatting Toolbar
|
|
214
|
-
if (showTextToolbar &&
|
|
215
|
-
textToolbarRef.current &&
|
|
216
|
-
!textToolbarRef.current.contains(target) &&
|
|
217
|
-
textButtonRef.current &&
|
|
218
|
-
!textButtonRef.current.contains(target)) {
|
|
219
|
-
setShowTextToolbar(false);
|
|
220
|
-
}
|
|
221
|
-
// Emoji Picker
|
|
222
|
-
if (showEmojiPicker &&
|
|
223
|
-
emojiPickerRef.current &&
|
|
224
|
-
!emojiPickerRef.current.contains(target) &&
|
|
225
|
-
emojiButtonRef.current &&
|
|
226
|
-
!emojiButtonRef.current.contains(target)) {
|
|
227
|
-
setShowEmojiPicker(false);
|
|
228
|
-
}
|
|
229
|
-
// Sticker Picker
|
|
230
|
-
if (showStickerPicker &&
|
|
231
|
-
stickerPickerRef.current &&
|
|
232
|
-
!stickerPickerRef.current.contains(target) &&
|
|
233
|
-
stickerButtonRef.current &&
|
|
234
|
-
!stickerButtonRef.current.contains(target)) {
|
|
235
|
-
setShowStickerPicker(false);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
239
|
-
return () => {
|
|
240
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
241
|
-
};
|
|
242
|
-
}, [showTextToolbar, showEmojiPicker, showStickerPicker]);
|
|
243
|
-
// Dynamic positioning logic for popups
|
|
244
|
-
const calculatePopupPosition = useCallback((buttonRef, popupRef, setPopupStyle) => {
|
|
245
|
-
const wrapper = popupWrapperRef.current;
|
|
246
|
-
const button = buttonRef.current;
|
|
247
|
-
const popup = popupRef.current;
|
|
248
|
-
if (!wrapper || !button || !popup)
|
|
249
|
-
return;
|
|
250
|
-
const wrapperRect = wrapper.getBoundingClientRect();
|
|
251
|
-
const buttonRect = button.getBoundingClientRect();
|
|
252
|
-
const popupRect = popup.getBoundingClientRect();
|
|
253
|
-
const viewportWidth = window.innerWidth;
|
|
254
|
-
let desiredLeft = buttonRect.left - wrapperRect.left;
|
|
255
|
-
// Add a small margin (e.g., 8px) from the right edge of the viewport
|
|
256
|
-
const rightViewportMargin = 8;
|
|
257
|
-
if (wrapperRect.left + desiredLeft + popupRect.width > viewportWidth - rightViewportMargin) {
|
|
258
|
-
desiredLeft = viewportWidth - wrapperRect.left - popupRect.width - rightViewportMargin;
|
|
259
|
-
// Ensure it doesn't go off the left edge (e.g., 8px margin from left)
|
|
260
|
-
desiredLeft = Math.max(8, desiredLeft);
|
|
261
|
-
}
|
|
262
|
-
setPopupStyle({ left: `${desiredLeft}px` });
|
|
263
|
-
}, []);
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
if (showTextToolbar) {
|
|
266
|
-
calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle);
|
|
267
|
-
window.addEventListener("resize", () => calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle));
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
setTextToolbarStyle({}); // Reset style when closed
|
|
271
|
-
}
|
|
272
|
-
return () => window.removeEventListener("resize", () => calculatePopupPosition(textButtonRef, textToolbarRef, setTextToolbarStyle));
|
|
273
|
-
}, [showTextToolbar, calculatePopupPosition]);
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
if (showEmojiPicker) {
|
|
276
|
-
calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle);
|
|
277
|
-
window.addEventListener("resize", () => calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle));
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
setEmojiPickerStyle({}); // Reset style when closed
|
|
281
|
-
}
|
|
282
|
-
return () => window.removeEventListener("resize", () => calculatePopupPosition(emojiButtonRef, emojiPickerRef, setEmojiPickerStyle));
|
|
283
|
-
}, [showEmojiPicker, calculatePopupPosition]);
|
|
284
|
-
useEffect(() => {
|
|
285
|
-
if (showStickerPicker) {
|
|
286
|
-
calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle);
|
|
287
|
-
window.addEventListener("resize", () => calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle));
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
setStickerPickerStyle({}); // Reset style when closed
|
|
291
|
-
}
|
|
292
|
-
return () => window.removeEventListener("resize", () => calculatePopupPosition(stickerButtonRef, stickerPickerRef, setStickerPickerStyle));
|
|
293
|
-
}, [showStickerPicker, calculatePopupPosition]);
|
|
294
|
-
// Cleanup object URLs when selectedFiles change or component unmounts
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
const objectUrls = [];
|
|
297
|
-
selectedFiles.forEach((file) => {
|
|
298
|
-
if (file.type.startsWith("image/")) {
|
|
299
|
-
objectUrls.push(URL.createObjectURL(file));
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
return () => {
|
|
303
|
-
objectUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
304
|
-
};
|
|
305
|
-
}, [selectedFiles]);
|
|
306
|
-
// Small, left-aligned action buttons
|
|
307
|
-
const actionButtons = [
|
|
308
|
-
{
|
|
309
|
-
icon: Type,
|
|
310
|
-
label: "Định dạng văn bản",
|
|
311
|
-
onClick: handleTextToolbarToggle,
|
|
312
|
-
color: showTextToolbar ? "text-blue-500 bg-blue-50" : "text-gray-500 hover:text-blue-500",
|
|
313
|
-
active: showTextToolbar,
|
|
314
|
-
ref: textButtonRef,
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
icon: Smile,
|
|
318
|
-
label: "Emoji",
|
|
319
|
-
onClick: handleEmojiToggle,
|
|
320
|
-
color: showEmojiPicker ? "text-yellow-500 bg-yellow-50" : "text-gray-500 hover:text-yellow-500",
|
|
321
|
-
active: showEmojiPicker,
|
|
322
|
-
ref: emojiButtonRef,
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
icon: Sticker,
|
|
326
|
-
label: "Sticker",
|
|
327
|
-
onClick: handleStickerToggle,
|
|
328
|
-
color: showStickerPicker ? "text-purple-500 bg-purple-50" : "text-gray-500 hover:text-purple-500",
|
|
329
|
-
active: showStickerPicker,
|
|
330
|
-
ref: stickerButtonRef,
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
icon: ImageIcon,
|
|
334
|
-
label: "Hình ảnh",
|
|
335
|
-
onClick: handleImageClick,
|
|
336
|
-
color: "text-gray-500 hover:text-green-500",
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
icon: Paperclip,
|
|
340
|
-
label: "Tệp đính kèm",
|
|
341
|
-
onClick: handleFileClick,
|
|
342
|
-
color: "text-gray-500 hover:text-blue-500",
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
icon: User,
|
|
346
|
-
label: "Chia sẻ liên hệ",
|
|
347
|
-
onClick: onContactShare,
|
|
348
|
-
color: "text-gray-500 hover:text-indigo-500",
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
icon: Mic,
|
|
352
|
-
label: "Ghi âm",
|
|
353
|
-
onClick: handleVoiceRecord,
|
|
354
|
-
color: isRecording ? "text-red-500 bg-red-50" : "text-gray-500 hover:text-red-500",
|
|
355
|
-
active: isRecording,
|
|
356
|
-
},
|
|
357
|
-
];
|
|
358
|
-
const isSendButtonDisabled = disabled || isUploading || (message.trim() === "" && selectedFiles.length === 0);
|
|
359
|
-
return (_jsxs("div", { className: `bg-white border-t border-gray-200 p-3 sm:p-4 ${className}`, children: [_jsxs("div", { className: "relative", ref: popupWrapperRef, children: [_jsx(TextFormattingToolbar, { ref: textToolbarRef, isOpen: showTextToolbar, onClose: () => setShowTextToolbar(false), onFormatSelect: handleFormatSelect, selectedFormats: selectedFormats, style: textToolbarStyle }), _jsx(EmojiPicker, { ref: emojiPickerRef, isOpen: showEmojiPicker, onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false), style: emojiPickerStyle }), _jsx(StickerPicker, { ref: stickerPickerRef, isOpen: showStickerPicker, onStickerSelect: handleStickerSelect, onClose: () => setShowStickerPicker(false), style: stickerPickerStyle })] }), _jsxs("div", { className: "bg-gray-50 rounded-2xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200", children: [selectedFiles.length > 0 && (_jsx("div", { className: "px-4 pt-3 pb-2 border-b border-gray-100 flex flex-wrap gap-2", children: selectedFiles.map((file, index) => {
|
|
360
|
-
const isImage = file.type.startsWith("image/");
|
|
361
|
-
const fileUrl = isImage ? URL.createObjectURL(file) : null;
|
|
362
|
-
return (_jsxs("div", { className: "flex items-center bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-1 rounded-lg relative overflow-hidden", children: [isImage ? (_jsx("img", { src: fileUrl || "", alt: file.name, className: "h-12 w-12 object-cover rounded-md mr-2" })) : (_jsxs("div", { className: "flex items-center space-x-1", children: [_jsx(FileText, { className: "w-4 h-4 text-blue-600" }), " ", _jsx("span", { className: "truncate max-w-[100px] sm:max-w-[150px]", children: file.name })] })), _jsx("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}`, children: _jsx(X, { className: "w-3 h-3" }) })] }, index));
|
|
363
|
-
}) })), _jsx("div", { className: "px-4 py-3", children: _jsxs("form", { onSubmit: handleSubmit, className: "flex items-end space-x-3", children: [_jsx("div", { className: "flex-1 relative", children: _jsx("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: {
|
|
364
|
-
minHeight: "20px",
|
|
365
|
-
maxHeight: "120px",
|
|
366
|
-
} }) }), _jsx("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", children: isUploading ? _jsx(Loader2, { className: "w-4 h-4 animate-spin" }) : _jsx(Send, { className: "w-4 h-4" }) })] }) }), _jsx("div", { className: "px-4 pb-3 border-t border-gray-100", children: _jsx("div", { className: "flex items-center space-x-2 justify-start", children: actionButtons.map((button, index) => {
|
|
367
|
-
const Icon = button.icon;
|
|
368
|
-
return (_jsx("button", { ref: button.ref, onClick: button.onClick, disabled: disabled || isUploading, className: `
|
|
369
|
-
p-1.5 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed
|
|
370
|
-
hover:bg-gray-100 active:scale-95
|
|
371
|
-
${button.color}
|
|
372
|
-
`, "aria-label": button.label, title: button.label, children: _jsx(Icon, { className: "w-4 h-4" }) }, index));
|
|
373
|
-
}) }) }), isRecording && (_jsx("div", { className: "px-4 pb-2", children: _jsxs("div", { className: "flex items-center space-x-2 text-red-500 text-sm", children: [_jsx("div", { className: "w-2 h-2 bg-red-500 rounded-full animate-pulse" }), _jsx("span", { children: "\u0110ang ghi \u00E2m..." })] }) })), showVoiceWave && (_jsx("div", { className: "px-4 pb-2", children: _jsx("div", { className: "flex items-center space-x-1", children: [...Array(8)].map((_, i) => (_jsx("div", { className: "w-1 bg-blue-500 rounded-full animate-pulse", style: {
|
|
374
|
-
height: `${Math.random() * 20 + 10}px`,
|
|
375
|
-
animationDelay: `${i * 0.1}s`,
|
|
376
|
-
} }, i))) }) }))] }), _jsx("input", { ref: fileInputRef, type: "file", onChange: handleFileChange, className: "hidden", accept: ".pdf,.doc,.docx,.txt,.zip,.rar", multiple // Allow multiple file selection
|
|
377
|
-
: true }), _jsx("input", { ref: imageInputRef, type: "file", onChange: handleImageChange, className: "hidden", accept: "image/*", multiple // Allow multiple image selection
|
|
378
|
-
: true })] }));
|
|
379
|
-
}
|