# Post Share (in-app) — Mobile Integration Guide

Sharing a post **into a chat** as a native card — not a pasted link. The
message stores only the `postId`; the server renders a live, per-viewer card on
read. This doc is for the mobile (React Native) client. Full chat flow:
[mobile-chat-guide.md](../chat/mobile-chat-guide.md) (this is §5.10, standalone here).

## Scope

**In-app DM/group share only.** For **external** channels (WhatsApp, Messages,
email) keep using the URL share (`POST /api/posts/:id/share`, see
[mobile-posts-guide.md](mobile-posts-guide.md)). Native share wins in-app
because the card renders instantly (no OG-preview wait), works with link
previews off, and its counts/thumbnail are always current — the body is
resolved fresh on every read, never a frozen snapshot.

## 1 — Send a share

It's a normal message send with a `postId` instead of text/media.

```
POST /api/chat/conversations/:id/messages
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "postId": "019f45c6-…",          // the post being shared (required)
  "content": "check this out",       // optional caption (≤2000)
  "clientMessageId": "uuid-…"        // same idempotency rules as any message
}
```

### Response `200`

```jsonc
{
  "success": true,
  "data": {
    "id": "019f4673-…",
    "type": "POST_SHARE",            // branch on this
    "sharedPostId": "019f45c6-…",
    "content": "check this out",      // the caption (may be "")
    "conversationId": "019f4673-…",
    "senderId": "…",
    "status": { "type": "sent", … }
    // NOTE: no `sharedPost` card body here — resolve it, see §2
  }
}
```

### Errors

| Code  | When                                                                   |
| ----- | ---------------------------------------------------------------------- |
| `400` | `postId` + `media` sent together (a share can't carry attachments)     |
| `401` | Missing/expired token                                                  |
| `403` | Not a participant / removed / messaging-restricted                     |
| `404` | Post not found/hidden, **or you can't view it** (can't share what you can't see) |
| `429` | Rate limited (60 messages / min)                                       |

Sending a native share also records the post's share count (`_count.shares`),
same as a URL share.

## 2 — Get the card (one resolver, two entry points)

The card is **never** on the `new_message` socket broadcast — a single
broadcast can't hold a viewer-specific card. Resolve it the same way in both
cases:

- **Loading history** — `GET /api/chat/conversations/:id/messages` already
  inlines `sharedPost` on every `POST_SHARE` row. Nothing extra to do.
- **Live (`new_message` arrives)** — the event carries `type:"POST_SHARE"` +
  `sharedPostId` only. Batch-resolve:

```
POST /api/posts/share-cards
Authorization: Bearer <accessToken>
Content-Type: application/json

{ "postIds": ["019f45c6-…", "019f4676-…"] }   // dedupe across the visible page
```

```jsonc
// Response — cards come back in request order
{ "success": true, "data": { "cards": [ /* SharedPostCard */ ] } }
```

Cache each card by `postId`. Counts are live, so refetch when you want fresh
numbers (e.g. reopening the thread).

## 3 — `sharedPost` shape + the three states

Branch on `available` (and `reason` when false).

```jsonc
// ── available: true → render the full card ──
{
  "id": "019f45c6-…",
  "available": true,
  "author": {
    "id": "…", "username": "adolf",
    "grade": "7", "school": "Lincoln HS",     // may be null
    "profile": { "displayName": "Adolf", "avatarUrl": "https://…" }
  },
  "type": "FIND_TEAMMATES",                    // PostType
  "title": "Chess team",                        // may be null
  "content": "Looking for a chess team…",       // WHOLE body — clamp for display
  "thumbnailUrl": "https://…/thumb.jpg",        // first image thumbnail, or null
  "mediaCount": 1,
  "counts": { "likes": 0, "comments": 0, "reposts": 0, "shares": 1 },
  "createdAt": "2026-07-09T10:40:02.530Z"
}

// ── available: false, reason: "private" → identity-only LOCKED card ──
// Author is private and the viewer isn't an accepted follower.
{
  "id": "019f4676-…",
  "available": false,
  "reason": "private",
  "author": {                                   // identity ONLY
    "id": "…", "username": "charlie",
    "profile": { "displayName": "Charlie Brown", "avatarUrl": "https://…" }
  }
}

// ── available: false, reason: "unavailable" → "Post unavailable" ──
// Post deleted/hidden, OR the author blocked the viewer. NO identity.
{ "id": "019f4676-…", "available": false, "reason": "unavailable" }
```

| State                          | Render                                                        |
| ------------------------------ | ------------------------------------------------------------ |
| `available: true`              | Full card. Tap → deep-link to the post detail screen.        |
| `reason: "private"`            | Locked card: avatar + displayName + "Private account — follow to view". Non-navigable. |
| `reason: "unavailable"`        | Neutral "Post unavailable" bubble. No identity. Non-navigable. |

A caption (`message.content`) may accompany any state — render it above the
card as normal message text.

## 4 — Client checklist

- Send: reuse your normal optimistic-send + `clientMessageId` retry path
  (§5.2/5.3 of the chat guide). The optimistic bubble can render the card
  immediately from the post the user just tapped-share on — no round trip.
- On `new_message` with `type:"POST_SHARE"`: resolve via
  `POST /api/posts/share-cards` (batch the visible page, dedupe by `postId`).
- Sidebar/last-message preview for a bare share (no caption) shows
  "Shared a post" — the server already returns that as `lastMessage.content`.
- Never trust a cached card's counts as authoritative — refetch on thread open.

## 5 — Examples

```bash
# alice@example.com / Password123 → accessToken
TOKEN=...
CID=<conversationId>   POST_ID=<postId>

# Send a native share with a caption
curl -X POST localhost:3001/api/chat/conversations/$CID/messages \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"postId\":\"$POST_ID\",\"content\":\"check this out\",\"clientMessageId\":\"$(uuidgen)\"}"

# Resolve the card(s) as the recipient
curl -X POST localhost:3001/api/posts/share-cards \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"postIds\":[\"$POST_ID\"]}"

# History (card is inlined on POST_SHARE rows)
curl -s localhost:3001/api/chat/conversations/$CID/messages \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data.items[] | select(.type=="POST_SHARE") | {sharedPostId, sharedPost}'
```

Swagger UI: `http://localhost:3001/api/docs` (Chat → `POST /api/chat/conversations/{id}/messages`, Posts → `POST /api/posts/share-cards`).
