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) => {
- {person.name} - {conv.lastMessageTime} +
+ {conv.pinned && } + {person.name} +
+
+ + {conv.lastMessageTime} +

{formatPreviewContent(conv.lastMessage)}

diff --git a/src/app/api/conversations/[id]/pin/route.ts b/src/app/api/conversations/[id]/pin/route.ts new file mode 100644 index 0000000..946d9d9 --- /dev/null +++ b/src/app/api/conversations/[id]/pin/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server" +import { getSessionUser } from "@/lib/auth" +import { query } from "@/lib/db" + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + try { + const user = await getSessionUser() + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + + const { id } = await params + const body = await request.json() + const pin = body.pin === true + + await query( + `UPDATE conversation_participants + SET pinned_at = $1 + WHERE conversation_id = $2 AND user_id = $3`, + [pin ? new Date().toISOString() : null, id, user.id], + ) + + return NextResponse.json({ pinned: pin }) + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to toggle pin" + console.error("Pin conversation error:", message) + return NextResponse.json({ error: message }, { status: 500 }) + } +} diff --git a/src/app/api/conversations/route.ts b/src/app/api/conversations/route.ts index 8a6c214..cd3ba0f 100644 --- a/src/app/api/conversations/route.ts +++ b/src/app/api/conversations/route.ts @@ -13,6 +13,7 @@ export async function GET() { c.id, c.updated_at, cp_me.last_read_at, + cp_me.pinned_at, u.id AS other_user_id, u.first_name || ' ' || u.last_name AS other_user_name, u.email AS other_user_email, @@ -28,7 +29,7 @@ export async function GET() { WHERE c.id IN ( SELECT conversation_id FROM conversation_participants WHERE user_id = $1 ) - ORDER BY c.updated_at DESC + ORDER BY cp_me.pinned_at DESC NULLS LAST, c.updated_at DESC LIMIT 50`, [user.id], ) @@ -36,6 +37,7 @@ export async function GET() { const conversations = result.rows.map((row: any) => ({ id: row.id, updatedAt: row.updated_at, + pinned: row.pinned_at !== null, otherUser: { id: row.other_user_id, name: row.other_user_name,