"use client"; import { useState } from "react"; import { MoreHorizontal } from "lucide-react"; import { toast } from "sonner"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { ActorAvatar } from "@/components/common/actor-avatar"; import { Markdown } from "@/components/markdown"; import { useActorName } from "@/features/workspace"; import { timeAgo } from "@/shared/utils"; import { ReplyInput } from "./reply-input"; import type { TimelineEntry } from "@/shared/types"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface CommentCardProps { entry: TimelineEntry; allReplies: Map; currentUserId?: string; onReply: (parentId: string, content: string) => Promise; onEdit: (commentId: string, content: string) => Promise; onDelete: (commentId: string) => void; } // --------------------------------------------------------------------------- // Single comment row (used for both parent and replies within the same Card) // --------------------------------------------------------------------------- function CommentRow({ entry, currentUserId, onEdit, onDelete, }: { entry: TimelineEntry; currentUserId?: string; onEdit: (commentId: string, content: string) => Promise; onDelete: (commentId: string) => void; }) { const { getActorName } = useActorName(); const [editing, setEditing] = useState(false); const [editContent, setEditContent] = useState(""); const isOwn = entry.actor_type === "member" && entry.actor_id === currentUserId; const isTemp = entry.id.startsWith("temp-"); const startEdit = () => { setEditContent(entry.content ?? ""); setEditing(true); }; const cancelEdit = () => { setEditing(false); setEditContent(""); }; const saveEdit = async () => { const trimmed = editContent.trim(); if (!trimmed) return; try { await onEdit(entry.id, trimmed); setEditing(false); setEditContent(""); } catch { toast.error("Failed to update comment"); } }; return (
{getActorName(entry.actor_type, entry.actor_id)} {timeAgo(entry.created_at)} } /> {new Date(entry.created_at).toLocaleString()} {!isTemp && isOwn && ( } /> Edit onDelete(entry.id)} variant="destructive"> Delete )}
{editing ? (
{ e.preventDefault(); saveEdit(); }} className="mt-2 pl-8" > setEditContent(e.target.value)} aria-label="Edit comment" className="w-full text-sm bg-transparent border-b border-border outline-none py-1" onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }} />
) : (
{entry.content ?? ""}
)}
); } // --------------------------------------------------------------------------- // CommentCard — One Card per thread (parent + all replies flat inside) // --------------------------------------------------------------------------- function CommentCard({ entry, allReplies, currentUserId, onReply, onEdit, onDelete, }: CommentCardProps) { // Collect all nested replies recursively into a flat list const allNestedReplies: TimelineEntry[] = []; const collectReplies = (parentId: string) => { const children = allReplies.get(parentId) ?? []; for (const child of children) { allNestedReplies.push(child); collectReplies(child.id); } }; collectReplies(entry.id); return ( {/* Parent comment */}
{/* Replies — flat, separated by border */} {allNestedReplies.map((reply) => (
))} {/* Reply input — always visible at bottom */}
onReply(entry.id, content)} />
); } export { CommentCard, type CommentCardProps };