1 Commits
Author SHA1 Message Date
Paul LütznerandClaude Opus 5 67b3a93013 feat(compose): send gives the screen back, and the undo moves to a toast
"Das warten beim Absenden ist pain." Pressing Send kept the composer on screen
for the whole cancel window and made its own pill the cancel — nothing moved,
and the way back was exactly where the way forward had been. That shape is
defensible and it has the one problem that got reported: eight seconds of a
window that will not close is a long time when you have finished writing.

So the default is the other trade. The composer closes at once, the mail is
already where it will live, and the cancel moves to a toast that counts down
beside it. The held shape stays, one setting away —
User::SETTING_COMPOSE_SEND_FEEDBACK, on the compose behaviour panel next to the
forward-quote choice.

Almost nothing new was needed, which is the interesting part. The toast partial
has supported an undo button and a countdown all along, ui--toast already
renders the timer, and compose--undo-send was sitting in the tree marked
UNWIRED "while the replacement is reviewed" — with _send_toast.html.twig beside
it, kept for the same reason. This is that review coming back the other way.
The template is gone, because the toast is emitted by the stream that needs it
now, and the controller's docblock says what it is for again.

The tidying is deliberately the SAME template the end of the cancel window
renders, with its toast suppressed. A send must not settle differently
depending on which shape rendered it, and two copies of "put the message where
it belongs" is exactly how the forward bug that template documents got in.

`hide_after` on the toast IS the cancel window rather than a duration somebody
picked: the Undo goes when the toast goes, so a toast that outlived the window
would offer a cancel that silently fails, and one that died early would take
away a cancel that still worked.

Both settings post their own form. The controller writes only the key it is
sent — an absent `sendFeedback` read as its default would have stored the
default and silently undone the other setting on every save.

Also fixed here, and found by the specs rather than by me:

**Three destructive forms had stopped confirming entirely.** Revoking a trusted
device, revoking every remembered device and switching off two-factor
authentication carry `data-turbo="false"` AND `data-turbo-confirm`. Opting out
of Turbo means setConfirmMethod is never consulted, so the attribute is inert —
they had been `onsubmit="return confirm(…)"`, which worked regardless, and the
conversion silently removed the guard. templates/admin/_live_frame.html.twig
already warned about this exact combination in a comment: "an attribute that
looks like a guard but is not is worse than none."

Fixed with a capture-phase submit listener rather than by editing the three
templates, because the next form to combine the two is then covered by having
done nothing. It re-issues through requestSubmit() so validation still runs, and
the flag that stops it asking twice is compared as a string — dataset values are
strings, and `true ===` would have asked forever.

compose-send-cancel and compose-undo-reopen now put their user on "hold" first
and back to the default afterwards, because they describe that shape;
compose-send-optimistic covers the new default on both surfaces.

Verified: 4009 unit tests, PHPStan clean, both compose suites and the security,
label and confirm specs green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:17:52 +02:00