diff --git a/database/migrations/027_pin_conversations.sql b/database/migrations/027_pin_conversations.sql new file mode 100644 index 0000000..e43a068 --- /dev/null +++ b/database/migrations/027_pin_conversations.sql @@ -0,0 +1,2 @@ +-- Add pinned_at column to conversation_participants for pinning chats +ALTER TABLE conversation_participants ADD COLUMN IF NOT EXISTS pinned_at TIMESTAMPTZ; diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index 568e86e..d27a467 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -18,6 +18,7 @@ import { Search, Send, Phone, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, CornerDownRight, Forward, Pencil, Download, Undo2, CalendarDays, Loader2, FolderOpen, Mail, + Pin, PinOff, } from "lucide-react" import { hasBlockedCodeExtension, filterBlockedFiles } from "@/lib/blocked-extensions" import { @@ -211,8 +212,15 @@ const formatPreviewContent = (content: string) => { const messages = useMemo(() => conversationMessages.get(activeChat || "") || [], [conversationMessages, activeChat]) const conversation = useMemo(() => conversations.find((c) => c.id === activeChat), [conversations, activeChat]) const filteredConversations = useMemo(() => { - if (!searchQuery.trim()) return conversations - return conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase())) + let list = conversations + if (searchQuery.trim()) { + list = conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase())) + } + return [...list].sort((a, b) => { + if (a.pinned && !b.pinned) return -1 + if (!a.pinned && b.pinned) return 1 + return 0 + }) }, [conversations, searchQuery]) // Fetch conversations from API @@ -776,6 +784,23 @@ const formatPreviewContent = (content: string) => { const isImageFile = (file: File) => file.type.startsWith("image/") + const handleTogglePin = async (convId: string, pinned: boolean) => { + try { + const res = await fetch(`/api/conversations/${convId}/pin`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pin: !pinned }), + }) + if (res.ok) { + setConversations((prev) => + prev.map((c) => (c.id === convId ? { ...c, pinned: !pinned } : c)) + ) + } + } catch { + console.warn("Failed to toggle pin") + } + } + const getDisplayContent = (content: string) => { try { const parsed = JSON.parse(content) @@ -827,7 +852,7 @@ const formatPreviewContent = (content: string) => { }) }} className={cn( - "w-full flex items-start gap-3 p-4 text-left transition-colors hover:bg-muted/50 relative", + "w-full group flex items-start gap-3 p-4 text-left transition-colors hover:bg-muted/50 relative", isActive && "bg-muted" )} > @@ -837,8 +862,21 @@ const formatPreviewContent = (content: string) => {
{formatPreviewContent(conv.lastMessage)}