c3776238c7
1:1 conversations (userAId < userBId dedup pair), messages, per-participant read tracking (conversation_reads). Block relationship is enforced on every send. UI: /messages list + /messages/[id] thread (5s poll), MessageButton on profiles, unread-badged nav icon. This completes the social-feature backlog: notifications, rate limiting, blocking, reporting, search/discovery, mentions, DMs, plus fixes for the recipe-visibility 404, follow race, and 2-level comment thread cap found during the earlier audit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { MessageCircle } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
export function MessagesNavLink() {
|
|
const [unreadTotal, setUnreadTotal] = useState(0);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
try {
|
|
const res = await fetch("/api/v1/conversations");
|
|
if (!res.ok || cancelled) return;
|
|
const data = (await res.json()) as { conversations: { unreadCount: number }[] };
|
|
setUnreadTotal(data.conversations.reduce((sum, c) => sum + c.unreadCount, 0));
|
|
} catch {
|
|
// silent
|
|
}
|
|
}
|
|
void load();
|
|
const interval = setInterval(load, 30_000);
|
|
return () => { cancelled = true; clearInterval(interval); };
|
|
}, []);
|
|
|
|
return (
|
|
<Button variant="ghost" size="icon" className="relative" nativeButton={false} render={<Link href="/messages" />}>
|
|
<MessageCircle className="h-4 w-4" />
|
|
{unreadTotal > 0 && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="absolute -top-1 -right-1 h-4 min-w-4 px-1 text-[10px] leading-none flex items-center justify-center"
|
|
>
|
|
{unreadTotal > 9 ? "9+" : unreadTotal}
|
|
</Badge>
|
|
)}
|
|
<span className="sr-only">Messages</span>
|
|
</Button>
|
|
);
|
|
}
|