Summary
Direct Messages shipped (folders, threads, read-state, unread badge) without three live routes. One of them makes the DM composer advertise something it cannot do.
| Route |
Live |
What it unlocks |
GET /api/dm/conversations |
✅ 200 (probed 2026-09-07: {"items":[],"nextCursor":null}) |
One row per conversation grouped by pairKey, newest first — the natural inbox, versus today's folder listing that the client collapses into conversation rows itself |
GET /api/dm/{id} |
exists |
Fetch a single message (deep-link target) |
POST /api/dm/images/upload |
exists |
DM image attachments |
The image gap is the user-visible one
/help/direct-messages: "You can attach photos to a direct message, up to 8 per message. Photos are resized automatically. You'll need a verified email address to send images."
The macOS DM composer surfaces attachments but has no upload route behind them. Wire the same ImagePrep path the message composer uses, with the server-driven size ceilings from ContentLimits (shipped as G14) rather than fresh constants.
The conversations route is a correctness improvement, not just a convenience
Today DirectMessagesListViewModel loads a folder page and groups it into conversation rows client-side (DirectMessagesListViewModel.swift:10 — "the folder listing is a flat, newest-first list"). That means a conversation whose most recent message falls off the end of the fetched page is invisible, and the grouping is only as complete as the page. GET /api/dm/conversations is server-side grouping with its own cursor and does not have that failure mode.
⚠️ It returned empty on the test account, so the populated items[] shape is unverified. Send a DM between two accounts that follow each other and record the real payload before tightening the decoder — this API has a documented history of envelope drift (POST /api/messages wrapping under data).
Constraints to honour
- Mutual-follow only. "You can only message people you follow each other." The recipient picker must be sourced from mutual followers, and the empty state must say why: "If the list is empty, it means no one who follows you also follows you back yet."
- Markdown, up to 10,000 characters. Confirm the native composer's limit matches; it is far larger than a message.
- Opening a conversation marks it read automatically.
- Inbox / Sent / Deleted with delete + restore already ship (
DirectMessagesListViewModel.swift:176) — leave them alone.
- Email verification gates image sending — reuse the gate from the email-verification issue rather than a second check.
Division of labor
Kit
Domain
App
Tests
Acceptance criteria
- The DM inbox no longer depends on client-side grouping of a folder page.
- Attaching a photo to a DM works, or is clearly explained why it cannot.
- Full E2E gate green.
Notes
work-consolidation.md tracks this as G22. Size S–M.
Summary
Direct Messages shipped (folders, threads, read-state, unread badge) without three live routes. One of them makes the DM composer advertise something it cannot do.
GET /api/dm/conversations{"items":[],"nextCursor":null})pairKey, newest first — the natural inbox, versus today's folder listing that the client collapses into conversation rows itselfGET /api/dm/{id}POST /api/dm/images/uploadThe image gap is the user-visible one
/help/direct-messages: "You can attach photos to a direct message, up to 8 per message. Photos are resized automatically. You'll need a verified email address to send images."The macOS DM composer surfaces attachments but has no upload route behind them. Wire the same
ImagePreppath the message composer uses, with the server-driven size ceilings fromContentLimits(shipped as G14) rather than fresh constants.The conversations route is a correctness improvement, not just a convenience
Today
DirectMessagesListViewModelloads a folder page and groups it into conversation rows client-side (DirectMessagesListViewModel.swift:10— "the folder listing is a flat, newest-first list"). That means a conversation whose most recent message falls off the end of the fetched page is invisible, and the grouping is only as complete as the page.GET /api/dm/conversationsis server-side grouping with its own cursor and does not have that failure mode.items[]shape is unverified. Send a DM between two accounts that follow each other and record the real payload before tightening the decoder — this API has a documented history of envelope drift (POST /api/messageswrapping underdata).Constraints to honour
DirectMessagesListViewModel.swift:176) — leave them alone.Division of labor
Kit
DirectMessages.conversations(cursor:)→GET /api/dm/conversationsDirectMessages.message(id:)→GET /api/dm/{id}DirectMessages.uploadImage(...)→POST /api/dm/images/uploadDomain
ImagePrep+ContentLimitspath; enforce the 8-image cap.App
DirectMessagesRootViewinbox paints from conversations with its own cursor pagination.NewMessageSheet/DMThreadViewactually upload, gated on email verification with the shared explanation.Tests
Acceptance criteria
Notes
work-consolidation.mdtracks this as G22. Size S–M.