From 77b246b5652ae1bd4779fe613c0d060c5098d8d2 Mon Sep 17 00:00:00 2001 From: Rene Date: Fri, 26 Jun 2026 16:35:31 +0200 Subject: [PATCH] Attachment fixed --- scripts/run-python.mjs | 13 ++ src/app/(dashboard)/chats/page.tsx | 201 +++++++++++++----- .../api/conversations/[id]/messages/route.ts | 72 +++++++ src/app/api/conversations/route.ts | 13 +- 4 files changed, 245 insertions(+), 54 deletions(-) diff --git a/scripts/run-python.mjs b/scripts/run-python.mjs index ead08c7..fc0a2db 100644 --- a/scripts/run-python.mjs +++ b/scripts/run-python.mjs @@ -6,8 +6,21 @@ import { execSync, spawn } from "node:child_process" import { platform } from "node:os" +import { statSync } from "node:fs" function detectPython() { + const commonPaths = [ + `${process.env.LOCALAPPDATA}\\Programs\\Python\\Python313\\python.exe`, + `${process.env.LOCALAPPDATA}\\Programs\\Python\\Python312\\python.exe`, + `${process.env.ProgramFiles}\\Python313\\python.exe`, + `${process.env.ProgramFiles}\\Python312\\python.exe`, + ] + for (const p of commonPaths) { + try { + statSync(p) + return p + } catch {} + } const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"] for (const cmd of candidates) { try { diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index deef9aa..8211be4 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -17,8 +17,9 @@ import { import { Search, Send, Phone, Video, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, - CornerDownRight, Forward, Pencil, Download, Undo2, + CornerDownRight, Forward, Pencil, Download, Undo2, FolderOpen, Mail, } from "lucide-react" +import { hasBlockedCodeExtension, filterBlockedFiles } from "@/lib/blocked-extensions" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, @@ -126,6 +127,7 @@ export default function ChatsPage() { const [activeChat, setActiveChat] = useState(null) const [messageInput, setMessageInput] = useState("") const [showEmojiPicker, setShowEmojiPicker] = useState(false) + const [showAttachMenu, setShowAttachMenu] = useState(false) const [attachments, setAttachments] = useState([]) const [panelWidth, setPanelWidth] = useState(320) const [isResizing, setIsResizing] = useState(false) @@ -157,9 +159,12 @@ export default function ChatsPage() { const [searchingUsers, setSearchingUsers] = useState(false) const [unreadMap, setUnreadMap] = useState>(new Map()) const [previewAvatarUrl, setPreviewAvatarUrl] = useState(null) + const [previewImageUrl, setPreviewImageUrl] = useState(null) const fileInputRef = useRef(null) const textareaRef = useRef(null) const emojiPickerRef = useRef(null) + const isSendingRef = useRef(false) + const attachMenuRef = useRef(null) const messagesEndRef = useRef(null) const mediaRecorderRef = useRef(null) const recordingChunksRef = useRef([]) @@ -252,6 +257,17 @@ const formatPreviewContent = (content: string) => { setMessageAttachments((a) => { const n = new Map(a) for (const k of n.keys()) if (!validMsgIds.has(k)) n.delete(k) + // Restore persisted file attachments from JSON content + for (const msgs of next.values()) { + for (const m of msgs) { + try { + const parsed = JSON.parse(m.content) + if (parsed.fileAttachments && Array.isArray(parsed.fileAttachments)) { + n.set(m.id, parsed.fileAttachments) + } + } catch {} + } + } return n }) setReplyMap((r) => { @@ -301,6 +317,26 @@ const formatPreviewContent = (content: string) => { return () => document.removeEventListener("mousedown", handleClickOutside) }, []) + // Strip blocked files from attachments on every change + useEffect(() => { + if (attachments.length === 0) return + const { allowed, rejected } = filterBlockedFiles(attachments) + if (rejected.length > 0) { + toast.error(`Blocked file type: ${rejected[0].name}`) + setAttachments(allowed) + } + }, [attachments]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (attachMenuRef.current && !attachMenuRef.current.contains(e.target as Node)) { + setShowAttachMenu(false) + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + // revoke blob URLs on unmount useEffect(() => { return () => { @@ -408,14 +444,18 @@ const formatPreviewContent = (content: string) => { const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB const handleFileSelect = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files ?? []) - const oversized = files.filter((f) => f.size > MAX_FILE_SIZE) + const raw = Array.from(e.target.files ?? []) + const oversized = raw.filter((f) => f.size > MAX_FILE_SIZE) if (oversized.length > 0) { toast.error(`${oversized[0].name} exceeds the 10MB file size limit`) if (e.target) e.target.value = "" return } - setAttachments((prev) => [...prev, ...files]) + const { allowed, rejected } = filterBlockedFiles(raw) + if (rejected.length > 0) { + toast.error(`Blocked file type: ${rejected[0].name}`) + } + setAttachments((prev) => [...prev, ...allowed]) if (e.target) e.target.value = "" } @@ -425,7 +465,11 @@ const formatPreviewContent = (content: string) => { const addMessageToChat = async (content: string, voice?: { url: string; duration: number }, replyTo?: { senderName: string; content: string }, fileAttachments?: { name: string; type: string; url: string }[]) => { if (!activeChat || !user) return - const payload = content + const payload = voice + ? "[Voice message]" + : fileAttachments && fileAttachments.length > 0 + ? JSON.stringify({ text: content, fileAttachments }) + : content try { const res = await fetch(`/api/conversations/${activeChat}/messages`, { method: "POST", @@ -444,7 +488,7 @@ const formatPreviewContent = (content: string) => { setConversations((prev) => { const updated = prev.map((conv) => conv.id === activeChat - ? { ...conv, lastMessage: content, lastMessageTime: "Just now" } + ? { ...conv, lastMessage: getDisplayContent(content), lastMessageTime: "Just now" } : conv ) const idx = updated.findIndex((c) => c.id === activeChat) @@ -486,37 +530,47 @@ const formatPreviewContent = (content: string) => { const handleSend = async (e: React.FormEvent) => { e.preventDefault() if (!messageInput.trim() && attachments.length === 0) return - if (editingMessage) { - setConversationMessages((prev) => { - const next = new Map(prev) - const msgs = (next.get(activeChat || "") || []).map((m) => - m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m - ) - next.set(activeChat || "", msgs) - return next - }) - setEditingMessage(null) - setMessageInput("") - setTimeout(() => autoResizeTextarea(), 0) + if (isSendingRef.current) return + // Check blocked files before sending + if (attachments.some((f) => hasBlockedCodeExtension(f.name))) { + toast.error("Remove blocked file types before sending") return } - const fileAttachments = attachments.length > 0 - ? attachments.map((f) => { - const url = URL.createObjectURL(f) - blobUrlsRef.current.push(url) - return { name: f.name, type: f.type, url } + isSendingRef.current = true + try { + if (editingMessage) { + setConversationMessages((prev) => { + const next = new Map(prev) + const msgs = (next.get(activeChat || "") || []).map((m) => + m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m + ) + next.set(activeChat || "", msgs) + return next }) - : undefined - if (replyingTo) { - const replySender = replyingTo.senderId === user.id ? user.name : otherParticipant(conversation!).name - await addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) - setReplyingTo(null) - } else { - await addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) + setEditingMessage(null) + setMessageInput("") + setTimeout(() => autoResizeTextarea(), 0) + return + } + const fileAttachments = attachments.length > 0 + ? await Promise.all(attachments.map(async (f) => { + const url = await fileToDataURL(f) + return { name: f.name, type: f.type, url } + })) + : undefined + if (replyingTo) { + const replySender = replyingTo.senderId === user.id ? user.name : otherParticipant(conversation!).name + await addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) + setReplyingTo(null) + } else { + await addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) + } + setMessageInput("") + setTimeout(() => autoResizeTextarea(), 0) + setAttachments([]) + } finally { + isSendingRef.current = false } - setMessageInput("") - setTimeout(() => autoResizeTextarea(), 0) - setAttachments([]) } const handleDeleteMessage = async (messageId: string) => { @@ -555,7 +609,7 @@ const formatPreviewContent = (content: string) => { setConversations((prev) => prev.map((conv) => conv.id === activeChat - ? { ...conv, lastMessage: prevMessages.filter((m) => m.id !== messageId).at(-1)?.content ?? conv.lastMessage } + ? { ...conv, lastMessage: getDisplayContent(prevMessages.filter((m) => m.id !== messageId).at(-1)?.content ?? "") } : conv ) ) @@ -711,6 +765,21 @@ const formatPreviewContent = (content: string) => { const isImageFile = (file: File) => file.type.startsWith("image/") + const getDisplayContent = (content: string) => { + try { + const parsed = JSON.parse(content) + if (parsed.text !== undefined) return parsed.text + } catch {} + return content + } + + const fileToDataURL = (file: File): Promise => new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + reader.readAsDataURL(file) + }) + return (
{/* Conversations list - left panel */} @@ -977,26 +1046,30 @@ const formatPreviewContent = (content: string) => { {files.map((file, i) => file.type.startsWith("image/") ? (
- {file.name} window.open(file.url, "_blank")} /> + {file.name} setPreviewImageUrl(file.url)} />
) : ( - - - {file.name} - - +
+ + + + +
) )}
) : null })()} - {isOnlyEmoji(msg.content) ? ( - {msg.content.trim()} + {isOnlyEmoji(getDisplayContent(msg.content)) ? ( + {getDisplayContent(msg.content).trim()} ) : ( - msg.content + getDisplayContent(msg.content) )} )} @@ -1065,9 +1138,27 @@ const formatPreviewContent = (content: string) => { )}
- +
+ + {showAttachMenu && ( +
+ + + + +
+ )} +
{isRecording ? (
@@ -1102,7 +1193,7 @@ const formatPreviewContent = (content: string) => {
) : ( <> -