# 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`.

```jsonc
// 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 `replyToUserId` — **not** 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:

```jsonc
{
  "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

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

Response — like state **after** the call:

```jsonc
{
  "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.

---

## Optimistic UI (recommended)

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

| 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` + `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`.
