Files
pl_mail/assets/fonts
Paul Lützner 07253c8bb7 feat(compose): wire the emoji picker and inline images
Two of the five inert buttons in the compose toolbar's placeholder loop now
do what they have always claimed to: the smiley opens a real picker, and the
image button puts a picture in the body rather than beside it. The other
three are left in the loop untouched.

Emoji — the full Unicode set, via `emoji-picker-element` in a `ui--dropdown`
shaped like the integration picker beside it, so Escape and outside-click
come free. Two things the component does by default are not acceptable here
and are configured out: its data source is a jsdelivr URL, replaced by
emojibase JSON vendored per locale under assets/emoji (so search works in
German); and its labels are English, replaced by the same Twig-side i18n
object the rest of the window uses. A Playwright assertion pins the rule the
whole feature is built around — emoji enter the body when somebody picks one
and at no other time, so `:)` and `:smile:` survive typing byte for byte.

Noto Color Emoji (OFL) ships with it, COLRv1, as the ten unicode-range woff2
subsets Google Fonts itself serves: 2.0 MB on disk, but a body with three
smileys fetches only the ~140 KB slice they live in. Scoped to the picker and
the compose body, so the grid matches the phone keyboard people know it by
instead of changing shape per operating system.

Inline images — the read and send sides already existed (MessagePart carries
contentId/isInline; buildEmail() already embeds), so this adds the two
missing halves. DraftAttachmentService::attachInline() stores the bytes down
the same path as an attachment and describes them differently: inline
disposition, a generated content id, isInline — which keeps syncFlag() from
putting a paperclip on a mail whose only "attachment" is a picture the reader
can already see. InlineImageRewriter is the inverse of resolveCids(): the
editor holds the attachment route because a browser cannot render `cid:`, the
wire holds `cid:` because a recipient cannot resolve anything else, and
`data-cid` is the identity that bridges them. It runs inside
DraftPersister::save(), so autosave, send and plainTextBody() all see the
same body.

Paste and drag-drop go through the same endpoint. Without them a pasted
screenshot becomes a data: URI that every later autosave posts back as
base64.

Refusal messages from DraftAttachmentService were spelled in English in the
method that produced them — the one thing a German compose window said in
English. They go through the translator now, in all three locales.
2026-08-12 17:39:16 +02:00
..