Skip to main content
Version: 1.0

Mobile Comment Likes — Integration Guide

Like/unlike any comment. Boolean like — one per user per comment. Companion to mobile-posts-guide.md §9 (comment list/create/delete + single-level nesting). Read that first for the comment object shape.

  • Base URL: http://localhost:3001 (dev)
  • Auth: Authorization: Bearer <accessToken> on every endpoint (required)
  • Headers: Content-Type: application/json, X-Client-Type: mobile

Comment nesting — depth is max 2

Comments are Instagram-style, single-level. The thread is never deeper than 2 levels:

comment (parentId = null) ← level 1
└─ reply (parentId = comment.id) ← level 2 (flat — no replies[] of its own)

A reply to a reply is re-anchored to the root comment, so it lands at level 2 next to its siblings. There is no level 3. On the wire:

  • top-level comments carry a flat replies[] array
  • reply objects do not carry a replies[] field

Render likes on both levels identically — every comment and every reply carries likeCount + isLiked.

Replies — who owns the @handle, who gets notified

The client owns the composer. When the user taps "Reply", prefill the input with @<handle> of the person being replied to (your UX call). The user may edit or delete it. The server stores the submitted content verbatim — it never injects or re-adds an @handle.

The reply relationship is tracked separately from the text. The server records replyToUserId (the author of the comment you replied to) as a structural field, independent of what's in content. So attribution survives even if the user removes the @handle.

// user taps "Reply" on bob's comment; composer prefilled "@bob "; user sends as-is
POST /api/posts/:id/comments { "content": "@bob totally agree", "parentId": "<bob's comment id>" }
// stored verbatim; response carries the relationship:
{ "content": "@bob totally agree", "replyToUserId": "<bob>", "parentId": "<root comment id>", ... }

Notifications — one per interaction:

  • The replied-to author is always notified ("replied to your comment", type: "comment"), driven by replyToUserIdnot by the text. Deleting the @bob from the composer does not silence this. Skipped only on a self-reply.
  • Any other @mentions in the text notify those users ("mentioned you", type: "mention"). The replied-to author is deduped out of mention notifications — if they're both replied-to and @-mentioned, they get the one reply notification, not two.
  • A top-level comment notifies the post author ("commented on your post"); same dedup applies if the post author is also @-mentioned.

Example — Bob replies @alice I agree. @charlie thoughts? under Alice's comment: Alice gets one "Bob replied to your comment"; Charlie gets "Bob mentioned you". Both @alice and @charlie mention rows still exist for highlight / tap-to-profile (see mobile-mentions-guide.md for offsets).

Note on threading: a reply to a reply is still flattened — parentId collapses to the root comment (max 2 levels), while replyToUserId preserves who was actually answered.


Comment object — like fields

GET /api/posts/:id/comments returns each comment (and each reply) with:

{
"id": "cmt_xxx",
"content": "...",
"author": { /* id, username, profile */ },
"parentId": null, // root comment id on replies; null on top-level
"replyToUserId": null, // who this reply answers (structural); null on top-level
"likeCount": 3, // total likes on this comment
"isLiked": false, // whether the calling user has liked it
"replies": [ /* level-2 replies, same shape minus replies[] */ ]
}

isLiked is computed for the calling user — auth is required on the list endpoint for this reason.


Like / unlike

PUT /api/posts/:postId/comments/:commentId/like
{ "liked": true } // or false to unlike

Response — like state after the call:

{
"success": true,
"data": {
"liked": true,
"likeCount": 4,
"updatedAt": "2026-06-30T07:35:40.451Z"
}
}

Behaviors

  • Idempotent{ liked: true } when already liked is a no-op (likeCount unchanged); same for { liked: false } when not liked. Safe to retry.
  • Self-like allowed — you can like your own comment; no notification fires.
  • Notification — the comment author is notified only on the first unliked → liked transition (type: "like", data.entityType: "comment"). Unlike + re-like does not re-notify.
  • 404 — comment doesn't exist, isn't in :postId, or the post isn't visible to you.
  • 429 — rate limit exceeded (30 / 10s per user), no body.

Mirror the post-like pattern exactly:

  1. On tap: flip isLiked and adjust likeCount (+1 / −1) locally. No spinner.
  2. Fire the PUT in the background.
  3. On response: reconcile likeCount from the payload (server is truth).
  4. On HTTP error: roll back the local flip + toast.
  5. Track updatedAt per commentId — discard a response older than the last applied (guards against out-of-order rapid taps).

Errors

codemeaning
401missing / invalid token
404comment not found, not in that post, or post not visible
429rate limit (30 / 10s per user)

Checklist

  • Add likeCount + isLiked to your Comment type (top-level and reply).
  • Like button on every comment/reply: optimistic flip + PUT .../like.
  • Reconcile likeCount from the response; roll back on error.
  • Render max 2 levels — never expect replies[] on a reply object.
  • Replies: prefill the composer with @handle (client-owned, user may delete). Server stores text verbatim + replyToUserId. Expect the replied-to author to get a comment ("replied") notification regardless of the text; other @mentions get mention notifications (deduped).
  • Handle the like notification with data.entityType === "comment" → navigate to data.webPath / data.deepLink.