Skip to main content
Version: Latest

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 (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). 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

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

CodeWhen
400postId + media sent together (a share can't carry attachments)
401Missing/expired token
403Not a participant / removed / messaging-restricted
404Post not found/hidden, or you can't view it (can't share what you can't see)
429Rate 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 historyGET /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
// 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).

// ── 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" }
StateRender
available: trueFull 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

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

⤓ Download .md