Skip to content

feat(dm): conversations inbox, single-message fetch, and photo attachments - G22 #53

Description

@Adron

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

  • DirectMessages.conversations(cursor:) → GET /api/dm/conversations
  • DirectMessages.message(id:) → GET /api/dm/{id}
  • DirectMessages.uploadImage(...) → POST /api/dm/images/upload
  • DTOs + contract tests, against a populated conversation list

Domain

  • Switch the inbox to the server-grouped conversations feed; keep the folder listing for Sent/Deleted, which the conversations route does not replace.
  • DM image upload through the shared ImagePrep + ContentLimits path; enforce the 8-image cap.
  • Single-message fetch for deep links.

App

  • DirectMessagesRootView inbox paints from conversations with its own cursor pagination.
  • Attachments in NewMessageSheet / DMThreadView actually upload, gated on email verification with the shared explanation.
  • Recipient picker sourced from mutual followers with the documented empty state.

Tests

  • happy — conversations paginate; an image attaches and appears in the thread
  • invalid — a 9th image is refused client-side; an unverified account cannot attach
  • upstream-failure — upload fails → the message still sends as text, with the failure surfaced (do not lose the draft)
  • boundary — empty conversation list; a 10,000-character body; a conversation whose last message predates the first page

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions