Add conversation pinning: pinned chats stay at top of sidebar
Build & Auto-Repair / build (push) Has been cancelled

This commit is contained in:
JCBSComputer
2026-07-02 16:09:07 +02:00
parent 4c732c850f
commit 211d026fb8
4 changed files with 78 additions and 6 deletions
+43 -5
View File
@@ -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) => {
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium truncate">{person.name}</span>
<span className="text-xs text-muted-foreground shrink-0">{conv.lastMessageTime}</span>
<div className="flex items-center gap-1.5 min-w-0">
{conv.pinned && <Pin className="h-3 w-3 shrink-0 text-muted-foreground rotate-45" />}
<span className="text-sm font-medium truncate">{person.name}</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={(e) => { e.stopPropagation(); handleTogglePin(conv.id, conv.pinned) }}
className="h-5 w-5 flex items-center justify-center rounded hover:bg-muted-foreground/20 transition-colors opacity-0 group-hover:opacity-100"
title={conv.pinned ? "Unpin" : "Pin"}
>
{conv.pinned ? <PinOff className="h-3 w-3 text-muted-foreground" /> : <Pin className="h-3 w-3 text-muted-foreground rotate-45" />}
</button>
<span className="text-xs text-muted-foreground">{conv.lastMessageTime}</span>
</div>
</div>
<p className="text-xs text-muted-foreground truncate mt-0.5">{formatPreviewContent(conv.lastMessage)}</p>
</div>