Skip to content

Keep unsent drafts, as the web's stash does - #13

Merged
simonhamp merged 2 commits into
mainfrom
feat/draft-stash
Oct 18, 2026
Merged

simonhamp merged 2 commits into
mainfrom
feat/draft-stash

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Closing a composer or leaving a screen no longer loses what you typed. The app now keeps unsent drafts the way the web's useStash does.

Rebased onto main at d055983, after #3 to #12 merged. Two commits: the stash itself, and a small follow-up that gives Saved's rows the dots too.

What gets stashed

Same names as the web, per signed-in user:

  • commit: the Log's inline composer. The compose sheet from the nav bar uses the same draft, as the web's dialog shares the Log composer's stash. When the sheet closes, the inline composer takes the draft as the sheet left it.
  • quote-{id}: a quote, per quoted commit.
  • reply-{id}: a reply, per commit, from the commit page's reply bar.

A draft is saved as its field's model changes. EDGE sends no focus or blur, so this rides on the native:model.debounce syncs the fields already send, in updated* hooks. No extra renders.

When the composer opens again (the Log mounting, the sheet opening, the commit page mounting), the draft is back in the field with the web's stash@{0} and drop beside Commit or under the reply bar. drop throws it away. Emptying the field clears the stash and the label. Sending clears it. If devnull says no, the draft stays.

Sign out and delete account both go through Account::forget(), which now forgets the user's drafts too. That also covers being signed out because devnull no longer takes the key, which the web has no equivalent of.

Storage is a new App\Devnull\Stash: one entry per user in the default cache, like ShownTabs and LastVisit. It keeps at most 50 drafts and drops the one written longest ago, so old quote and reply drafts can't pile up forever.

Row badges

The web adds stash@{0} after Quote and Reply when that commit has a stashed draft ("You have an unsent quote here"). That text doesn't fit the action row on a phone, so here it's a 5pt dot after the icon or label, inside the button's 44x36pt target. VoiceOver reads the web's wording. Quote's icon moves about 4pt left of centre while it has a dot, which is worth a look on a real phone. Log, Profile (both pager pages), Saved and the commit page pass the draft names to their rows.

In pastéis mode the pastel sits after the star. The dots on Quote and on an icon-only Reply fit inside the 44pt minimum those buttons already have, so they add no width. Reply only shows its label when there's no "› N replies" link, so a label and a dot never share a row with that link.

Differences from the web

  • The web won't stash a body that contains a secret (it runs @sanity-labs/secret-scan). The app has no secret scanner, so it stashes whatever you type, on the phone only.
  • The web focuses the field and moves the caret to the end after restoring or dropping. EDGE can't focus a field from PHP. The sheet's field already autofocuses.
  • The web stashes images too. The app has no image uploads yet.
  • The web only keeps the first part of a series draft now and clears the old commit:later key. The app has no series composer, so there's nothing to clear.

What to check

  • Type in the Log composer, switch tabs and back: the draft and stash@{0} drop are there.
  • Type in the compose sheet, swipe it away: the inline composer shows the draft. Open the sheet again: same draft, marked restored.
  • Start a quote, swipe it away: the Quote button on that commit has a dot. Quote a different commit: empty.
  • Start a reply on a commit page, go back: Reply on that row has a dot. Open the commit again: the reply is back with the label under the bar.
  • Send each one and check the draft, label and dot are gone. Sign out and back in: no drafts.

How it was verified

php artisan test --compact: 833 pass on the rebased branch, with 24 in tests/Feature/Devnull/StashTest.php and tests/Feature/Screens/DraftStashTest.php. They cover stash, restore, drop, discard on send and sign-out clearing for each kind of draft, plus the dots on Saved and in pastéis mode. Three older tests that expected the sheet to forget its draft now expect it kept. Pint is clean. CI passes.

After the rebase, on a fresh iPhone 17 Pro simulator (iOS 26.2) against the timeline API, signed in as local_dev:

  1. Typed in the Log composer, switched to Notifications and back. The draft was there with stash@{0} drop.
  2. Opened the compose sheet: same draft, marked restored. Added to it and swiped the sheet away. The inline composer showed the longer draft.
  3. Started a quote and swiped it away: only that commit's Quote got the dot. A quote on another commit opened empty.
  4. Started a reply from Reply and went back: that commit's Reply got the dot. Opened the commit from its row: the reply was back with the label under the bar. drop emptied it and the dot went.
  5. A quote started from Saved marked the row there. The same commit's dot showed on the Profile pager's rows.

No local API serves pastéis mode any more, and the timeline API's /me switches it off on every refresh. So for the pastéis check I made a throwaway build where the app always thinks it's on, never committed. The Quote dot sits beside the pastel, "Reply •" keeps its label, and an icon-only Reply's dot fits beside "› 1 reply".

Composer restored Sheet restored Composer takes the sheet's draft
Quote and Reply dots Reply restored Saved
Profile pager Pastéis mode Pastéis, beside "› 1 reply"

Screenshots live on the draft-stash-screenshots branch. Delete it after merge.

🤖 Generated with Claude Code

simonhamp and others added 2 commits October 18, 2026 22:43
The Log's composer, the compose sheet, quotes and replies now stash what
you type, per user, under the web's names: commit, quote-{id} and
reply-{id}. The sheet shares the composer's draft, as the web's dialog
does. A draft that comes back shows the web's stash@{0} and drop.

Sending, emptying the field, drop, signing out and deleting the account
forget it. Quote and Reply mark a commit with a stashed draft with a dot,
since the web's stash@{0} doesn't fit the action row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saved joined the tabs after the stash was written, so its rows had no
dots on Quote and Reply. It passes the drafts' names like the other
screens. A test also checks the dots stay inside their buttons in
pastéis mode, beside the pastel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simonhamp
simonhamp merged commit 06d7bfe into main Oct 18, 2026
1 check passed
@simonhamp
simonhamp deleted the feat/draft-stash branch October 19, 2026 09:24
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