refactor(timeline): use avatar for activity entries, remove filter, allow nested replies

- Activity entries now show ActorAvatar instead of a small dot (consistent with comments)
- Remove All/Comments/Activity filter toggle (comments are just a type of activity)
- Remove one-level reply restriction in backend (allow nested threading)
- Remove unused Circle import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-28 22:13:23 +08:00
parent 6cc8f7586a
commit 56c06ec13b
2 changed files with 3 additions and 28 deletions

View file

@ -10,7 +10,6 @@ import {
Calendar,
ChevronLeft,
ChevronRight,
Circle,
Link2,
MessageSquare,
MoreHorizontal,
@ -198,7 +197,6 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [propertiesOpen, setPropertiesOpen] = useState(true);
const [detailsOpen, setDetailsOpen] = useState(true);
const [filter, setFilter] = useState<"all" | "comments" | "activity">("all");
const [replyingTo, setReplyingTo] = useState<string | null>(null);
const [replyEmpty, setReplyEmpty] = useState(true);
@ -750,9 +748,6 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
<div className="flex items-center gap-3">
<h2 className="text-base font-semibold">Activity</h2>
<div className="flex gap-1">
<Button variant={filter === "all" ? "secondary" : "ghost"} size="sm" className="h-6 text-xs" onClick={() => setFilter("all")}>All</Button>
<Button variant={filter === "comments" ? "secondary" : "ghost"} size="sm" className="h-6 text-xs" onClick={() => setFilter("comments")}>Comments</Button>
<Button variant={filter === "activity" ? "secondary" : "ghost"} size="sm" className="h-6 text-xs" onClick={() => setFilter("activity")}>Activity</Button>
</div>
</div>
<div className="flex items-center gap-2">
@ -847,21 +842,11 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
}
}
// Apply filter
const filtered = topLevel.filter((e) => {
if (filter === "all") return true;
if (filter === "comments") return e.type === "comment";
if (filter === "activity") return e.type === "activity";
return true;
});
return filtered.map((entry) => {
return topLevel.map((entry) => {
if (entry.type === "activity") {
return (
<div key={entry.id} className="flex items-center gap-2 py-1.5 text-sm text-muted-foreground">
<div className="w-7 flex justify-center">
<Circle className="h-1.5 w-1.5 fill-current" />
</div>
<div key={entry.id} className="flex items-center gap-2.5 py-1.5 text-sm text-muted-foreground">
<ActorAvatar actorType={entry.actor_type} actorId={entry.actor_id} size={28} />
<span className="font-medium">{getActorName(entry.actor_type, entry.actor_id)}</span>
<span>{formatActivity(entry)}</span>
<Tooltip>

View file

@ -94,16 +94,6 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
var parentID pgtype.UUID
if req.ParentID != nil {
parentID = parseUUID(*req.ParentID)
// Only allow one level of nesting: parent must be a top-level comment
parent, err := h.Queries.GetComment(r.Context(), parentID)
if err != nil {
writeError(w, http.StatusBadRequest, "parent comment not found")
return
}
if parent.ParentID.Valid {
writeError(w, http.StatusBadRequest, "replies to replies are not supported")
return
}
}
comment, err := h.Queries.CreateComment(r.Context(), db.CreateCommentParams{