@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.
- package/dist/components/AutoScrollAnchor.d.ts +2 -0
- package/dist/components/AutoScrollAnchor.d.ts.map +1 -0
- package/dist/components/AutoScrollAnchor.jsx +11 -0
- package/dist/components/ChatBubble.d.ts +2 -0
- package/dist/components/ChatBubble.d.ts.map +1 -0
- package/dist/components/ChatBubble.jsx +32 -0
- package/dist/components/ChatHeader.d.ts +8 -0
- package/dist/components/ChatHeader.d.ts.map +1 -0
- package/dist/components/ChatHeader.jsx +72 -0
- package/dist/components/ChatInput.d.ts +4 -0
- package/dist/components/ChatInput.d.ts.map +1 -0
- package/dist/components/ChatInput.jsx +444 -0
- package/dist/components/ChatInputDemo.d.ts +2 -0
- package/dist/components/ChatInputDemo.d.ts.map +1 -0
- package/dist/components/ChatInputDemo.jsx +53 -0
- package/dist/components/ChatInputWithCustomIcon.d.ts +17 -0
- package/dist/components/ChatInputWithCustomIcon.d.ts.map +1 -0
- package/dist/components/ChatInputWithCustomIcon.jsx +167 -0
- package/dist/components/ChatLayout.d.ts +6 -0
- package/dist/components/ChatLayout.d.ts.map +1 -0
- package/dist/components/ChatLayout.jsx +122 -0
- package/dist/components/ConversationItem.d.ts +9 -0
- package/dist/components/ConversationItem.d.ts.map +1 -0
- package/dist/components/ConversationItem.jsx +51 -0
- package/dist/components/ConversationList.d.ts +8 -0
- package/dist/components/ConversationList.d.ts.map +1 -0
- package/dist/components/ConversationList.jsx +22 -0
- package/dist/components/DateDivider.d.ts +7 -0
- package/dist/components/DateDivider.d.ts.map +1 -0
- package/dist/components/DateDivider.jsx +28 -0
- package/dist/components/EmojiPicker.d.ts +4 -0
- package/dist/components/EmojiPicker.d.ts.map +1 -0
- package/dist/components/EmojiPicker.jsx +229 -0
- package/dist/components/ImageLightbox.d.ts +8 -0
- package/dist/components/ImageLightbox.d.ts.map +1 -0
- package/dist/components/ImageLightbox.jsx +16 -0
- package/dist/components/ImagePreviewModal.d.ts +12 -0
- package/dist/components/ImagePreviewModal.d.ts.map +1 -0
- package/dist/components/ImagePreviewModal.jsx +84 -0
- package/dist/components/MessageItem.d.ts +3 -0
- package/dist/components/MessageItem.d.ts.map +1 -0
- package/dist/components/MessageItem.jsx +99 -0
- package/dist/components/MessageItemDemo.d.ts +2 -0
- package/dist/components/MessageItemDemo.d.ts.map +1 -0
- package/dist/components/MessageItemDemo.jsx +179 -0
- package/dist/components/MessageList.d.ts +15 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.jsx +306 -0
- package/dist/components/MessageListDemo.d.ts +2 -0
- package/dist/components/MessageListDemo.d.ts.map +1 -0
- package/dist/components/MessageListDemo.jsx +183 -0
- package/dist/components/StickerPicker.d.ts +4 -0
- package/dist/components/StickerPicker.d.ts.map +1 -0
- package/dist/components/StickerPicker.jsx +106 -0
- package/dist/components/SwipeIndicator.d.ts +9 -0
- package/dist/components/SwipeIndicator.d.ts.map +1 -0
- package/dist/components/SwipeIndicator.jsx +28 -0
- package/dist/components/TextFormattingToolbar.d.ts +4 -0
- package/dist/components/TextFormattingToolbar.d.ts.map +1 -0
- package/dist/components/TextFormattingToolbar.jsx +52 -0
- package/dist/components/TypingIndicator.d.ts +6 -0
- package/dist/components/TypingIndicator.d.ts.map +1 -0
- package/dist/components/TypingIndicator.jsx +27 -0
- package/dist/components/VoiceWaveIcon.d.ts +7 -0
- package/dist/components/VoiceWaveIcon.d.ts.map +1 -0
- package/dist/components/VoiceWaveIcon.jsx +11 -0
- package/dist/context/ChatContext.d.ts +72 -0
- package/dist/context/ChatContext.d.ts.map +1 -0
- package/dist/context/ChatContext.jsx +346 -0
- package/dist/hooks/useChat.d.ts +5 -0
- package/dist/hooks/useChat.d.ts.map +1 -0
- package/dist/hooks/useChat.js +73 -0
- package/dist/hooks/useConversationList.d.ts +5 -0
- package/dist/hooks/useConversationList.d.ts.map +1 -0
- package/dist/hooks/useConversationList.js +9 -0
- package/dist/hooks/useMessages.d.ts +5 -0
- package/dist/hooks/useMessages.d.ts.map +1 -0
- package/dist/hooks/useMessages.js +192 -0
- package/dist/hooks/useSocket.d.ts +7 -0
- package/dist/hooks/useSocket.d.ts.map +1 -0
- package/dist/hooks/useSocket.js +120 -0
- package/dist/hooks/useSwipeGesture.d.ts +11 -0
- package/dist/hooks/useSwipeGesture.d.ts.map +1 -0
- package/dist/hooks/useSwipeGesture.js +54 -0
- package/dist/hooks/useTextSelection.d.ts +13 -0
- package/dist/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/hooks/useTextSelection.js +132 -0
- package/dist/hooks/useTyping.d.ts +7 -0
- package/dist/hooks/useTyping.d.ts.map +1 -0
- package/dist/hooks/useTyping.js +64 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/types/chat.d.ts +39 -0
- package/dist/types/chat.d.ts.map +1 -0
- package/dist/types/chat.js +1 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +60 -0
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|