Commit Graph
409 Commits
Author SHA1 Message Date
Linus Rath c8edd1f2e1 Merge pull request #715 from lucletoffe/fix/unread-dot-alignment
fix(list): align the unread dot with the first line, not the row midpoint
2026-09-07 17:56:31 +02:00
Linus Rath 6a44442594 Merge pull request #953 from shukiv/fix/list-avatar-vertical-center
fix(list): centre the sender avatar against the row
2026-09-07 17:56:01 +02:00
Linus Rath 75585f224e Merge pull request #987 from ibayue/feature/composer-sticky-toolbar-font-size
feat(composer): sticky formatting toolbar with font-size and background color
2026-09-07 17:42:52 +02:00
ibayueandClaude cc64a9871a fix(composer): background-color picker uses the text-color palette
The dedicated pastel highlighter palette read washed-out against the
light popover. Use the same 16 TEXT_COLORS swatches, grid and styling
as the text-color picker so the two controls read as siblings (Gmail
does the same for both pickers).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:54 +08:00
ibayueandClaude 0459976138 fix(composer): cleaner font-size glyph, number shown once a size is set
The lucide ALargeSmall icon read poorly and lingered after picking a
size. Replace it with two baseline-aligned A's drawn in text (large +
small sharing a baseline - the classic size glyph), and once a size is
selected show that number on the button as before.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:54 +08:00
ibayueandClaude 3957b35809 feat(composer): add background color and clearer font-size button
- Background color toolbar button next to text color, using the
  BackgroundColor extension from @tiptap/extension-text-style (same
  textStyle-mark pattern as Color) with a 2x8 soft-tint highlighter
  palette and a remove action; emitted as inline
  style="background-color: …" which survives email round-trips.
- Font-size button now shows a big-A/small-A glyph (ALargeSmall) with
  the active size as a small badge, replacing the plain "Aa" text that
  was easy to miss.
- background_color/remove_background_color toolbar labels in all 24
  locales; unit tests for apply/unset/parse/coexistence with color and
  font-size on the same span.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:54 +08:00
ibayueandClaude 8fbe508ba3 fix(composer): keep toolbar background at main's exact muted/30
The previous fix paired bg-muted/30 with
supports-[backdrop-filter]:bg-muted/70, intending a fallback for
browsers without backdrop-filter - but every modern browser supports
it, so the 70%-opaque variant was always active and the toolbar read
darker than main at rest. Drop the override entirely: the background
is bg-muted/30 in every state, identical to main, and pinned
legibility comes from backdrop-blur-sm alone (blurring a solid page
background is a no-op at rest; it only softens body text sliding
underneath once the toolbar is pinned).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:51 +08:00
ibayueandClaude 08e279d21d fix(composer): restore main's translucent toolbar look
The sticky rework had swapped the toolbar background to opaque
bg-muted, which reads darker than main's bg-muted/30. Restore the
translucent background and pair it with backdrop-blur + a slightly
stronger muted overlay (only when backdrop-filter is supported) so
pinned-on-body-text stays legible without the flat solid band.
Also pin the font-size button's line height so the toolbar's row
height matches main exactly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:51 +08:00
ibayueandClaude 0ead019c5c fix(composer): keep From/To/Cc/Bcc/Subject fields pinned above the body
Move the fields section out of the composer's scroll container so it
stays visible while scrolling long bodies, matching the pinned
formatting toolbar. Since the fields were the first child of the
scroll container, hoisting them above it is visually identical to
making them sticky - but needs no dynamic offset for the toolbar's
own sticky top (the fields' height varies with Cc/Bcc toggling and
recipient-chip wrapping). The scroll container now wraps only the
body and the signature preview.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:51 +08:00
ibayueandClaude 7e0a86a660 feat(composer): sticky formatting toolbar and font-size picker
- Pin the rich-text toolbar to the top of the composer's scroll
  container (sticky) so it stays visible while editing long bodies;
  the previously translucent background becomes opaque so body text
  doesn't bleed through while pinned.
