"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; import { Send } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; type Message = { id: string; content: string; senderId: string; createdAt: string; }; export function MessageThread({ conversationId, currentUserId, }: { conversationId: string; currentUserId: string; }) { const t = useTranslations("messages"); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [nextCursor, setNextCursor] = useState(null); const [content, setContent] = useState(""); const [sending, setSending] = useState(false); const bottomRef = useRef(null); // Loads the latest page of messages. On the very first load this replaces // the (empty) list outright; on subsequent polls it merges in only the // messages we don't already have, so it doesn't clobber older history the // user paged back through via loadMore(). const load = useCallback(async () => { const res = await fetch(`/api/v1/conversations/${conversationId}/messages`); if (res.ok) { const data = (await res.json()) as { messages: Message[]; nextCursor: string | null }; setMessages((prev) => { if (prev.length === 0) return data.messages; const existingIds = new Set(prev.map((m) => m.id)); const fresh = data.messages.filter((m) => !existingIds.has(m.id)); return fresh.length > 0 ? [...prev, ...fresh] : prev; }); setNextCursor((prev) => prev ?? data.nextCursor); } setLoading(false); }, [conversationId]); // Loads an older page (before the oldest message currently loaded) and // prepends it, using the server-provided cursor. const loadMore = useCallback(async () => { if (!nextCursor || loadingMore) return; setLoadingMore(true); try { const res = await fetch( `/api/v1/conversations/${conversationId}/messages?before=${encodeURIComponent(nextCursor)}` ); if (res.ok) { const data = (await res.json()) as { messages: Message[]; nextCursor: string | null }; setMessages((prev) => [...data.messages, ...prev]); setNextCursor(data.nextCursor); } } finally { setLoadingMore(false); } }, [conversationId, nextCursor, loadingMore]); useEffect(() => { void load(); const interval = setInterval(() => { void load(); }, 5000); return () => clearInterval(interval); }, [load]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages.length]); async function send() { if (!content.trim()) return; setSending(true); try { const res = await fetch(`/api/v1/conversations/${conversationId}/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: content.trim() }), }); if (!res.ok) { const err = await res.json().catch(() => ({})) as { error?: string }; toast.error(err.error ?? t("sendFailed")); return; } setContent(""); await load(); } finally { setSending(false); } } return (
{loading ? (

{t("loading")}

) : messages.length === 0 ? (

{t("noMessagesYet")}

) : ( <> {nextCursor && (
)} {messages.map((m) => { const isOwn = m.senderId === currentUserId; return (
{m.content}
); })} )}