From e1c3b44c7f3d1ab4c636d8ae3b896151adff7f89 Mon Sep 17 00:00:00 2001 From: SrashtiChauhan Date: Sun, 10 May 2026 16:19:40 +0530 Subject: [PATCH 1/2] feat(chat): add emoji picker, image upload, and voice message support --- frontend/app/chat/page.tsx | 223 ++++++++++++++++++++++++++++++------- frontend/package.json | 1 + package-lock.json | 22 ++++ 3 files changed, 205 insertions(+), 41 deletions(-) diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 748111d..db6b30c 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useRef } from "react"; import { io, Socket } from "socket.io-client"; +import EmojiPicker from "emoji-picker-react"; +import { Smile, Paperclip, Mic, Square } from "lucide-react"; type Message = { id?: string; @@ -9,6 +11,8 @@ type Message = { text: string; time?: string; status?: string; + image?: string; + audio?: string; reactions?: { [emoji: string]: number }; }; @@ -18,6 +22,13 @@ export default function ChatPage() { const [username, setUsername] = useState(""); const [typingUser, setTypingUser] = useState(""); const [onlineUsers, setOnlineUsers] = useState([]); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + const [isRecording, setIsRecording] = useState(false); +const [audioBlob, setAudioBlob] = useState(null); + +const mediaRecorderRef = useRef(null); +const audioChunksRef = useRef([]); const socketRef = useRef(null); const typingTimeoutRef = useRef(null); @@ -48,6 +59,9 @@ export default function ChatPage() { // NEW MESSAGE socket.on("newMessage", (msg) => { + // prevent duplicate message for sender + if (msg.username === username) return; + setMessages((prev) => [ ...prev, { @@ -59,7 +73,6 @@ socket.on("newMessage", (msg) => { }, ]); - // new logic if (!isAtBottomRef.current) { setUnreadCount((prev) => prev + 1); } @@ -91,14 +104,10 @@ socket.on("newMessage", (msg) => { ); }); socket.on("reactionUpdate", ({ messageId, reactions }) => { - setMessages((prev) => - prev.map((msg) => - msg.id === messageId - ? { ...msg, reactions } - : msg - ) - ); -}); + setMessages((prev) => + prev.map((msg) => + msg.id === messageId ? { ...msg, reactions } : msg )); + }); return () => { socket.disconnect(); @@ -119,7 +128,7 @@ socket.on("newMessage", (msg) => { } }, [messages]); - // MARK ONLY LAST MESSAGE AS SEEN (FIXED) + // MARK ONLY LAST MESSAGE AS SEEN useEffect(() => { if (!socketRef.current) return; @@ -131,18 +140,42 @@ socket.on("newMessage", (msg) => { // SEND async function handleSend() { - if (!input.trim() || !username.trim()) return; - - await fetch("http://localhost:5000/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ text: input, username }), - }); + if ((!input.trim() && !selectedImage && !audioBlob) || !username.trim()) return; + + const localMessage: Message = { + id: Date.now().toString(), + user: username, + text: input, + image: selectedImage || undefined, + audio: audioBlob || undefined, + time: new Date().toLocaleTimeString(), + status: "sent", +}; - setInput(""); - } + // immediately show message in UI + setMessages((prev) => [...prev, localMessage]); + + // send text to backend + await fetch("http://localhost:5000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: input, + username, + }), + }); + + // clear inputs + setInput(""); + setSelectedImage(null); + setAudioBlob(null); + // auto scroll + bottomRef.current?.scrollIntoView({ + behavior: "smooth", + }); +} // TYPING function handleTyping(e: any) { @@ -152,12 +185,64 @@ socket.on("newMessage", (msg) => { socketRef.current.emit("typing", username); } } - function handleReact(messageId: string | undefined, emoji: string) { - if (!messageId) return; + //emoji select function + function handleEmojiClick(emojiData: any) { + setInput((prev) => prev + emojiData.emoji); + setShowEmojiPicker(false); + } + function handleImageUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + + if (!file) return; + + const imageUrl = URL.createObjectURL(file); + setSelectedImage(imageUrl); + } + + async function startRecording() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + + const mediaRecorder = new MediaRecorder(stream); + + mediaRecorderRef.current = mediaRecorder; + + audioChunksRef.current = []; + + mediaRecorder.ondataavailable = (event) => { + audioChunksRef.current.push(event.data); + }; + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(audioChunksRef.current, { + type: "audio/webm", + }); + + const audioUrl = URL.createObjectURL(audioBlob); + + setAudioBlob(audioUrl); + }; - // send reaction to server - socketRef.current?.emit("react", { messageId, emoji, username,}); + mediaRecorder.start(); + + setIsRecording(true); + } catch (error) { + console.error("Recording failed:", error); + } } +function stopRecording() { + mediaRecorderRef.current?.stop(); + setIsRecording(false); +} + + function handleReact(messageId: string | undefined, emoji: string) { + if (!messageId) return; + + // send reaction to server + socketRef.current?.emit("react", { messageId, emoji, username,}); + } @@ -223,14 +308,9 @@ return ( const isMe = msg.user === username; return ( -
+
{msg.user}

)} -

{msg.text}

-
+ {msg.text &&

{msg.text}

} + + {msg.image && ( + Shared image + )} + {msg.audio && ( + + )} + +
{["👍", "❤️", "😂"].map((emoji) => ( + + {showEmojiPicker && ( +
+ +
+ )} +
+ + + + + - ))} - {/* counts will show here*/} - {msg.reactions && - Object.entries(msg.reactions).filter(([_, count]) => count > 0).map(([emoji, count]) => ( - + {/* USERNAME */} + {!isMe && !isSameUser && ( +

+ {msg.user} +

+ )} + + {/* TEXT */} + {msg.text && ( +

+ {msg.text} +

+ )} + + {/* IMAGE */} + {msg.image && msg.image.startsWith("blob:") && ( + Shared image + )} + + {/* AUDIO */} + {msg.audio && msg.audio.startsWith("blob:") && ( + + )} + + {/* REACTIONS */} +
+ {["👍", "❤️", "😂"].map((emoji) => ( + + ))} + + {msg.reactions && + Object.entries(msg.reactions) + .filter(([_, count]) => count > 0) + .map(([emoji, count]) => ( + {emoji} {count} ))} -
- -

- {msg.time} {msg.status === "seen" ? "✓✓" : "✓"} -

+ + {/* TIME */} +

+ {msg.time}{" "} + {msg.status === "seen" ? "✓✓" : "✓"} +

- ); - })} +
+ ); + })} + +
+
-
+ {/* UNREAD MESSAGE */} + {unreadCount > 0 && ( +
+
- {unreadCount > 0 && ( -
- -
-)} - - {/* TYPING */} - {typingUser && typingUser !== username && ( -

- {typingUser} is typing... -

- )} - {selectedImage && ( -
- Preview -
-)} -{audioBlob && ( -
- -
-)} + )} - {/* INPUT */} -
+ {/* TYPING */} + {typingUser && typingUser !== username && ( +

+ {typingUser} is typing... +

+ )} -
- + {/* IMAGE PREVIEW */} + {selectedImage && ( +
+ Preview +
+ )} - {showEmojiPicker && ( -
- + {/* RECORDING */} + {isRecording && ( +

+ Recording voice message... +

+ )} + + {/* AUDIO PREVIEW */} + {audioBlob && ( +
+
)} -
- - - - - + + {/* INPUT SECTION */} +
+ + {/* EMOJI */} +
+ + {showEmojiPicker && ( +
+ +
+ )}
+ + {/* FILE UPLOAD */} + + + {/* VOICE */} + + + {/* MESSAGE INPUT */} + + + {/* SEND BUTTON */} +
- ); +
+); } \ No newline at end of file