Files
pl_mail/templates/compose/_inline_undo.stream.html.twig
Paul Lützner 85cc9e7166 refactor(mail): compose gives up the parts that were never about HTTP
1,130 lines and 93 methods, with sixteen section headers the author had already
drawn — decomposed in the head, not on disk. Four pieces come out.

ComposeAttachmentController takes the three upload actions, and they are the
one genuine seam in that class. Everything else — new, reply, forward, draft,
send, schedule, undo — is a single pipeline building the same ComposeType form
from the same request context and answering with the same window, so pulling
"sending" away from "composing" would only have moved nine shared private
helpers into a shared parent and called the result two classes. These three
share none of it. MAX_ATTACHMENT_BYTES goes with them, which is where the file
picker and the window now read it.

ComposeContext replaces `array{inline, frame, thread, replyTo, urlParams}`, a
shape written out in EIGHT docblocks because every method touching it had to
redeclare it for PHPStan — so a key added to it meant eight edits, and a key
misspelled at a read site meant null rather than an error. `urlParams` was also
a STORED key derived from the other four, so the array could carry a value that
disagreed with the fields it came from; it is a method now and cannot. A value
resolver builds it, so actions type-hint it and four of them stop taking a
Request they no longer need.

ComposeWindow and ComposeFormFactory take the rest: the default From account,
the signature seed, the picker's services, the unsendable-address check, the
discard's row arithmetic, and the one place the form is built. All of it was
reachable only through a route before, so "which account is the default From"
could not be asked without building a request.

Two template assumptions fell out of it. `ctx|merge({thread: …})` was the last
thing standing between this shape and being a type — merge is an array filter —
and is `ctx.withThread()` now. And _window.html.twig defaulted every field it
read, with fallbacks that had DRIFTED from the controller's: it defaulted
`frame` to 'compose_dock', so a window could render as dock while the server
thought it was inline. A guaranteed type needs no fallbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 801639173c4c5017bfa0852791eb97306ee9db39)
(cherry picked from commit 2e1d713897de293c5e7623f95e661286dd245951)
2026-08-17 11:25:39 +02:00

46 lines
2.1 KiB
Twig

{#
compose/_inline_undo.stream.html.twig
─────────────────────────────────────────────────────────────────────────────
Cancelling an inline send: take the message back out of the conversation and
re-open the draft in the inline editor with its text intact.
Variables: form, message, ctx (the compose context), threadEntity (MessageThread|null),
pickerIntegrations (the reopened window reads it, and the include is `only`)
#}
<turbo-stream action="remove" target="thread_message_{{ message.id }}">
</turbo-stream>
{% if threadEntity is not null %}
{# Back to being a draft in the conversation. The editor below hides this
row while it is open, and restores it on close. #}
<turbo-stream action="append" target="thread_messages_{{ threadEntity.id }}">
<template>
{% include 'mail/_thread_message.html.twig' with { message: message, expanded: false } only %}
</template>
</turbo-stream>
{% endif %}
{% if threadEntity is not null %}
<turbo-stream action="replace" target="reply_zone_{{ threadEntity.id }}">
<template>
{% include 'mail/_reply_zone.html.twig' with { thread: threadEntity } only %}
</template>
</turbo-stream>
{% endif %}
{# The window's own root is <turbo-frame id="compose_inline">, so replacing
the empty frame the reply zone just rendered swaps the editor back in. #}
<turbo-stream action="replace" target="{{ ctx.frame }}">
<template>
{# ctx.withThread(), not ctx|merge({thread: …}): ctx is a
ComposeContext now, and merge is an array filter. The object answers
the same question — see App\Domain\DTO\Mail\ComposeContext. #}
{% include 'compose/_window.html.twig' with {
form: form,
message: message,
ctx: ctx.withThread(threadEntity is not null ? threadEntity.id : null),
pickerIntegrations: pickerIntegrations,
} only %}
</template>
</turbo-stream>