{message.deletedForEveryone ? (
@@ -378,7 +405,9 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
{message.isForwarded && (
@@ -400,13 +429,18 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
onClick={() => {
const id = message.replyTo._id;
setHighlightedMessage(id);
+
document
.getElementById(`msg-${id}`)
?.scrollIntoView({
behavior: "smooth",
block: "center",
});
- setTimeout(() => setHighlightedMessage(null), 1200);
+
+ setTimeout(
+ () => setHighlightedMessage(null),
+ 1200
+ );
}}
className={`mb-1.5 px-2.5 py-1 rounded-xl text-xs border-l-3 cursor-pointer transition-colors ${
isMe
@@ -421,12 +455,32 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
>
{message.replyTo.senderId?.fullName || "User"}
+
- {message.replyTo.text || "Media attachment"}
+ {message.replyTo.text ||
+ (message.replyTo.sticker
+ ? "✨ Sticker"
+ : "Media attachment")}
)}
+ {/* Sticker */}
+ {message.sticker && (
+
+

+
+ )}
+
{/* Media Attachments */}
{(message.image ||
message.audio ||
@@ -439,12 +493,14 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
alt="attachment"
/>
)}
+
{message.audio && (
)}
+
{message.file?.url && (
)}
@@ -461,6 +517,7 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
Toxic
)}
+
{message.spam && (
@@ -525,7 +582,7 @@ const MessageBubble = ({ message, sender, isMe, chatId }) => {
))}
)}
-
+
diff --git a/frontend/src/components/MessageInput.jsx b/frontend/src/components/MessageInput.jsx
index c8cb9006..b4265ce6 100644
--- a/frontend/src/components/MessageInput.jsx
+++ b/frontend/src/components/MessageInput.jsx
@@ -9,9 +9,11 @@ import {
Smile,
Clapperboard,
BarChart2,
+ Sticker as StickerIcon,
} from "lucide-react";
import toast from "react-hot-toast";
import { useChatStore } from "../store/useChatStore";
+import StickerPicker from "./StickerPicker";
import EmojiPicker from "emoji-picker-react";
import GifPicker from "./GifPicker";
import PollCreatorModal from "./PollCreatorModal";
@@ -24,11 +26,13 @@ const MessageInput = () => {
const [recording, setRecording] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [showGifPicker, setShowGifPicker] = useState(false);
+ const [showStickerPicker, setShowStickerPicker] = useState(false);
const [dragActive, setDragActive] = useState(false);
const [showPollModal, setShowPollModal] = useState(false);
const emojiRef = useRef(null);
const gifRef = useRef(null);
+ const stickerRef = useRef(null);
const fileRef = useRef(null);
const imageRef = useRef(null);
const mediaRecorderRef = useRef(null);
@@ -50,7 +54,10 @@ const MessageInput = () => {
} = useChatStore();
const isAI = selectedChatType === "private" && selectedUser?.isAI;
- const hasContent = Boolean(text.trim() || imagePreview || fileData || audioData);
+
+ const hasContent = Boolean(
+ text.trim() || imagePreview || fileData || audioData
+ );
useEffect(() => {
return () => {
@@ -64,13 +71,23 @@ const MessageInput = () => {
if (emojiRef.current && !emojiRef.current.contains(event.target)) {
setShowEmojiPicker(false);
}
+
if (gifRef.current && !gifRef.current.contains(event.target)) {
setShowGifPicker(false);
}
+
+ if (
+ stickerRef.current &&
+ !stickerRef.current.contains(event.target)
+ ) {
+ setShowStickerPicker(false);
+ }
};
document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
+
+ return () =>
+ document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleEmojiClick = (emojiData) => {
@@ -79,13 +96,16 @@ const MessageInput = () => {
const handleImageChange = (e) => {
if (isAI) return;
+
const file = e.target.files?.[0];
+
if (!file || !file.type.startsWith("image/")) {
toast.error("Select an image");
return;
}
const reader = new FileReader();
+
reader.onload = () => setImagePreview(reader.result);
reader.readAsDataURL(file);
};
@@ -95,12 +115,42 @@ const MessageInput = () => {
setShowGifPicker(false);
};
+ const handleStickerSelect = async (stickerUrl) => {
+ setShowStickerPicker(false);
+
+ if (isAI) {
+ toast.error("AI assistant cannot receive stickers");
+ return;
+ }
+
+ const payload = {
+ text: "",
+ sticker: stickerUrl,
+ replyTo: replyingTo?._id || null,
+ };
+
+ try {
+ if (selectedChatType === "group") {
+ await sendGroupMessage(payload);
+ } else {
+ await sendMessage(payload);
+ }
+
+ clearReplyingTo();
+ } catch (error) {
+ toast.error("Failed to send sticker");
+ }
+ };
+
const handleFileChange = (e) => {
if (isAI) return;
+
const file = e.target.files?.[0];
+
if (!file) return;
const reader = new FileReader();
+
reader.onload = () => {
setFileData({
base64: reader.result,
@@ -109,13 +159,17 @@ const MessageInput = () => {
size: file.size,
});
};
+
reader.readAsDataURL(file);
};
const startRecording = async () => {
if (isAI) return;
+
try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const stream =
+ await navigator.mediaDevices.getUserMedia({ audio: true });
+
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
@@ -126,10 +180,15 @@ const MessageInput = () => {
};
mediaRecorder.onstop = () => {
- const blob = new Blob(audioChunksRef.current, { type: "audio/webm" });
+ const blob = new Blob(audioChunksRef.current, {
+ type: "audio/webm",
+ });
+
const reader = new FileReader();
+
reader.onload = () => setAudioData(reader.result);
reader.readAsDataURL(blob);
+
stream.getTracks().forEach((track) => track.stop());
};
@@ -147,6 +206,7 @@ const MessageInput = () => {
const handleTyping = (value) => {
setText(value);
+
if (isAI) return;
if (!value.trim()) {
@@ -155,12 +215,15 @@ const MessageInput = () => {
}
startTyping();
+
clearTimeout(typingTimeoutRef.current);
+
typingTimeoutRef.current = setTimeout(stopTyping, 1200);
};
const handleSend = async (e) => {
e.preventDefault();
+
if (!hasContent) return;
stopTyping();
@@ -176,15 +239,18 @@ const MessageInput = () => {
replyTo: replyingTo?._id || null,
};
- selectedChatType === "group"
- ? await sendGroupMessage(payload)
- : await sendMessage(payload);
+ if (selectedChatType === "group") {
+ await sendGroupMessage(payload);
+ } else {
+ await sendMessage(payload);
+ }
}
setText("");
setImagePreview(null);
setFileData(null);
setAudioData(null);
+ clearReplyingTo();
};
const handleDragOver = (e) => {
@@ -201,14 +267,17 @@ const MessageInput = () => {
setDragActive(false);
const file = e.dataTransfer.files?.[0];
+
if (!file) return;
if (file.type.startsWith("image/")) {
const reader = new FileReader();
+
reader.onload = () => setImagePreview(reader.result);
reader.readAsDataURL(file);
} else {
const reader = new FileReader();
+
reader.onload = () => {
setFileData({
base64: reader.result,
@@ -217,6 +286,7 @@ const MessageInput = () => {
size: file.size,
});
};
+
reader.readAsDataURL(file);
}
};
@@ -243,9 +313,18 @@ const MessageInput = () => {
{replyingTo && (