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:
parent
90295d8554
commit
fc8969a399
1 changed files with 123 additions and 21 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue