Skip to main content
Version: 1.0

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)

SurfaceNew
PostRequest objectconversationId (string | null), authorReadAt (ISO | null)
Author inbox countsunread number
PATCH .../requests/:reqId acceptopens/links a DM, returns conversationId
post_request_accepted notificationdata.conversationId + chat deep link
PATCH .../requests/:reqIdnew "reopen" action — ACCEPTED back to PENDING (+ post_request_reopened notification)
PATCH /api/posts/:id409 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:

EventDo
tapopen 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 REJECTEDdrop 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.

ACCEPTED with conversationId: null is 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:

EventRoomWhen
conversation_status_changed { conversationId, status: "ACCEPTED" }both usersonly if a PENDING chat request already existed between the pair (it flips)
new_message (full message, type: "POST_SHARE")conversation roomthe seed. Resolve the card via POST /api/posts/share-cards as usual
chat_list_update { conversationId, status: "ACCEPTED", lastMessage, unreadIncrement }both personal roomssidebar 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: null in 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 authorReadAt set → no highlight. Keep the highlight for the lifetime of the screen from the payload you rendered — do not refetch to "confirm".
  • counts.unread is 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 unread until actually delivered.
  • unread spans 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 eventAt never 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

CaseWhat the client seesDo
accept, blocked pair200, conversationId: nullauthor: normal accepted row, no chat affordance. requester: ACCEPTED row linking to post
accept twice / stale inbox409 "Request is not pending"refetch inbox
post deleted after acceptseed message with sharedPostId: null; requester's /requests/mine row gone (cascade)placeholder card; DM survives
conversation deleted laterconversationId back to null on the rowfall back to post link
same requester accepted on 2 postssame conversationId on both rowsone DM, two seed messages
author reopens an accepted requestrow back to PENDING, conversationId: null; conversation + seed message survive in chatre-add DM-tab request row; chat list unchanged. Re-accept relinks the same DM, no duplicate seed
request while intake paused/completed409 on submitper base guide — CTA state from the post object

7 — Wiring checklist

  1. DM tab = merge requests/mine?status=PENDING rows + conversations (§1).
  2. On post_request_accepted: drop the request row, navigate via data.conversationId; null → post fallback (§2).
  3. Render the seed like any POST_SHARE message — zero new message UI (§2).
  4. Handle the three accept-moment socket events (§3) — all pre-existing handlers from the chat guide.
  5. Responses screens: highlight authorReadAt: null rows from the fetched payload; badges from counts.unread (§4).
  6. Disable post editing when eventAt has passed; surface 409 messages (§5).