Skip to content

Make stars, commits, replies and messages instant, with ToastKit toasts - #86

Draft
simonhamp wants to merge 6 commits into
mainfrom
feat/instant-actions
Draft

simonhamp wants to merge 6 commits into
mainfrom
feat/instant-actions

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Starring, committing, replying and sending a Remotes message now feel instant. Each tap writes a row to a new pending_writes table in the app's SQLite, updates the screen and the response cache straight away, and sends the request from a background task. Before this, a star waited on a synchronous request on the UI thread.

When devnull turns a write down, or the request fails or times out, the change is undone and a toast says why in devnull's words.

  • Star or unstar: reverts, and the toast offers Retry.
  • Commit, quote or reply: the staged row goes and the composer reopens with the text, photos and poll. If you've moved to another screen, the toast offers Edit, which takes you back to it.
  • Remotes message: stays as rejected with the inline Retry, and the toast offers Retry too. Staged and rejected messages now survive leaving the thread.
  • A 401 still signs you out, and signing out empties the outbox.

If the app is killed mid-send, stars and messages are resent on the next launch. Messages de-dupe on their client id. Commits are not resent, because the commit API has no idempotency key and a resend could post twice. They come back as failed with Edit instead.

Every Dialog::toast is now a ToastKit toast (victorycodedev/toastkit 1.4, free), through a small App\Devnull\Toasts helper. "Opened issue #N." gets a View issue button. Alerts are unchanged.

Things to look at:

  • ToastKit comes from Simon's fork (dev-fix/android-toasts-above-sheets as 1.4.0) until Draw Android toasts above sheets and dialogs victorycodedev/toastkit#5 is released. 1.4.0 draws Android toasts behind bottom sheets and dialogs, so a failed commit's error toast was hidden under the reopened composer. Switch back to the release once it's out.

  • A failed commit reopens the composer once the old sheet has gone. iOS reports that close, so the reopen waits for it. Android's bottom sheet only fires @dismiss when the user drags it down or presses back, so on Android it reopens straight away. Android's sheet also covers the error toast once it's back up.

  • Sending from the reply bar no longer jumps to the new reply. It shows under the parent as staged, then becomes the real reply.

  • If a commit lands while you're on another screen, the Log shows it in the new commits pill rather than at the top.

  • Composer server errors now show as a toast with the composer reopened, not inline in the sheet. Photo and camera errors still show inline.

  • ToastKit 1.4.0 came out on 21 October. I registered its service provider by hand because native:plugin:register couldn't edit NativeServiceProvider.

  • Tests now use LazilyRefreshDatabase since screens touch the database.

Screenshots

Taken on an iOS 26.2 simulator and an Android 15 emulator, signed in against a local devnull API. The failure shots were taken with the API stopped.

Toast

Deleting a commit. Before is the old Dialog toast at the bottom, after is the ToastKit toast at the top.

iOS light iOS dark Android light Android dark
Before
After

Star while devnull is unreachable

Before shows the error inline on the commit page. After, the star goes back and the toast offers Retry.

iOS light iOS dark Android light Android dark
Before
After

Commit on its way

Taken just after tapping Post, with the local API slowed down. Before, the sheet waits for the answer. After, the sheet is gone and the commit sits dimmed at the top of the Log as staged.

iOS light iOS dark Android light Android dark
Before
After

Commit while devnull is unreachable

Before shows the error inline in the sheet. After, the composer opens again with the text. On iOS the toast shows over it with Edit. On Android the reopened sheet covers the toast.

iOS light iOS dark Android light Android dark
Before
After

Remotes message while devnull is unreachable

Both show the message as rejected with retry. After adds a toast with Retry.

iOS light iOS dark Android light Android dark
Before
After

Tests: 1226 passed on main, 1234 passed here. New coverage is in tests/Feature/Screens/OutboxTest.php.

🤖 Generated with Claude Code

simonhamp and others added 6 commits October 24, 2026 20:25
The built-in toast is a plain system toast. ToastKit draws native
overlays on both platforms with variants and action buttons, which the
outbox work needs for Retry and Edit.

Every Dialog::toast call now goes through App\Devnull\Toasts, which
sets the position, font and durations in one place. Confirmations are
neutral and failures are errors. Dialog::alert is unchanged.

The plugin is free, so it also works with composer.free.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starring went through a synchronous request on the UI thread, and the
composer and reply bar waited on devnull before closing. Now every one
of these writes goes into a pending_writes table first, the screen shows
it straight away, and the request goes from a background task. Nothing
on the tap path touches the network.

The answer comes back as a shared AsyncTask event, so whichever screen
is showing handles it, not only the one the write came from. On success
the row goes and the screen and ResponseCache take devnull's answer. On
failure the screen and cache go back and an error toast says why:

- A star flips back, and the toast offers Retry.
- A commit or reply leaves the screen. Its text goes back into its
  stash and the failed row keeps the photos and poll. The screen that
  owns the composer opens it again with everything. Elsewhere the toast
  offers Edit, which opens the composer or the commit it was for.
- A Remotes message stays in the thread as rejected with its inline
  Retry, and the toast has Retry too. Staged and rejected messages now
  survive leaving the thread.

New commits and replies show as a dimmed "staged" row at the top of the
Log or under their parent until the real one replaces them.

When the Log opens, rows left over from a killed app are handled. Stars
and messages are sent again since devnull can't do them twice (messages
dedupe on client_id). Commits are not: the commit API has no
idempotency key, so they're marked failed with a toast and Edit. A
commit whose task times out says devnull may have it for the same
reason.

Signing out empties the outbox. Tests use LazilyRefreshDatabase now
that screens touch the database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the sheet was swiped away before devnull opened the issue, the toast
used to say "Opened issue #N." with no way to get to it. It now has a
View issue button that opens the issue in the in-app browser, the same
as the button in the sheet. Only http and https links are opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmit

With no connection, devnull's failure lands while the sheet the commit
was sent from is still sliding away. The sheet was opened again on the
draft right then, and when iOS reported the old sheet gone,
commitSheetClosed() reset the sheet and closed the new one too. Because
the reopen had reported success, the toast had no Edit button, so the
draft was only reachable through the stash.

Closing the sheet from here now marks it as closing until iOS says it
has gone. A failure that lands in that window is held, the toast keeps
Edit, and the sheet opens on the draft once commitSheetClosed() runs.
The toast only drops Edit when the sheet really opened. Quotes and
replies written in the sheet go through the same path. The reply bar
has no sheet, so it isn't affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The last fix held the reopen until the sheet's @Dismiss arrived. That
works on iOS, where the sheet reports every close. On Android the sheet
only sends @Dismiss when the user drags it away or presses back, never
when PHP closes it. So sheetClosing stayed true for good and every
failed commit after that waited for an event that never came.

Android doesn't have the race anyway. It keeps the sheet drawn through
its hide animation, so showing it again right away just shows it. The
closing flag is now only set on iOS (System::isIos()), so on Android
the sheet opens again at once and the toast has no Edit.

Nothing else waits on @Dismiss after a close from PHP. Cleanup on
Android works as it did on main: the next open resets the sheet, and
photos sent with a commit leave the sheet before it closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ToastKit 1.4.0 puts its Android toasts in the activity's decor view, so any
toast shown while a bottom sheet is open lands behind the sheet. The commit
sheet's failure toasts were hidden that way. The fork's
fix/android-toasts-above-sheets branch hosts them in their own windows
instead. It's required as 1.4.0 until the fix is released upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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