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 byreplyToUserId— not by the text. Deleting the@bobfrom the composer does not silence this. Skipped only on a self-reply. - Any other
@mentionsin 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 —
parentIdcollapses to the root comment (max 2 levels), whilereplyToUserIdpreserves 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 (likeCountunchanged); 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.
Optimistic UI (recommended)
Mirror the post-like pattern exactly:
- On tap: flip
isLikedand adjustlikeCount(+1 / −1) locally. No spinner. - Fire the PUT in the background.
- On response: reconcile
likeCountfrom the payload (server is truth). - On HTTP error: roll back the local flip + toast.
- Track
updatedAtper commentId — discard a response older than the last applied (guards against out-of-order rapid taps).
Errors
| code | meaning |
|---|---|
| 401 | missing / invalid token |
| 404 | comment not found, not in that post, or post not visible |
| 429 | rate limit (30 / 10s per user) |
Checklist
- Add
likeCount+isLikedto your Comment type (top-level and reply). - Like button on every comment/reply: optimistic flip +
PUT .../like. - Reconcile
likeCountfrom 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 acomment("replied") notification regardless of the text; other@mentionsgetmentionnotifications (deduped). - Handle the
likenotification withdata.entityType === "comment"→ navigate todata.webPath/data.deepLink.