Skip to content

feat(drafts): open drafts in the composer for editing, with save-draft on close - #42

Open
lucletoffe wants to merge 1 commit into
bulwarkmail:mainfrom
lucletoffe:feat/edit-drafts
Open

feat(drafts): open drafts in the composer for editing, with save-draft on close#42
lucletoffe wants to merge 1 commit into
bulwarkmail:mainfrom
lucletoffe:feat/edit-drafts

Conversation

@lucletoffe

Copy link
Copy Markdown
Contributor

Problem

Drafts are currently dead ends in the mobile app. Tapping a message in the Drafts folder opens the read-only thread view, whose only actions (reply, forward, tag, mark read/unread…) treat it like any received message — there is no way to resume editing a draft, and the composer itself never offers to save one. Drafts written in the webmail (or any other JMAP client) can be viewed but not continued on mobile.

Root cause: there is simply no edit path in the code. Compose only knows reply | replyAll | forward (src/navigation/types.ts), and a row tap always navigates to EmailThread regardless of the $draft keyword (App.tsx).

What this PR does

  • Tap a draft → composer. A $draft message tapped in the list reopens the composer prefilled with its recipients, subject, body, and attachments (reused by blobId, no re-upload). Inline cid: images are hydrated to data URLs (same 2 MB ceiling as EmailBodyView) so they render and survive the round trip; non-hydrated cid parts keep their cid so the reference still resolves for the recipient.
  • Edit button in the thread view for drafts reached through other paths (search, thread).
  • Send replaces the draft. Sending an edited draft destroys the original after submission.
  • Save draft on close. The discard dialog gains a Save draft option (for edited drafts and fresh composes alike). JMAP emails are immutable apart from keywords/mailboxIds (RFC 8621 §4), so saveDraft re-creates the message with $draft in the drafts mailbox and destroys the replaced one in the same Email/set call.
  • No spurious prompt. Reopening a draft and closing it untouched exits silently — dirtiness is a content fingerprint against the live editor DOM (read through the same issue-[Bug]: blank response #9-safe path as send, so text typed right before closing is never silently dropped).
  • No silent data loss. A draft's Bcc and In-Reply-To/References survive editing even though the composer UI doesn't expose those fields yet.
  • Own-account drafts only: drafts browsed in a shared account's folder stay read-only, consistent with the composer not supporting sending on behalf of shared accounts.

Implementation notes

  • src/api/email.ts: the email-object assembly is extracted from sendEmail into buildEmailCreate (shared with the new saveDraft); sendEmail's request shape and submission behaviour are unchanged.
  • src/lib/draft-compose.ts: pure Email → composer state mapping (body extraction mirrors EmailBodyView, sanitized with stripDangerousTags), plus the cid: → data-URL rewrite (stamps the editor's data-cid marker so rewriteInlineImages converts them back at send time).
  • The composer is gated on the loaded draft so the editor mounts with the draft body as its initial HTML instead of racing a later setHtml into the webview.

Tests

  • saveDraft: create with $draft in the drafts mailbox, atomic destroy of the replaced draft, recipient-less drafts, server rejection.
  • draftToComposeInit: field mapping, text→HTML escaping, dangerous-tag stripping, attachment classification and cid unwrapping, threading headers, empty-draft defaults.
  • rewriteCidSrcToDataUrls: rewrite + data-cid stamp, non-hydrated cid left untouched, no-op without cid refs.

npx tsc --noEmit clean; vitest 448/448 passing (the auth-store.test.ts collection error is pre-existing on main).

🤖 Generated with Claude Code

https://claude.ai/code/session_01ACzTUX8qVZ4qAp8gqUHb5t

…t on close

Drafts were dead ends: tapping one opened the read-only thread view whose
only actions (reply, tag, mark unread) treat it like a received message —
there was no way to resume editing, and the composer never saved drafts.

- Tapping a $draft message in the list now reopens the composer prefilled
  with its recipients, subject, body, attachments (reused by blobId) and
  hydrated inline cid images; the thread view gains an Edit button for
  drafts reached through other paths.
- Sending an edited draft destroys the original after submission; JMAP
  emails being immutable (RFC 8621 §4), saving re-creates the draft with
  $draft in the drafts mailbox and destroys the replaced one atomically.
- Closing a dirty composer now offers Save draft next to Discard; an
  untouched reopened draft closes silently (content-fingerprint baseline).
- A draft's Bcc and In-Reply-To/References survive the round trip even
  though the composer UI doesn't expose them yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACzTUX8qVZ4qAp8gqUHb5t
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant