@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,17 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
interface ChatInputProps {
|
|
3
|
+
onSendMessage?: (message: string) => void;
|
|
4
|
+
onEmojiClick?: (emoji: string) => void;
|
|
5
|
+
onFileUpload?: (file: File) => void;
|
|
6
|
+
onImageUpload?: (file: File) => void;
|
|
7
|
+
onContactShare?: () => void;
|
|
8
|
+
onVoiceRecord?: () => void;
|
|
9
|
+
onVoiceMessage?: () => void;
|
|
10
|
+
onQuickReact?: () => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function ChatInputWithCustomIcon({ onSendMessage, onEmojiClick, onFileUpload, onImageUpload, onContactShare, onVoiceRecord, onVoiceMessage, onQuickReact, placeholder, disabled, className, }: ChatInputProps): React.JSX.Element;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=ChatInputWithCustomIcon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInputWithCustomIcon.d.ts","sourceRoot":"","sources":["../../src/components/ChatInputWithCustomIcon.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,UAAU,cAAc;IACtB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IACnC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IACpC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,WAA6B,EAC7B,QAAgB,EAChB,SAAc,GACf,EAAE,cAAc,qBAgRhB"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useRef, useCallback } from "react";
|
|
3
|
+
import { Smile, ImageIcon, Paperclip, Mic, ThumbsUp, Send, Type, Sticker, User } from "lucide-react";
|
|
4
|
+
import { VoiceWaveIcon } from "./VoiceWaveIcon";
|
|
5
|
+
export function ChatInputWithCustomIcon({ onSendMessage, onEmojiClick, onFileUpload, onImageUpload, onContactShare, onVoiceRecord, onVoiceMessage, onQuickReact, placeholder = "Nhập tin nhắn", disabled = false, className = "", }) {
|
|
6
|
+
const [message, setMessage] = useState("");
|
|
7
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
8
|
+
const [showVoiceWave, setShowVoiceWave] = useState(false);
|
|
9
|
+
const textareaRef = useRef(null);
|
|
10
|
+
const fileInputRef = useRef(null);
|
|
11
|
+
const imageInputRef = useRef(null);
|
|
12
|
+
const handleSubmit = useCallback((e) => {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
if (!message.trim() || disabled)
|
|
15
|
+
return;
|
|
16
|
+
onSendMessage === null || onSendMessage === void 0 ? void 0 : onSendMessage(message.trim());
|
|
17
|
+
setMessage("");
|
|
18
|
+
// Reset textarea height
|
|
19
|
+
if (textareaRef.current) {
|
|
20
|
+
textareaRef.current.style.height = "auto";
|
|
21
|
+
}
|
|
22
|
+
}, [message, disabled, onSendMessage]);
|
|
23
|
+
const handleInputChange = useCallback((e) => {
|
|
24
|
+
const value = e.target.value;
|
|
25
|
+
setMessage(value);
|
|
26
|
+
// Auto-resize textarea
|
|
27
|
+
const textarea = e.target;
|
|
28
|
+
textarea.style.height = "auto";
|
|
29
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
|
|
30
|
+
}, []);
|
|
31
|
+
const handleKeyPress = useCallback((e) => {
|
|
32
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
handleSubmit(e);
|
|
35
|
+
}
|
|
36
|
+
}, [handleSubmit]);
|
|
37
|
+
const handleFileClick = useCallback(() => {
|
|
38
|
+
var _a;
|
|
39
|
+
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
40
|
+
}, []);
|
|
41
|
+
const handleImageClick = useCallback(() => {
|
|
42
|
+
var _a;
|
|
43
|
+
(_a = imageInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
44
|
+
}, []);
|
|
45
|
+
const handleFileChange = useCallback((e) => {
|
|
46
|
+
var _a;
|
|
47
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
48
|
+
if (file) {
|
|
49
|
+
onFileUpload === null || onFileUpload === void 0 ? void 0 : onFileUpload(file);
|
|
50
|
+
e.target.value = ""; // Reset input
|
|
51
|
+
}
|
|
52
|
+
}, [onFileUpload]);
|
|
53
|
+
const handleImageChange = useCallback((e) => {
|
|
54
|
+
var _a;
|
|
55
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
56
|
+
if (file) {
|
|
57
|
+
onImageUpload === null || onImageUpload === void 0 ? void 0 : onImageUpload(file);
|
|
58
|
+
e.target.value = ""; // Reset input
|
|
59
|
+
}
|
|
60
|
+
}, [onImageUpload]);
|
|
61
|
+
const handleVoiceRecord = useCallback(() => {
|
|
62
|
+
setIsRecording(!isRecording);
|
|
63
|
+
onVoiceRecord === null || onVoiceRecord === void 0 ? void 0 : onVoiceRecord();
|
|
64
|
+
}, [isRecording, onVoiceRecord]);
|
|
65
|
+
const handleVoiceMessage = useCallback(() => {
|
|
66
|
+
setShowVoiceWave(!showVoiceWave);
|
|
67
|
+
onVoiceMessage === null || onVoiceMessage === void 0 ? void 0 : onVoiceMessage();
|
|
68
|
+
}, [showVoiceWave, onVoiceMessage]);
|
|
69
|
+
return (<div className={`bg-white border-t border-gray-200 p-3 sm:p-4 ${className}`}>
|
|
70
|
+
{/* Main Input Container */}
|
|
71
|
+
<div className="bg-gray-50 rounded-2xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200">
|
|
72
|
+
{/* Input Field */}
|
|
73
|
+
<div className="px-4 py-3">
|
|
74
|
+
<form onSubmit={handleSubmit} className="flex items-end space-x-3">
|
|
75
|
+
<div className="flex-1 relative">
|
|
76
|
+
<textarea ref={textareaRef} value={message} onChange={handleInputChange} onKeyPress={handleKeyPress} placeholder={placeholder} disabled={disabled} 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={{
|
|
77
|
+
minHeight: "20px",
|
|
78
|
+
maxHeight: "120px",
|
|
79
|
+
}}/>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Send Button or Quick React */}
|
|
83
|
+
{message.trim() ? (<button type="submit" disabled={disabled} 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="Send message">
|
|
84
|
+
<Send className="w-4 h-4"/>
|
|
85
|
+
</button>) : (<button type="button" onClick={onQuickReact} className="flex-shrink-0 p-2 text-blue-500 hover:bg-blue-50 rounded-full transition-colors duration-200" aria-label="Quick react">
|
|
86
|
+
<ThumbsUp className="w-4 h-4"/>
|
|
87
|
+
</button>)}
|
|
88
|
+
</form>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Action Icons Row */}
|
|
92
|
+
<div className="px-4 pb-3 border-t border-gray-100">
|
|
93
|
+
<div className="flex items-center justify-between space-x-1">
|
|
94
|
+
{/* Text Style */}
|
|
95
|
+
<button onClick={() => console.log("Text style clicked")} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-blue-500" aria-label="Text style" title="Text style">
|
|
96
|
+
<Type className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
{/* Emoji */}
|
|
100
|
+
<button onClick={() => onEmojiClick === null || onEmojiClick === void 0 ? void 0 : onEmojiClick("😊")} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-yellow-500" aria-label="Emoji" title="Emoji">
|
|
101
|
+
<Smile className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
{/* Sticker */}
|
|
105
|
+
<button onClick={() => console.log("Sticker clicked")} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-purple-500" aria-label="Sticker" title="Sticker">
|
|
106
|
+
<Sticker className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
107
|
+
</button>
|
|
108
|
+
|
|
109
|
+
{/* Image Upload */}
|
|
110
|
+
<button onClick={handleImageClick} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-green-500" aria-label="Image upload" title="Image upload">
|
|
111
|
+
<ImageIcon className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
{/* File Attachment */}
|
|
115
|
+
<button onClick={handleFileClick} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-blue-500" aria-label="File attachment" title="File attachment">
|
|
116
|
+
<Paperclip className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
117
|
+
</button>
|
|
118
|
+
|
|
119
|
+
{/* Contact Sharing */}
|
|
120
|
+
<button onClick={onContactShare} disabled={disabled} className="p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 active:scale-95 text-gray-500 hover:text-indigo-500" aria-label="Contact sharing" title="Contact sharing">
|
|
121
|
+
<User className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
{/* Record Audio */}
|
|
125
|
+
<button onClick={handleVoiceRecord} disabled={disabled} className={`
|
|
126
|
+
p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed
|
|
127
|
+
hover:bg-gray-100 active:scale-95
|
|
128
|
+
${isRecording ? "text-red-500 bg-red-50" : "text-gray-500 hover:text-red-500"}
|
|
129
|
+
`} aria-label="Record audio" title="Record audio">
|
|
130
|
+
<Mic className="w-4 h-4 sm:w-5 sm:h-5"/>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
{/* Voice Message */}
|
|
134
|
+
<button onClick={handleVoiceMessage} disabled={disabled} className={`
|
|
135
|
+
p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed
|
|
136
|
+
hover:bg-gray-100 active:scale-95
|
|
137
|
+
${showVoiceWave ? "text-blue-500 bg-blue-50" : "text-gray-500 hover:text-blue-500"}
|
|
138
|
+
`} aria-label="Voice message" title="Voice message">
|
|
139
|
+
<VoiceWaveIcon className="w-4 h-4 sm:w-5 sm:h-5" animated={showVoiceWave}/>
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Recording Indicator */}
|
|
145
|
+
{isRecording && (<div className="px-4 pb-2">
|
|
146
|
+
<div className="flex items-center space-x-2 text-red-500 text-sm">
|
|
147
|
+
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
|
|
148
|
+
<span>Recording...</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>)}
|
|
151
|
+
|
|
152
|
+
{/* Voice Wave Indicator */}
|
|
153
|
+
{showVoiceWave && (<div className="px-4 pb-2">
|
|
154
|
+
<div className="flex items-center space-x-1">
|
|
155
|
+
{[...Array(8)].map((_, i) => (<div key={i} className="w-1 bg-blue-500 rounded-full animate-pulse" style={{
|
|
156
|
+
height: `${Math.random() * 20 + 10}px`,
|
|
157
|
+
animationDelay: `${i * 0.1}s`,
|
|
158
|
+
}}/>))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Hidden File Inputs */}
|
|
164
|
+
<input ref={fileInputRef} type="file" onChange={handleFileChange} className="hidden" accept=".pdf,.doc,.docx,.txt,.zip,.rar"/>
|
|
165
|
+
<input ref={imageInputRef} type="file" onChange={handleImageChange} className="hidden" accept="image/*"/>
|
|
166
|
+
</div>);
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatLayout.d.ts","sourceRoot":"","sources":["../../src/components/ChatLayout.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,UAAU,CAAC,EAAE,SAAc,EAAE,EAAE,eAAe,+BA2J7D"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ConversationList } from "./ConversationList";
|
|
4
|
+
import { ChatHeader } from "./ChatHeader";
|
|
5
|
+
import { MessageList } from "./MessageList";
|
|
6
|
+
import { ChatInput } from "./ChatInput";
|
|
7
|
+
import { useChatContext } from "../context/ChatContext";
|
|
8
|
+
import { useSwipeGesture } from "../hooks/useSwipeGesture";
|
|
9
|
+
import { useMessages } from "../hooks/useMessages";
|
|
10
|
+
export function ChatLayout({ className = "" }) {
|
|
11
|
+
var _a;
|
|
12
|
+
const [selectedConversationId, setSelectedConversationId] = useState(null);
|
|
13
|
+
const [showSidebar, setShowSidebar] = useState(false);
|
|
14
|
+
const { state } = useChatContext();
|
|
15
|
+
const messagesHook = useMessages(selectedConversationId || "");
|
|
16
|
+
const handleConversationSelect = (conversationId) => {
|
|
17
|
+
setSelectedConversationId(conversationId);
|
|
18
|
+
setShowSidebar(false); // Hide sidebar on mobile after selection
|
|
19
|
+
};
|
|
20
|
+
const handleBackToList = () => {
|
|
21
|
+
setSelectedConversationId(null);
|
|
22
|
+
setShowSidebar(true);
|
|
23
|
+
};
|
|
24
|
+
// Swipe gesture for going back to conversation list
|
|
25
|
+
const swipeRef = useSwipeGesture({
|
|
26
|
+
onSwipeRight: () => {
|
|
27
|
+
// Only trigger on mobile when a conversation is selected
|
|
28
|
+
if (selectedConversationId && window.innerWidth < 768) {
|
|
29
|
+
handleBackToList();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
threshold: 100, // Minimum swipe distance
|
|
33
|
+
restraint: 100, // Maximum vertical movement allowed
|
|
34
|
+
allowedTime: 300, // Maximum time for swipe
|
|
35
|
+
enabled: !!selectedConversationId, // Only enable when conversation is selected
|
|
36
|
+
});
|
|
37
|
+
return (<div className={`flex h-screen bg-white ${className}`} ref={swipeRef}>
|
|
38
|
+
{/* Mobile Sidebar Overlay */}
|
|
39
|
+
{showSidebar && (<div className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" onClick={() => setShowSidebar(false)}/>)}
|
|
40
|
+
|
|
41
|
+
{/* Sidebar - Separate scroll container */}
|
|
42
|
+
<div className={`
|
|
43
|
+
fixed inset-y-0 left-0 z-50 w-full bg-white transform transition-transform duration-300 ease-in-out
|
|
44
|
+
md:relative md:translate-x-0 md:w-80 md:border-r md:border-gray-200
|
|
45
|
+
${showSidebar ? "translate-x-0" : "-translate-x-full"}
|
|
46
|
+
${selectedConversationId ? "hidden md:flex" : "flex"}
|
|
47
|
+
flex-col h-full
|
|
48
|
+
`}>
|
|
49
|
+
{/* Sidebar Header - Fixed */}
|
|
50
|
+
<div className="flex-shrink-0 p-3 sm:p-4 border-b border-gray-200 bg-white">
|
|
51
|
+
<div className="flex items-center justify-between">
|
|
52
|
+
<h2 className="text-lg font-semibold text-gray-900">Messages</h2>
|
|
53
|
+
<div className="flex items-center space-x-2">
|
|
54
|
+
{!state.isConnected && (<div className="flex items-center text-xs text-gray-500">
|
|
55
|
+
<div className="w-2 h-2 bg-gray-400 rounded-full mr-2"></div>
|
|
56
|
+
<span className="hidden sm:inline">Demo Mode</span>
|
|
57
|
+
</div>)}
|
|
58
|
+
{/* Mobile close button */}
|
|
59
|
+
<button onClick={() => setShowSidebar(false)} className="md:hidden p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100">
|
|
60
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Conversation List - Scrollable */}
|
|
69
|
+
<div className="flex-1 overflow-y-auto">
|
|
70
|
+
<ConversationList selectedConversationId={selectedConversationId || undefined} onConversationSelect={handleConversationSelect} className="h-full"/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Main Chat Area - Separate scroll container */}
|
|
75
|
+
<div className={`
|
|
76
|
+
flex-1 flex flex-col relative h-full
|
|
77
|
+
${selectedConversationId ? "flex" : "hidden md:flex"}
|
|
78
|
+
`}>
|
|
79
|
+
{selectedConversationId ? (<>
|
|
80
|
+
{/* Swipe indicator for mobile */}
|
|
81
|
+
<div className="md:hidden absolute top-2 left-2 z-10 pointer-events-none">
|
|
82
|
+
<div className="bg-black bg-opacity-20 text-white text-xs px-2 py-1 rounded-full opacity-0 transition-opacity duration-200 swipe-hint">
|
|
83
|
+
← Swipe to go back
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Chat Header - Fixed */}
|
|
88
|
+
<div className="flex-shrink-0">
|
|
89
|
+
<ChatHeader conversationId={selectedConversationId} onBackClick={handleBackToList} onMenuClick={() => setShowSidebar(true)}/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Message List - Scrollable */}
|
|
93
|
+
<div className="flex-1 min-h-0">
|
|
94
|
+
{" "}
|
|
95
|
+
{/* Added min-h-0 here */}
|
|
96
|
+
<MessageList messages={messagesHook.messages} currentUserId={((_a = state.config) === null || _a === void 0 ? void 0 : _a.userId) || ""} conversationId={selectedConversationId} className="h-full"/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Chat Input - Fixed */}
|
|
100
|
+
<div className="flex-shrink-0">
|
|
101
|
+
<ChatInput />
|
|
102
|
+
</div>
|
|
103
|
+
</>) : (<div className="flex-1 flex items-center justify-center text-gray-500 p-4">
|
|
104
|
+
<div className="text-center max-w-sm">
|
|
105
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
|
106
|
+
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
107
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
108
|
+
</svg>
|
|
109
|
+
</div>
|
|
110
|
+
<h3 className="text-lg font-medium text-gray-900 mb-2">Select a conversation</h3>
|
|
111
|
+
<p className="text-gray-500 text-sm sm:text-base">
|
|
112
|
+
Choose a conversation from the sidebar to start messaging
|
|
113
|
+
</p>
|
|
114
|
+
{!state.isConnected && (<p className="text-xs text-gray-400 mt-2">Running in demo mode - WebSocket disabled</p>)}
|
|
115
|
+
<button onClick={() => setShowSidebar(true)} className="mt-4 md:hidden px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|
116
|
+
View Conversations
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>);
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Conversation } from "../types";
|
|
2
|
+
interface ConversationItemProps {
|
|
3
|
+
conversation: Conversation;
|
|
4
|
+
isSelected?: boolean;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function ConversationItem({ conversation, isSelected, onClick }: ConversationItemProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ConversationItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationItem.d.ts","sourceRoot":"","sources":["../../src/components/ConversationItem.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAG5C,UAAU,qBAAqB;IAC7B,YAAY,EAAE,YAAY,CAAA;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,qBAAqB,+BAiE5F"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useChatContext } from "../context/ChatContext";
|
|
3
|
+
export function ConversationItem({ conversation, isSelected, onClick }) {
|
|
4
|
+
var _a;
|
|
5
|
+
const { state } = useChatContext();
|
|
6
|
+
// Get the other participant (not the current user)
|
|
7
|
+
const otherParticipant = conversation.participants.find((p) => { var _a; return p.id !== ((_a = state.config) === null || _a === void 0 ? void 0 : _a.userId); });
|
|
8
|
+
const formatTime = (date) => {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const diff = now.getTime() - date.getTime();
|
|
11
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
12
|
+
if (days === 0) {
|
|
13
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
14
|
+
}
|
|
15
|
+
else if (days === 1) {
|
|
16
|
+
return "Yesterday";
|
|
17
|
+
}
|
|
18
|
+
else if (days < 7) {
|
|
19
|
+
return date.toLocaleDateString([], { weekday: "short" });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return (<div className={`flex items-center p-3 sm:p-4 cursor-pointer hover:bg-gray-50 active:bg-gray-100 transition-colors ${isSelected ? "bg-blue-50 border-r-2 border-blue-500" : ""}`} onClick={onClick}>
|
|
26
|
+
<div className="relative flex-shrink-0">
|
|
27
|
+
<img src={(otherParticipant === null || otherParticipant === void 0 ? void 0 : otherParticipant.avatar) || "/placeholder.svg?height=48&width=48&query=user"} alt={(otherParticipant === null || otherParticipant === void 0 ? void 0 : otherParticipant.name) || "User"} className="w-10 h-10 sm:w-12 sm:h-12 rounded-full object-cover"/>
|
|
28
|
+
{(otherParticipant === null || otherParticipant === void 0 ? void 0 : 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>)}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="ml-3 flex-1 min-w-0">
|
|
32
|
+
<div className="flex items-center justify-between mb-1">
|
|
33
|
+
<h3 className="text-sm sm:text-base font-medium text-gray-900 truncate pr-2">
|
|
34
|
+
{(otherParticipant === null || otherParticipant === void 0 ? void 0 : otherParticipant.name) || "Unknown User"}
|
|
35
|
+
</h3>
|
|
36
|
+
{conversation.lastMessage && (<span className="text-xs text-gray-500 flex-shrink-0">
|
|
37
|
+
{formatTime(conversation.lastMessage.timestamp)}
|
|
38
|
+
</span>)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="flex items-center justify-between">
|
|
42
|
+
<p className="text-xs sm:text-sm text-gray-600 truncate pr-2">
|
|
43
|
+
{((_a = conversation.lastMessage) === null || _a === void 0 ? void 0 : _a.content) || "No messages yet"}
|
|
44
|
+
</p>
|
|
45
|
+
{conversation.unreadCount > 0 && (<span className="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white bg-blue-500 rounded-full flex-shrink-0 min-w-[20px] h-5">
|
|
46
|
+
{conversation.unreadCount > 99 ? "99+" : conversation.unreadCount}
|
|
47
|
+
</span>)}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>);
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ConversationListProps {
|
|
2
|
+
onConversationSelect?: (conversationId: string) => void;
|
|
3
|
+
selectedConversationId?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function ConversationList({ onConversationSelect, selectedConversationId, className, }: ConversationListProps): import("react").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=ConversationList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationList.d.ts","sourceRoot":"","sources":["../../src/components/ConversationList.tsx"],"names":[],"mappings":"AAIA,UAAU,qBAAqB;IAC7B,oBAAoB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACvD,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,oBAAoB,EACpB,sBAAsB,EACtB,SAAc,GACf,EAAE,qBAAqB,+BAiCvB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useConversationList } from "../hooks/useConversationList";
|
|
3
|
+
import { ConversationItem } from "./ConversationItem";
|
|
4
|
+
export function ConversationList({ onConversationSelect, selectedConversationId, className = "", }) {
|
|
5
|
+
const { conversations, isLoading } = useConversationList();
|
|
6
|
+
if (isLoading) {
|
|
7
|
+
return (<div className={`flex flex-col space-y-2 p-3 sm:p-4 ${className}`}>
|
|
8
|
+
{[...Array(5)].map((_, i) => (<div key={i} className="animate-pulse">
|
|
9
|
+
<div className="flex items-center space-x-3 p-3">
|
|
10
|
+
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gray-300 rounded-full"></div>
|
|
11
|
+
<div className="flex-1">
|
|
12
|
+
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
|
|
13
|
+
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>))}
|
|
17
|
+
</div>);
|
|
18
|
+
}
|
|
19
|
+
return (<div className={`flex flex-col ${className}`}>
|
|
20
|
+
{conversations.map((conversation) => (<ConversationItem key={conversation.id} conversation={conversation} isSelected={selectedConversationId === conversation.id} onClick={() => onConversationSelect === null || onConversationSelect === void 0 ? void 0 : onConversationSelect(conversation.id)}/>))}
|
|
21
|
+
</div>);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DateDivider.d.ts","sourceRoot":"","sources":["../../src/components/DateDivider.tsx"],"names":[],"mappings":"AAEA,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAA;IACV,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,gBAAgB,+BA4BlE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export function DateDivider({ date, customLabel }) {
|
|
3
|
+
const formatDate = (date) => {
|
|
4
|
+
if (customLabel)
|
|
5
|
+
return customLabel;
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
8
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
9
|
+
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
10
|
+
if (messageDate.getTime() === today.getTime()) {
|
|
11
|
+
return "Hôm nay";
|
|
12
|
+
}
|
|
13
|
+
else if (messageDate.getTime() === yesterday.getTime()) {
|
|
14
|
+
return "Hôm qua";
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
return date.toLocaleDateString("vi-VN", {
|
|
18
|
+
weekday: "long",
|
|
19
|
+
year: "numeric",
|
|
20
|
+
month: "long",
|
|
21
|
+
day: "numeric",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return (<div className="flex items-center justify-center my-3 sm:my-4">
|
|
26
|
+
<div className="bg-gray-100 text-gray-600 text-xs px-2 py-1 sm:px-3 sm:py-1 rounded-full">{formatDate(date)}</div>
|
|
27
|
+
</div>);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmojiPicker.d.ts","sourceRoot":"","sources":["../../src/components/EmojiPicker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAsKrD,eAAO,MAAM,WAAW,sGAiGvB,CAAA"}
|