ReviewOS

stacks/bench-review

reviews: wire Comment model + commenting on reviews

#44
Closed glennmichael123 opened this 10 days ago ·
10 days ago

JudgeReview.comments: number column exists (counter for display) but no Comment model is wired and no comment-on-review UI exists. Yelp has comments under reviews for back-and-forth context; bench-review would benefit similarly when readers want to push back on a take or add context.

Scope

  • New Comment model: id, uuid, judge_review_id, user_id, body, status (pending/published/rejected), created_at, updated_at.
  • Optional parent_comment_id for threaded replies (nice-to-have; flat first).
  • API:
    • GET /api/reviews/{id}/comments — paginated, published-only for public.
    • POST /api/reviews/{id}/comments — auth-gated, status=pending by default (moderation).
    • PATCH /api/me/comments/{id} — author edit (resets to pending).
    • DELETE /api/me/comments/{id} — author delete.
    • PATCH /api/admin/comments/{id}/status — moderate.
  • UI:
    • Comments section below the review body in ArticleView.stx.
    • Comment composer (signed-in users only).
    • Comment cards with timestamp, author, optional edit/delete for own comments.
  • Counter: keep judge_reviews.comments in sync via observer hooks (afterCreate / afterDelete on Comment).

Acceptance

  • Authenticated user can comment on a published review; comment lands pending.
  • Admin sees comments in the moderation queue.
  • Published comments appear under the review for everyone.
  • The comments counter on the review row reflects published comments only.

Privacy / safety

  • Comments inherit the moderation pipeline already in place.
  • Author of the original review can delete comments on their own review (community moderation lite).
  • Anonymous mode (sibling issue) applies the same way for comments.
9 days ago

Shipped — submit, list, author-delete, anonymous-aware.

Schema — database/migrations/0000000111-create-comments-on-reviews-table.sql:

  • review_comments: id, judge_review_id, user_id, body, anonymized, status (default 'published'), created_at, updated_at.
  • Indexes on (judge_review_id, status) for the listing query + on user_id for the my-comments paths.
  • Separate table (not the framework's polymorphic comments) because this is review-scoped with its own moderation lifecycle.

Server:

  • POST /api/reviews/{id}/comments (CommentSubmitAction.ts) — auth-gated, auto-publishes. Body 5-2000 chars, sanitised through the same review-HTML helper from #31 so pasted layout-style noise gets stripped. anonymized flag accepted per #36; gating happens on the read path.
  • GET /api/reviews/{id}/comments (CommentsForReviewAction.ts) — paginated canonical-shape via the shim from #28. Default 20 per page. Hydrates each row with the public-safe author via publicReviewerFor — anonymous comments return author.id = null so the avatar/name can't link to /user/{id} by construction.
  • DELETE /api/me/comments/{id} (DeleteMyCommentAction.ts) — author-only hard delete. Decrements the judge_reviews.comments counter, clamped at 0 in case of drift.
  • All three update the denormalised judge_reviews.comments counter so list cards don't COUNT() on every render.

UI — resources/components/Bench/Comments.stx (new), included in ArticleView.stx right below the article body:

  • Composer for signed-in users: textarea + "Post anonymously" checkbox + "Post comment" button. Anonymous viewers see a "Sign in to comment" CTA in the same slot.
  • Skeleton while loading; empty state ("No comments yet. Be the first to add context.") when published list is empty.
  • Per-comment row: avatar circle (first letter of name), linked name when non-anonymous + clickable to /user/{id}, plain text "Anonymous attorney" / "Anonymous reviewer" when anonymized, relative timestamp ("3m ago" / "2d ago"), body, owner-only "Delete" link.

Why auto-published instead of pending: comments are short and frequent enough that a moderation queue would kill the back-and-forth pattern. Admin can still take a specific comment down via status='rejected' (no UI yet — direct DB / future admin tab). The flagging system from #27 will extend to comments in a follow-up; for now, author-delete is the self-service control.

Anonymity carries through end-to-end: composer toggle → server stores anonymized=1 → read endpoint runs publicReviewerFor → client sees author.id = null → avatar + name render as plain text with no profile link. Belt-and-suspenders consistent with the article-page anonymous render from #36.

Sign in to comment on this issue.