diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx index 7ca9c12e..d1df85f5 100644 --- a/apps/web/features/issues/components/issue-detail.tsx +++ b/apps/web/features/issues/components/issue-detail.tsx @@ -315,7 +315,13 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) { const handleDeleteComment = async (commentId: string) => { try { await api.deleteComment(commentId); - setTimeline((prev) => prev.filter((e) => e.id !== commentId)); + setTimeline((prev) => + prev + // Promote replies of deleted comment to top-level + .map((e) => e.parent_id === commentId ? { ...e, parent_id: null } : e) + // Remove the deleted comment + .filter((e) => e.id !== commentId) + ); } catch { toast.error("Failed to delete comment"); } @@ -379,7 +385,11 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) { useCallback((payload: unknown) => { const { comment_id, issue_id } = payload as CommentDeletedPayload; if (issue_id === id) { - setTimeline((prev) => prev.filter((e) => e.id !== comment_id)); + setTimeline((prev) => + prev + .map((e) => e.parent_id === comment_id ? { ...e, parent_id: null } : e) + .filter((e) => e.id !== comment_id) + ); } }, [id]), ); diff --git a/server/cmd/server/activity_listeners.go b/server/cmd/server/activity_listeners.go index 8a05da40..2a639a79 100644 --- a/server/cmd/server/activity_listeners.go +++ b/server/cmd/server/activity_listeners.go @@ -186,19 +186,26 @@ func handleTaskActivity(ctx context.Context, bus *events.Bus, queries *db.Querie } // publishActivityEvent sends an activity:created event for WS broadcasting. +// Payload matches frontend ActivityCreatedPayload: { issue_id, entry: TimelineEntry } func publishActivityEvent(bus *events.Bus, original events.Event, activity db.ActivityLog) { + actorType := "" + if activity.ActorType.Valid { + actorType = activity.ActorType.String + } + action := activity.Action bus.Publish(events.Event{ Type: protocol.EventActivityCreated, WorkspaceID: original.WorkspaceID, ActorType: original.ActorType, ActorID: original.ActorID, Payload: map[string]any{ - "activity": map[string]any{ + "issue_id": util.UUIDToString(activity.IssueID), + "entry": map[string]any{ + "type": "activity", "id": util.UUIDToString(activity.ID), - "issue_id": util.UUIDToString(activity.IssueID), - "actor_type": util.TextToPtr(activity.ActorType), + "actor_type": actorType, "actor_id": util.UUIDToString(activity.ActorID), - "action": activity.Action, + "action": &action, "details": json.RawMessage(activity.Details), "created_at": util.TimestampToString(activity.CreatedAt), }, diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index 99d45113..d4f4fc99 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -94,6 +94,16 @@ 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{