Mobile Post Request ↔ DM Wiring — Integration Guide
Wiring guide for the request→DM integration only: pending requests in the requester's DM tab, accept opening a chat, the author's "new response" badge, and the edit freeze. Prerequisites (read first):
mobile-post-requests-guide.md(request endpoints + CTA),mobile-chat-guide.md(conversations + sockets),mobile-post-share-guide.md(POST_SHARE card rendering).
- Base URL:
http://localhost:3001(dev) — production URL from backend team - Auth:
Authorization: Bearer <accessToken>on every endpoint - Headers:
Content-Type: application/json,X-Client-Type: mobile
What changed (delta over the base requests guide)
| Surface | New |
|---|---|
PostRequest object | conversationId (string | null), authorReadAt (ISO | null) |
Author inbox counts | unread number |
PATCH .../requests/:reqId accept | opens/links a DM, returns conversationId |
post_request_accepted notification | data.conversationId + chat deep link |
PATCH .../requests/:reqId | new "reopen" action — ACCEPTED back to PENDING (+ post_request_reopened notification) |
PATCH /api/posts/:id | 409 once eventAt has passed |
1 — Requester's DM tab (client-side merge)
There is no combined endpoint. The DM tab renders two sources:
GET /api/posts/requests/mine?status=PENDING → request rows (this guide)
GET /api/chat/conversations → real conversations (chat guide)
Render each pending request as a row above/alongside conversations:
📌 Request · "Looking for a 4th" ← post.title from the embedded post
Waiting for author action… ← static copy for status PENDING
Row behavior:
| Event | Do |
|---|---|
| tap | open the post (or a request detail sheet) — there is no chat yet |
post_request_accepted push (or refetch shows ACCEPTED) | drop the row; navigate/point to conversationId — the DM now exists |
post_request_reopened push (or refetch shows PENDING again) | re-add the row — the author is re-reviewing; CTA reverts to pendingLabel. The old conversation still exists in the chat list but the request row no longer carries conversationId |
refetch shows REJECTED | drop the row (surface state on the post CTA, per base guide) |
user withdraws (DELETE .../requests/me) | drop the row |
Refresh triggers: pull-to-refresh, app foreground, and the
post_request_accepted / post_request_rejected notifications. There is no
socket event for request state — requests are not chat entities.
2 — Accept → conversation handoff
When the author accepts, the requester's next fetch (or the push payload) carries the chat:
// PATCH /api/posts/:id/requests/:reqId → 200 (author side)
// GET /api/posts/requests/mine → items[] (requester side)
{
"status": "ACCEPTED",
"conversationId": "conv_abc123", // navigate: /chat/conv_abc123
...
}
The DM opens with a seed message already inside: a normal POST_SHARE
message, senderId = the requester, content = their request note,
sharedPostId = the post. Render it exactly like any shared post
(mobile-post-share-guide.md) — no special casing. Accepting more requests
from the same person appends more seed messages into the same DM.
⚠
ACCEPTEDwithconversationId: nullis a VALID state (blocked pair, or linkage failed server-side). Never assume ACCEPTED ⇒ chat exists. Null → link to the post instead of the chat. Do not retry client-side; the backend has an operational repair path.
If the post is later deleted, the seed message survives with
sharedPostId: null → render the standard "post unavailable" placeholder.
3 — Socket events you will receive
All existing events from mobile-chat-guide.md — the seed message reuses the
normal send pipeline. On the accept moment:
| Event | Room | When |
|---|---|---|
conversation_status_changed { conversationId, status: "ACCEPTED" } | both users | only if a PENDING chat request already existed between the pair (it flips) |
new_message (full message, type: "POST_SHARE") | conversation room | the seed. Resolve the card via POST /api/posts/share-cards as usual |
chat_list_update { conversationId, status: "ACCEPTED", lastMessage, unreadIncrement } | both personal rooms | sidebar row. unreadIncrement: 1 for the author, 0 for the requester (they're the sender) |
No message / message_request push fires for the seed — the
post_request_accepted push is the single notification and it deep-links to
ksn://chat/:conversationId (falls back to ksn://posts/:id when
conversationId is null).
4 — Author's responses screen: "new" badge
Both author inboxes (GET /api/posts/:id/requests,
GET /api/posts/requests/inbox) now return:
"counts": { "pending": 3, "accepted": 1, "rejected": 0, "withdrawn": 2, "unread": 2 }
Contract — fetch marks read. There is no mark-read endpoint and no viewport tracking:
- A row with
authorReadAt: nullin the payload you just received is "new" → highlight it. The server marks exactly those rows read as it serves them. - Your next fetch returns them with
authorReadAtset → no highlight. Keep the highlight for the lifetime of the screen from the payload you rendered — do not refetch to "confirm". counts.unreadis computed before the same response's marking, so the badge you display alongside page 1 is accurate at fetch time.- Pagination: only served rows get marked. Unfetched pages still count in
unreaduntil actually delivered. unreadspans all statuses (a withdrawn-before-seen row is still "new"). Requester endpoints never mark anything.
Badge suggestion: use counts.unread from the aggregate inbox for the
home-screen "Requests" tab badge; per-post counts.unread for each "View
Responses" entry point.
5 — Post edit freeze (author UX)
PATCH /api/posts/:id now returns 409
("Post can no longer be edited after its event date") once the post's
eventAt is in the past. Wiring:
- Hide/disable the edit action when
post.eventAt < now. - Posts without
eventAtnever freeze. - This is independent of the existing prompt freeze (
requestPrompt*locked after the first request — also 409). Handle both by surfacing the server message.
6 — Edge matrix
| Case | What the client sees | Do |
|---|---|---|
| accept, blocked pair | 200, conversationId: null | author: normal accepted row, no chat affordance. requester: ACCEPTED row linking to post |
| accept twice / stale inbox | 409 "Request is not pending" | refetch inbox |
| post deleted after accept | seed message with sharedPostId: null; requester's /requests/mine row gone (cascade) | placeholder card; DM survives |
| conversation deleted later | conversationId back to null on the row | fall back to post link |
| same requester accepted on 2 posts | same conversationId on both rows | one DM, two seed messages |
| author reopens an accepted request | row back to PENDING, conversationId: null; conversation + seed message survive in chat | re-add DM-tab request row; chat list unchanged. Re-accept relinks the same DM, no duplicate seed |
| request while intake paused/completed | 409 on submit | per base guide — CTA state from the post object |
7 — Wiring checklist
- DM tab = merge
requests/mine?status=PENDINGrows + conversations (§1). - On
post_request_accepted: drop the request row, navigate viadata.conversationId; null → post fallback (§2). - Render the seed like any POST_SHARE message — zero new message UI (§2).
- Handle the three accept-moment socket events (§3) — all pre-existing handlers from the chat guide.
- Responses screens: highlight
authorReadAt: nullrows from the fetched payload; badges fromcounts.unread(§4). - Disable post editing when
eventAthas passed; surface 409 messages (§5).