feat(compose): sending with a pulse and a way out, and forwards that behave

The send button's "Sending…" becomes a living one-two-three dot cycle — a
static label reads as a hang — and a Cancel appears beside it for the
in-flight moment: every send lands in a cancel window, so a click here is
remembered and the window's own control is pressed the instant it renders.
The watcher lives at module scope because the stream that renders it
replaces the very element a controller-held watcher would die with.

A forward opens unsigned (its content is the mail being passed on), with
the address rows unfolded and a blinking caret in a typable To — Tom
Select's enhancement is awaited, not hoped for — and the quoted original
counts as content, so no forward is questioned as an empty mail. The
"show quoted text" fold stays default but becomes a choice: Settings →
General → Composing, stored only when switched off, absent key meaning
default as the settings bag always reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Fable 5 committed 2026-08-18 00:27:44 +02:00
1 parent 470e58d324
commit 2828dc31dc
17 files changed
+564 -31

No files matched your search

+15
View File
@@ -78,6 +78,21 @@ Variables: message, params (from the including context); placement.
}),
}|json_encode|e('html_attr') }}">
{# Hidden until a send is in flight. While the request is out the pill
says "Sending" with living dots, and this is the way out: a click here
marks the send as unwanted, and the moment the server answers with its
cancel window (every send gets one — see CANCEL_WINDOW_MS) the undo is
fired on the user's behalf. Honest about the race: if the window is
already gone the cancel simply fails the way a late click on the
countdown bar does. #}
<button type="button" hidden
data-compose--compose-target="sendCancel"
data-action="click->compose--compose#cancelInFlight"
class="flex items-center justify-center text-xs text-ink-muted underline
px-2 py-1.5 max-md:min-h-11 hover:text-ink transition-colors">
{{ 'compose.sending_cancel'|trans }}
</button>
<div class="flex items-center rounded-full overflow-hidden bg-accent">
<button type="submit"
data-compose--compose-target="sendBtn"
+6
View File
@@ -62,6 +62,7 @@ Variables: form, message, ctx (an App\Domain\DTO\Mail\ComposeContext)
tooLarge: 'compose.attach_too_large'|trans({ '%name%': '%s', '%limit%': '%d' }),
imageFailed: 'compose.image_insert_failed'|trans,
noSignature: 'compose.signature_none'|trans,
cancelSend: 'compose.sending_cancel'|trans,
}|json_encode|e('html_attr') }}"
{# The toolbar controller's own vocabulary. Same arrangement and same
reason as compose--compose's above: the link popover writes words
@@ -80,6 +81,11 @@ Variables: form, message, ctx (an App\Domain\DTO\Mail\ComposeContext)
a spinner where a signature should be. See signature_map(). #}
data-compose--compose-signatures-value="{{ signature_map()|json_encode|e('html_attr') }}"
data-compose--compose-inline-value="{{ inline ? 'true' : 'false' }}"
{# 'forward' moves the caret to To, spares the quote-only body the
empty-mail question, and is the case the quote-collapse setting
below is about. See ComposeController::forwardMessage(). #}
data-compose--compose-mode-value="{{ mode|default('new') }}"
data-compose--compose-collapse-forward-quote-value="{{ app.user and false == app.user.getSetting(constant('App\\Entity\\User\\User::SETTING_COMPOSE_FORWARD_QUOTE_COLLAPSED'), true) ? 'false' : 'true' }}"
data-compose--compose-min-chars-value="{{ min_chars|default(5) }}"
data-compose--compose-max-attachment-bytes-value="{{ constant('App\\Controller\\Mail\\ComposeAttachmentController::MAX_ATTACHMENT_BYTES') }}"
{% if thread is not null %}data-compose--compose-thread-value="{{ thread }}"{% endif %}