fix(issues): remove duplicate commenter header in collapsible comment

The parent comment's header (avatar, name, time, context menu) is now
the collapsible trigger itself. Only the body, reactions, replies, and
reply input collapse — the header is always visible. This removes the
duplicate author info that appeared when expanded.
This commit is contained in:
Jiayuan 2026-03-31 15:07:41 +08:00 committed by Jiang Bohan
parent 90295d8554
commit fc8969a399

View file

@ -193,6 +193,33 @@ function CommentCard({
}: CommentCardProps) {
const { getActorName } = useActorName();
const [open, setOpen] = useState(true);
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");
}
};
// Collect all nested replies recursively into a flat list
const allNestedReplies: TimelineEntry[] = [];
@ -207,23 +234,36 @@ function CommentCard({
const replyCount = allNestedReplies.length;
const contentPreview = (entry.content ?? "").replace(/\n/g, " ").slice(0, 80);
const reactions = entry.reactions ?? [];
return (
<Card className={`!py-0 !gap-0 overflow-hidden${entry.id.startsWith("temp-") ? " opacity-60" : ""}`}>
<Card className={`!py-0 !gap-0 overflow-hidden${isTemp ? " opacity-60" : ""}`}>
<Collapsible open={open} onOpenChange={setOpen}>
{/* Collapsed header — always visible */}
<div className="px-4">
<CollapsibleTrigger className="flex w-full items-center gap-2 py-2.5 text-left">
<ChevronRight className={cn("h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform", open && "rotate-90")} />
<ActorAvatar actorType={entry.actor_type} actorId={entry.actor_id} size={20} />
<span className="text-sm font-medium shrink-0">
{/* Header — always visible, acts as toggle */}
<div className="px-4 py-3">
<div className="flex items-center gap-2.5">
<CollapsibleTrigger className="shrink-0 text-muted-foreground hover:text-foreground transition-colors">
<ChevronRight className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-90")} />
</CollapsibleTrigger>
<ActorAvatar actorType={entry.actor_type} actorId={entry.actor_id} size={24} />
<span className="text-sm font-medium">
{getActorName(entry.actor_type, entry.actor_id)}
</span>
<span className="text-xs text-muted-foreground shrink-0">
{timeAgo(entry.created_at)}
</span>
<Tooltip>
<TooltipTrigger
render={
<span className="text-xs text-muted-foreground cursor-default">
{timeAgo(entry.created_at)}
</span>
}
/>
<TooltipContent side="top">
{new Date(entry.created_at).toLocaleString()}
</TooltipContent>
</Tooltip>
{!open && contentPreview && (
<span className="text-xs text-muted-foreground truncate ml-1">
<span className="text-xs text-muted-foreground truncate">
{contentPreview}{(entry.content ?? "").length > 80 ? "..." : ""}
</span>
)}
@ -232,19 +272,81 @@ function CommentCard({
{replyCount} {replyCount === 1 ? "reply" : "replies"}
</span>
)}
</CollapsibleTrigger>
{open && !isTemp && (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="icon-xs" className="ml-auto text-muted-foreground">
<MoreHorizontal className="h-4 w-4" />
</Button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {
navigator.clipboard.writeText(entry.content ?? "");
toast.success("Copied");
}}>
<Copy className="h-3.5 w-3.5" />
Copy
</DropdownMenuItem>
{isOwn && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={startEdit}>
<Pencil className="h-3.5 w-3.5" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onDelete(entry.id)} variant="destructive">
<Trash2 className="h-3.5 w-3.5" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
{/* Expanded content */}
{/* Collapsible body */}
<CollapsibleContent>
<div className="px-4">
<CommentRow
entry={entry}
currentUserId={currentUserId}
onEdit={onEdit}
onDelete={onDelete}
onToggleReaction={onToggleReaction}
/>
{/* Parent comment body */}
<div className="px-4 pb-3">
{editing ? (
<form
onSubmit={(e) => { e.preventDefault(); saveEdit(); }}
className="pl-10"
>
<input
autoFocus
value={editContent}
onChange={(e) => 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(); }}
/>
<div className="flex gap-2 mt-1.5">
<Button size="sm" type="submit">Save</Button>
<Button size="sm" variant="ghost" type="button" onClick={cancelEdit}>Cancel</Button>
</div>
</form>
) : (
<>
<div className="pl-10 text-sm leading-relaxed text-foreground/85">
<Markdown mode="minimal">{entry.content ?? ""}</Markdown>
</div>
{!isTemp && (
<ReactionBar
reactions={reactions}
currentUserId={currentUserId}
onToggle={(emoji) => onToggleReaction(entry.id, emoji)}
className="mt-1.5 pl-10"
/>
)}
</>
)}
</div>
{/* Replies */}