Skip to content

Commit c869556

Browse files
committed
Merge branch 'main' of github.com:EcstaticFly/Chatzy
2 parents 61cefa4 + 6c792e0 commit c869556

11 files changed

Lines changed: 537 additions & 53 deletions

File tree

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Chatzy is a **real-time chat platform** that enables users to **send messages, s
44

55
🔗 **Live Demo:** [Chatzy](https://chatzy-mxp8.onrender.com/)
66
📂 **Source Code:** [GitHub](https://github.com/EcstaticFly/Chatzy.git)
7-
🐳 **Docker Hub:** [suyash310/chatzy](https://hub.docker.com/u/suyash310)
7+
🐳 **Docker Hub:** [suyash310](https://hub.docker.com/u/suyash310)
88

99
## ✨ Features
1010
- **💬 Real-Time Messaging** – Instant text & image sharing via **Socket.io**.
@@ -177,9 +177,7 @@ docker compose down -v
177177

178178
### Setup Steps
179179

180-
**1. Copy Production Compose File (docker-compose.prod.yaml):**
181-
182-
Copy from here: [https://gist.github.com/EcstaticFly/1ec0a944465c98a5f0f28b8cff04a940](https://gist.github.com/EcstaticFly/1ec0a944465c98a5f0f28b8cff04a940)
180+
**1. Download Production Compose File (docker-compose.prod.yaml):** [Download](https://github.com/EcstaticFly/Chatzy/blob/main/docker-compose.prod.yaml)
183181

184182
```bash
185183
# Create a directory
@@ -240,4 +238,4 @@ Feel free to **fork** the repo and submit a **pull request**.
240238
This project is licensed under the **GNU GENERAL PUBLIC LICENSE v3**.
241239

242240
## 📬 Contact
243-
For inquiries, reach out to me at [Suyash Pandey](mailto:suyash.2023ug1100@iiitranchi.ac.in).
241+
For inquiries, reach out to me at [Suyash Pandey](mailto:suyash.2023ug1100@iiitranchi.ac.in).

client/src/components/chat-container.jsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MessageInput } from "./messageInput.jsx";
55
import { MessageSkeleton } from "./skeletons/messageSkeleton.jsx";
66
import { authStore } from "../store/authStore.js";
77
import { formatMessageTime } from "../configs/utils.js";
8-
import { X } from "lucide-react";
8+
import { X, FileText, Download, ArrowUpRightFromSquare } from "lucide-react";
99
import ReactMarkdown from "react-markdown";
1010

1111
export default function ChatContainer() {
@@ -38,6 +38,22 @@ export default function ChatContainer() {
3838
messageEndRef.current.scrollIntoView({ behavior: "smooth" });
3939
}, [messages]);
4040

41+
const handleDocumentClick = (e, url) => {
42+
e.preventDefault();
43+
e.stopPropagation();
44+
if (url) {
45+
window.open(url, "_blank", "noopener,noreferrer");
46+
}
47+
};
48+
49+
const formatFileSize = (bytes) => {
50+
if (!bytes || bytes === 0) return "0 Bytes";
51+
const k = 1024;
52+
const sizes = ["Bytes", "KB", "MB", "GB"];
53+
const i = Math.floor(Math.log(bytes) / Math.log(k));
54+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
55+
};
56+
4157
if (isChatLoading) {
4258
return (
4359
<div className="flex-1 flex flex-col overflow-auto">
@@ -76,6 +92,33 @@ export default function ChatContainer() {
7692
onClick={() => setSelectedImage(message)}
7793
/>
7894
)}
95+
96+
{message?.document && (
97+
<div
98+
className={`flex items-center gap-3 p-3 rounded-lg mb-2 cursor-pointer transition-colors ${
99+
message?.senderId === user._id
100+
? "bg-primary-content/10 hover:bg-primary-content/20"
101+
: "bg-base-300 hover:bg-base-300/80"
102+
}`}
103+
onClick={(e) => handleDocumentClick(e, message.document.url)}
104+
>
105+
<div className="flex-shrink-0">
106+
<FileText size={32} className="text-blue-500" />
107+
</div>
108+
<div className="flex-1 min-w-0">
109+
<p className="font-medium text-sm truncate">
110+
{message.document.name || "Document"}
111+
</p>
112+
<p className="text-xs opacity-70">
113+
{formatFileSize(message.document.size)}
114+
</p>
115+
</div>
116+
<div className="flex-shrink-0">
117+
<ArrowUpRightFromSquare size={20} className="opacity-70" />
118+
</div>
119+
</div>
120+
)}
121+
79122
{message.text && (
80123
<div className="text-sm prose prose-sm max-w-none">
81124
<ReactMarkdown>{message.text}</ReactMarkdown>

client/src/components/messageInput.jsx

Lines changed: 177 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,139 @@
1-
import { useRef, useState } from "react";
1+
import { useRef, useState, useEffect } from "react";
22
import { chatStore } from "../store/chatStore";
3-
import { X, Image, Send, Loader } from "lucide-react";
3+
import { X, Image, Send, Loader, FileText, Paperclip } from "lucide-react";
44
import { toast } from "react-toastify";
55

66
const chatBotId = "67a5af796174659ba813c735";
77

8+
// File size limits (in MB)
9+
const FILE_SIZE_LIMITS = {
10+
image: 3,
11+
document: 5,
12+
};
13+
814
export const MessageInput = () => {
9-
const { sendMessage, isSendingMessage } = chatStore();
10-
const { selectedUser } = chatStore();
15+
const { sendMessage, isSendingMessage, selectedUser } = chatStore();
1116
const [text, setText] = useState("");
17+
const [imageFile, setImageFile] = useState(null);
1218
const [imagePreview, setImagePreview] = useState(null);
13-
const fileInputRef = useRef(null);
19+
const [documentFile, setDocumentFile] = useState(null);
20+
const [documentPreview, setDocumentPreview] = useState(null);
21+
const [showAttachMenu, setShowAttachMenu] = useState(false);
22+
const imageInputRef = useRef(null);
23+
const documentInputRef = useRef(null);
24+
const attachMenuRef = useRef(null);
25+
26+
useEffect(() => {
27+
const handleClickOutside = (event) => {
28+
if (attachMenuRef.current && !attachMenuRef.current.contains(event.target)) {
29+
setShowAttachMenu(false);
30+
}
31+
};
32+
33+
document.addEventListener("mousedown", handleClickOutside);
34+
return () => document.removeEventListener("mousedown", handleClickOutside);
35+
}, []);
1436

1537
const handleImageChange = (event) => {
1638
const file = event.target.files[0];
39+
if (!file) return;
40+
1741
if (!file.type.startsWith("image/")) {
1842
toast.error("Please select an image file.");
1943
return;
2044
}
45+
46+
if (file.size > FILE_SIZE_LIMITS.image * 1024 * 1024) {
47+
toast.error(`Image size must be less than ${FILE_SIZE_LIMITS.image}MB`);
48+
return;
49+
}
50+
51+
setImageFile(file);
2152
const reader = new FileReader();
2253
reader.onloadend = () => {
2354
setImagePreview(reader.result);
2455
};
2556
reader.readAsDataURL(file);
57+
setShowAttachMenu(false);
58+
};
59+
60+
const handleDocumentChange = (event) => {
61+
const file = event.target.files[0];
62+
if (!file) return;
63+
64+
if (file.type !== "application/pdf") {
65+
toast.error("Only PDF files are allowed.");
66+
return;
67+
}
68+
69+
if (file.size > FILE_SIZE_LIMITS.document * 1024 * 1024) {
70+
toast.error(
71+
`Document size must be less than ${FILE_SIZE_LIMITS.document}MB`
72+
);
73+
return;
74+
}
75+
76+
setDocumentFile(file);
77+
setDocumentPreview({
78+
name: file.name,
79+
size: file.size,
80+
type: file.type,
81+
});
82+
setShowAttachMenu(false);
2683
};
2784

2885
const removeImage = () => {
86+
setImageFile(null);
2987
setImagePreview(null);
30-
if (fileInputRef.current) fileInputRef.current.value = null;
88+
if (imageInputRef.current) imageInputRef.current.value = null;
89+
};
90+
91+
const removeDocument = () => {
92+
setDocumentFile(null);
93+
setDocumentPreview(null);
94+
if (documentInputRef.current) documentInputRef.current.value = null;
95+
};
96+
97+
const handleAttachClick = () => {
98+
setShowAttachMenu(!showAttachMenu);
99+
};
100+
101+
const handleImageSelect = () => {
102+
imageInputRef.current?.click();
103+
};
104+
105+
const handleDocumentSelect = () => {
106+
documentInputRef.current?.click();
31107
};
32108

33109
const handleSendMessage = async (event) => {
34110
event.preventDefault();
35-
if (!text.trim() && !imagePreview) return;
111+
if (!text.trim() && !imageFile && !documentFile) return;
112+
36113
try {
37-
await sendMessage({
38-
text: text.trim(),
39-
image: imagePreview,
40-
});
114+
const formData = new FormData();
115+
if (text.trim()) formData.append("text", text.trim());
116+
if (imageFile) formData.append("image", imageFile);
117+
if (documentFile) formData.append("document", documentFile);
118+
119+
await sendMessage(formData);
120+
41121
setText("");
122+
setImageFile(null);
42123
setImagePreview(null);
43-
if (fileInputRef.current) fileInputRef.current.value = null;
124+
setDocumentFile(null);
125+
setDocumentPreview(null);
126+
if (imageInputRef.current) imageInputRef.current.value = null;
127+
if (documentInputRef.current) documentInputRef.current.value = null;
44128
} catch (error) {
45129
console.error("Message not sent.", error);
130+
toast.error("Failed to send message");
46131
}
47132
};
133+
48134
return (
49135
<div className="p-4 w-full">
136+
{/* Image Preview */}
50137
{imagePreview && (
51138
<div className="mb-3 flex items-center gap-2">
52139
<div className="relative">
@@ -66,6 +153,30 @@ export const MessageInput = () => {
66153
</div>
67154
)}
68155

156+
{/* Document Preview */}
157+
{documentPreview && (
158+
<div className="mb-3 flex items-center gap-2">
159+
<div className="relative flex items-center gap-2 p-2 bg-base-200 rounded-lg border border-zinc-700">
160+
<FileText className="size-8 text-primary" />
161+
<div className="flex-1">
162+
<p className="text-sm font-medium truncate max-w-[200px]">
163+
{documentPreview.name}
164+
</p>
165+
<p className="text-xs text-zinc-500">
166+
{(documentPreview.size / 1024).toFixed(2)} KB
167+
</p>
168+
</div>
169+
<button
170+
onClick={removeDocument}
171+
className="size-5 rounded-full bg-base-300 flex items-center justify-center hover:bg-error hover:text-error-content"
172+
type="button"
173+
>
174+
<X className="size-3" />
175+
</button>
176+
</div>
177+
</div>
178+
)}
179+
69180
<form onSubmit={handleSendMessage} className="flex items-center gap-2">
70181
<div className="flex-1 flex gap-2">
71182
<input
@@ -75,31 +186,71 @@ export const MessageInput = () => {
75186
value={text}
76187
onChange={(event) => setText(event.target.value)}
77188
/>
189+
78190
<input
79191
type="file"
80192
className="hidden"
81193
accept="image/*"
82-
ref={fileInputRef}
194+
ref={imageInputRef}
83195
onChange={handleImageChange}
84196
/>
85-
{selectedUser._id !== chatBotId ? (
86-
<button
87-
type="button"
88-
className={`hidden sm:flex btn btn-circle ${
89-
imagePreview ? "text-emerald-500" : "text-zinc-400"
90-
}`}
91-
onClick={() => fileInputRef.current?.click()}
92-
>
93-
<Image size={20} />
94-
</button>
95-
) : null}
197+
<input
198+
type="file"
199+
className="hidden"
200+
accept=".pdf"
201+
ref={documentInputRef}
202+
onChange={handleDocumentChange}
203+
/>
204+
205+
{selectedUser._id !== chatBotId && (
206+
<div className="relative" ref={attachMenuRef}>
207+
<button
208+
type="button"
209+
className={`btn btn-circle ${
210+
imagePreview || documentPreview ? "text-emerald-500" : "text-zinc-400"
211+
}`}
212+
onClick={handleAttachClick}
213+
title="Attach File"
214+
>
215+
<Paperclip size={20} />
216+
</button>
217+
218+
{/* Attachment Menu */}
219+
{showAttachMenu && (
220+
<div className="absolute bottom-full mb-2 right-0 bg-base-200 rounded-lg shadow-lg border border-zinc-700 py-1 min-w-[160px] z-10">
221+
<button
222+
type="button"
223+
className="w-full px-4 py-2 flex items-center gap-3 hover:bg-base-300 transition-colors text-left"
224+
onClick={handleImageSelect}
225+
>
226+
<Image size={18} className="text-emerald-500" />
227+
<span className="text-sm">Image</span>
228+
</button>
229+
<button
230+
type="button"
231+
className="w-full px-4 py-2 flex items-center gap-3 hover:bg-base-300 transition-colors text-left"
232+
onClick={handleDocumentSelect}
233+
>
234+
<FileText size={18} className="text-blue-500" />
235+
<span className="text-sm">PDF Document</span>
236+
</button>
237+
</div>
238+
)}
239+
</div>
240+
)}
96241
</div>
97242
<button
98243
type="submit"
99244
className="btn btn-circle btn-sm btn-primary"
100-
disabled={(!text.trim() && !imagePreview) || isSendingMessage}
245+
disabled={
246+
(!text.trim() && !imageFile && !documentFile) || isSendingMessage
247+
}
101248
>
102-
{isSendingMessage ? <Loader size={22} /> : <Send size={22} />}
249+
{isSendingMessage ? (
250+
<Loader size={22} className="animate-spin" />
251+
) : (
252+
<Send size={22} />
253+
)}
103254
</button>
104255
</form>
105256
</div>

client/src/store/chatStore.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,24 @@ export const chatStore = create((set, get) => ({
6464
}
6565
},
6666

67-
sendMessage: async (messageData) => {
67+
sendMessage: async (formData) => {
6868
set({ isSendingMessage: true });
6969
const { selectedUser, messages } = get();
7070
// console.log(selectedUser);
7171
try {
72-
const response = await axiosInstance.post(
72+
const res = await axiosInstance.post(
7373
`/messages/send/${selectedUser._id}`,
74-
messageData
75-
);
76-
set({ messages: [...messages, response.data] });
77-
} catch (e) {
78-
console.log(e);
79-
toast.error(
80-
e.response.data.message || "File size should be less than 5mb"
74+
formData,
75+
{
76+
headers: {
77+
"Content-Type": "multipart/form-data",
78+
},
79+
}
8180
);
81+
set({ messages: [...messages, res.data] });
82+
} catch (error) {
83+
console.error("Error sending message:", error);
84+
toast.error(error.response?.data?.message || "Failed to send message");
8285
} finally {
8386
set({ isSendingMessage: false });
8487
}

0 commit comments

Comments
 (0)