- Add a font-size picker to the toolbar (14/16/18/20/24/32px plus
  "Default") backed by a new TipTap FontSize extension that stores
  the size as an inline style on the TextStyle mark, mirroring how
  @tiptap/extension-color handles colour - inline styles survive
  email round-trips, and unsetting the size leaves other text-style
  attributes (e.g. colour) intact.
- Add font_size / font_size_default toolbar strings to all 25
  locales.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:58:50 +08:00
bartfaizoli76 06f51486e0 feat: add attachment-actions and composer-attachment-source plugin slots
Two new extension points for third-party attachment/cloud-storage plugins
(e.g. a WebDAV/Nextcloud file browser), neither of which the plugin API
currently supports:

- attachment-actions: a new slot rendered into every attachment's hover
  action row (all six render paths: beside-sender/below-header/mobile,
  each in its compact and expanded-popup form), with the AttachmentInfo
  (name, type, size, blobId, emailId) passed through extraProps. Lets a
  plugin offer a 'save this attachment elsewhere' action next to the
  existing download/preview buttons - previously the only per-attachment
  hook was the read-only onAttachmentDownload observer.

- composer-attachment-source: a new slot rendered in the composer toolbar
  next to the existing composer-toolbar slot, with an onAttach(upload)
  callback in extraProps. Lets a plugin that has already uploaded a file to
  the JMAP server itself (api.jmap.uploadBlob) - e.g. a file picked from an
  external cloud storage browser - have it added to the current draft as a
  real attachment. Previously there was no way for a plugin to add an
  attachment to the draft at all; the closest existing mechanism
  (onBeforeBlobUpload's ExternalAttachmentResult) only replaces a locally
  picked file with body text/a link, it cannot add a brand-new attachment
  from an external source.

Both slots follow the existing PluginSlot pattern (calendar-event-actions):
render nothing when no plugin offers the slot, so this is purely additive.

Motivating use case: the existing nextcloud-attachments community plugin
uploads local files to Nextcloud and appends a share link to the message
body - useful for large files, but there was no way for a plugin to do the
inverse (attach a file that already lives in cloud storage as a real binary
attachment, not a link) or to let attachment recipients save an incoming
attachment back out to their own cloud storage. These two slots make both
directions possible without touching plugin-store or the sandbox bridge -
existing slot/extraProps plumbing (see calendar-event-actions) already
supports arbitrary POJO props.
2026-09-05 11:14:10 +02:00
Shuki Vaknin 6f92547a49 fix(list): centre the sender avatar against the row
List rows lay out with items-start, because the sender line, subject and
preview stack and the body should govern the row height. The avatar
inherited that and pinned itself to the top, level with the sender line,
leaving it visibly high against a two- or three-line row.

Give the avatar self-center in both the single-message and thread rows.
The row keeps items-start, so nothing else moves — in particular the
extra-compact checkbox, which is mutually exclusive with the avatar and
carries its own mt-2 offset, is untouched.
2026-09-02 00:40:54 +03:00
Shuki Vaknin 71dd5f4fed fix(list): stop the batch toolbar hiding the message you just selected
Opening the toolbar feeds its height back into scrollTop so rows keep
their position on screen. At the top of the list there are no rows above
to hold steady, so that scroll instead slides the first message under the
toolbar — select the top message and it disappears behind the bar acting
on it.

Skip the compensation when the list is already at the top and let the
content move down there, which is what the toolbar taking up space
should do. Mid-list behaviour is unchanged, and the scroll is clamped at
zero so collapsing the toolbar cannot drive it negative.
2026-09-01 19:05:35 +03:00
Linus Rath a210b22e49 fix: set name.full on all contact write paths so vCards carry mandatory FN #430 2026-08-31 23:53:24 +02:00
Linus Rath d60eb59575 fix: pad attachment preview overlays for the iOS PWA safe area #936 2026-08-31 21:03:08 +02:00
Linus Rath a69b3651fd fix: render plain-text-only mail as text in the mobile thread view #489 2026-08-31 20:41:45 +02:00
Linus Rath cc90903f05 feat: request delivery notifications and REQUIRETLS when sending 2026-08-29 18:16:52 +02:00
Linus Rath b408b2597a feat: highlight search hits with SearchSnippet/get 2026-08-29 17:47:23 +02:00
Linus Rath 1859783a8b fix: stop the mobile More menu flashing open when a message is opened 2026-08-29 17:29:03 +02:00
Linus Rath 3ea64b2ffe fix: keep message rows in place when the batch toolbar opens 2026-08-29 17:21:02 +02:00
Linus Rath 40cbe82848 fix: drop duplicate selection checkbox in message list 2026-08-29 17:18:49 +02:00
Linus Rath 3ef2e6946f fix: embed the signature into re-opened drafts #848 2026-08-25 19:49:49 +02:00
Linus Rath 3472bc0419 fix: parse calendar invitations in directly viewed shared folders against the folder owner #867 2026-08-25 19:36:04 +02:00
Linus Rath 1faa6f6900 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-25 19:22:50 +02:00
Linus Rath 3ef2b45fb4 feat: configurable message-list ordering #718 2026-08-25 19:20:48 +02:00
Linus Rath e838794133 Merge pull request #895 from romanvru/feat/upload-progress
Real byte progress for attachment uploads, stock and plugin-offloaded
2026-08-25 18:55:07 +02:00
Linus Rath a29cf27ff8 fix: route multi-account email lookups and invitation parsing by source account #847 2026-08-25 18:50:50 +02:00
Linus Rath 342217ad99 fix: keep read/unread toolbar button a fixed width so buttons don't jump on open #864 2026-08-25 18:16:51 +02:00
Roman Varlakov 7928f72ee2 feat: real byte progress for attachment uploads, stock and plugin-offloaded
The composer chip only knew a boolean: `uploading` flipped on, an
indeterminate bar animated at 40% width, and nothing anywhere carried a
percentage, so a large attachment on a slow link looked frozen for the
whole transfer. The JMAP client has been able to report real progress
since #333 gave uploadBlob an XHR path with onProgress; the composer
just never passed it.

Stock path: pass onProgress and the existing AbortController's signal to
client.uploadBlob. The chip's bar becomes determinate as soon as bytes
are reported (falling back to the indeterminate animation otherwise),
and cancel now aborts the transfer itself instead of being checked only
once the upload has finished.

Plugin offload path (#751): a plugin that offloads an attachment moves
the bytes itself via api.http.post, which the composer cannot observe.
It also cannot be handed a callback: the sandbox bridge marshals
functions host-to-plugin only, and the File a plugin holds is a
structured clone, so object identity cannot correlate its request back
to an attachment. The staged file id can: the composer generated it and
handed it to onBeforeBlobUpload. A plugin echoes it back as the new
optional `progressFileId` on the post options; the host sends the Blob
via XMLHttpRequest (fetch exposes no upload progress events) and reports
bytes into a host-side registry keyed by that id, which the composer
subscribes to before running the hook. Progress never crosses into the
sandbox, and reports for an id the composer is not tracking are dropped,
so the plugin API grows exactly one optional field. Calls without
progressFileId keep the fetch path untouched.

Closes #754
2026-08-23 19:44:56 +06:00
Linus Rath a5d08b573a Merge pull request #862 from dealerweb/fix/signature-unlock
fix: let a double-click unlock the embedded signature for editing
2026-08-21 09:26:50 +02:00
Linus Rath 93fd9068f7 Merge pull request #863 from shukiv/feat/mobile-swipe-actions
feat(mobile): swipe list rows left/right for quick actions
2026-08-20 21:59:15 +02:00
Linus Rath 37d85e5447 feat: fullscreen email view in the standard interface 2026-08-20 16:19:12 +02:00
Linus Rath 5d2b937ae3 fix: keep attachments when re-opening a draft and destroy old draft versions only after a successful create/send #849 2026-08-20 13:25:25 +02:00
dealerweb bc5b18bea9 fix: address the review notes on the signature unlock
- hoist only font longhands from wrapper divs: applying the `font`
  shorthand after the line's own styles would reset the font-family/
  size the line declares itself; the CSSOM exposes shorthand
  declarations through the longhand queries anyway
- only preventDefault on dblclick when the unlock actually ran, so a
  read-only editor keeps its default double-click behaviour
2026-08-19 11:56:18 +02:00
Shuki Vaknin 9eaaa614fe fix(mobile): keep swipe action icon on the physically exposed edge in RTL
The reveal layer used logical justify-start/-end, which flip under RTL, so
the archive/delete icon appeared on the wrong side. The exposed edge
follows the physical translate direction, not text direction, so force the
decorative layer to LTR.
2026-08-19 03:46:10 +03:00
Shuki Vaknin 78c84f19a5 feat(mobile): swipe list rows left/right for quick actions
Adds Gmail-style horizontal swipe actions to message rows on touch
devices. Swiping a row past a threshold fires a configurable action and
snaps back; a coloured background with an icon is revealed under the
finger and grows when the release would commit.

- New `useSwipeActions` hook: claims a touch as a swipe only once the
  horizontal travel dominates (vertical stays native list scrolling via
  `touch-action: pan-y`, no preventDefault), resists past the commit
  distance, and suppresses the tap that would otherwise follow.
- Two settings (Settings -> reading/email behavior): "Swipe right
  action" (default Archive) and "Swipe left action" (default Delete),
  each one of none / archive / delete / toggle read / toggle star /
  spam. A direction set to an action the current folder does not expose
  simply does nothing.
- Composes with the existing long-press (context menu) handlers; desktop
  is unchanged (hover actions + right-click).
- Scoped to single-message rows; unit tests cover commit/short-drag/
  no-action-side/vertical-scroll/tap-suppression.
2026-08-19 02:18:01 +03:00
dealerweb 3d1bdf0943 fix: let a double-click unlock the embedded signature for editing
Since the signature became a verbatim, non-editable atom (#476), there
was no way to edit it in the composer at all (#822) - the styling
guarantee silently cost the editability #329 had established. Keep the
atom as the default so branded signatures still survive 1:1, and let an
explicit gesture opt out: double-clicking the block dissolves it into
regular editable content at the same position. Double-clicks that land
on a link are ignored (the click is working the link), and Enter on the
selected block keeps ProseMirror's default create-paragraph-after
behaviour.

Faithfulness of the dissolved signature:
- Leaf <div> lines become paragraphs; only vertical margins the line
  does not declare are reset to 0 (how a div actually renders), so
  explicit spacing and indents survive. Divs carrying hr/pre and other
  non-line blocks are left to the schema parse.
- Inherited styles (font, color, ...) on wrapper divs - which have no
  schema node and would be dropped - are hoisted onto the generated
  line paragraphs, the line's own declarations winning.
- The content is parsed as a CLOSED document via the schema DOMParser
  and spliced in with one transaction; insertContentAt fits an open
  slice, which can split text runs at hard breaks and turned <br>-lined
  signatures into one spaced paragraph per line.

The bracketing data-signature-block markers live outside the atom and
stay put, so the identity-switch splice and the send path's
already-embedded detection keep working on unlocked content, and
switching identities re-embeds a fresh locked block. Undo restores the
atom. A translated tooltip on the block makes the gesture discoverable.
2026-08-18 17:31:47 +02:00
Linus Rath eac56ca3bf fix: enforce external media preference on plugin-rendered bodies #797 2026-08-16 23:23:27 +02:00
Linus Rath 452f46f904 fix: hover action background is transparent on tagged rows 2026-08-16 23:16:29 +02:00
Linus Rath 83026fe621 fix: keep already written text when applying a template in compose #540 2026-08-16 22:20:49 +02:00
Linus Rath d8c9aaa66a feat: add setting to render plain-text emails in normal or monospace font #830 2026-08-16 21:54:52 +02:00
Linus Rath ffff165a4b Merge pull request #800 from rotterp/fix/shared-folder-account-context
fix: a directly-selected shared folder has no account context (tags, compose identity)
2026-08-16 21:27:18 +02:00
Linus Rath 527872cc13 Merge pull request #773 from dealerweb/fix/mailto-unsaved-draft-guard
fix: route mailto composer replacement through the close guard
2026-08-16 03:08:44 +02:00
Linus Rath 47cf0ed90d feat: rework pro split-screen shell, per-pane tab strips, drag & drop, pane-scoped overlays 2026-08-16 03:03:40 +02:00
Linus Rath 3a6d14871f fix: reach the tag and move sub menus in the mobile more actions menu #779 2026-08-16 02:41:56 +02:00
Linus Rath 89139b4055 fix: keep the signature in saved drafts 2026-08-16 01:50:39 +02:00
dealerweb 4e6043cf56 test: cover the failed-save rescue dropping the close follow-up
When the server refuses the draft save, the composer resets its explicit-
close flag so the unmount stash hands the text to the continue-draft slot
(#702). The close follow-up must be dropped in that case - running it
would open the new session on top of the rescue. This was the one branch
of the close guard without a test.
2026-08-13 11:00:13 +02:00
dealerweb 9ff14f5781 fix: route mailto composer replacement through the close guard
Opening a mailto: link while the composer held unsaved text used to
replace the session outright and drop the text on the floor. The mailto
entry point now hands a follow-up to the composer's dirty-aware close
handler, so the "Save or discard draft?" dialog runs first: save and
discard continue into the mailto draft, cancel leaves the current draft
untouched. When a failed save hands the text back to the continue-draft
slot (#702), that rescue wins and the follow-up is dropped.
2026-08-13 11:00:13 +02:00
Patrick RotterandClaude Opus 5 3560cf258b fix: route keyword writes to the selected shared account
Tags, pins and $answered/$forwarded/$mdnsent set on a message in a
shared/group mailbox opened directly from the "Shared" sidebar section
did not persist: they showed until the next reload and were then gone.

The call sites resolved the target account as
`isUnifiedView ? email.sourceAccountId : undefined`. Outside the unified
view that is always `undefined`, so the write went to the reaching
client's own account, which the server answers with `notUpdated` /
`updated: null` and no error - the same silent no-op that
`email-store-shared-folder-actions.test.ts` already covers for the batch
actions. Emails fetched from a shared folder this way are undecorated
(no `sourceAccountId`); the owner is only recoverable from the selected
mailbox.

`toggleStar` never had the bug because it routes through
`resolveEmailActionContext`, which resolves the owner from the email's
source in an aggregate view and from the selected mailbox otherwise.
That resolver was reimplemented inline at each keyword call site, and
the inline copies only ever covered the aggregate half - so starring a
shared message persisted while tagging the same message did not.

Move the two keyword writes into store actions built on that resolver
and route every call site through them, including the read-receipt and
Pro-compose paths that previously passed no accountId at all.

The store actions resolve only for an email the store actually knows.
A Pro tab fetches its own email and never enters `emails` or
`selectedEmail`; for those the selected mailbox says nothing about the
message's owner, so guessing from it would send an own-account write to
a shared account whenever a shared folder happened to be open. An
unknown email keeps the previous behaviour instead.

Behaviour on own-account and unified-view messages is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:42:06 +02:00
Linus Rath 70383dea18 Merge pull request #796 from lucas-gaitzsch/fix/scheduled-send-polish
fix: polish scheduled send follow-ups
2026-08-12 18:12:28 +02:00