Commit Graph
53 Commits
Author SHA1 Message Date
Linus Rath a4985a63a8 fix: open replies over the message so widget links work offline 2026-09-26 20:25:21 +02:00
Linus Rath c165fef588 feat: add android home-screen widgets for mail, calendar, tasks and files 2026-09-26 18:53:25 +02:00
Linus Rath 13cba64854 fix: translate the viewer's leftovers and label its icon buttons
The message details panel built its received line, attachment summary
and spam score by gluing strings together; each is now one ICU message
(the attachment count is a proper plural). The SPF/DKIM/DMARC results
use the webmail's translated result names, and an invitation's date is
formatted in the app language instead of always in English.

The quick reply box wrote its own "date, name <email>:" quote header;
it now uses the composer's translated "On {date}, {from} wrote:" line
so both kinds of reply look the same.

The back button, the subject and header star buttons, the sender
avatar and the More/Tag sheet close buttons get screen reader labels,
and the screens i18n test covers the viewer files.
2026-09-24 21:44:46 +02:00
Linus Rath b7a5953255 fix: ask for both neighbouring conversations in one request
When the pages beside the open message were let go, each asked for its
conversation on its own, so every open sent two `Thread/get` requests
for its neighbours.

Conversations asked for in the same tick for the same account now go
out together: `getThreadsHeaders` takes several thread ids in one
back-referenced Thread/get → Email/get, and the cache batches its
callers into it. A conversation the server does not know, or a failed
request, still falls back to the copy held from before.

`getThreadEmails`, which downloaded every member's full body, had no
callers left; it is removed and its tests moved to `getFullEmails` and
`getThreadsHeaders`.
2026-09-24 20:29:41 +02:00
Linus Rath d2066dad07 fix: act on the exact row picked in "All folders" and tag lists #1082
An "All folders" search or a tag view holds rows of the own and the
shared accounts, and Stalwart hands out ids per account, so an own and a
team message can share an id. The list still named its rows by bare id:
selecting one of such a pair selected both, a batch action hit both, and
a swipe or single-row action took whichever came first, in the other
account.

The list now names every row by `rowKeyOf` (its id, scoped by its
account in a list spanning accounts; the bare id in a folder, so nothing
changes there) end to end:
- FlatList keys, row presses, swipes, selection and select-all
- thread expansion of a selection (`expandThreadSelection`)
- every store action the list calls: read, unread, star, pin, the
  per-account keyword batch, move, archive, delete and spam, and the
  rows they drop or keep in view
- the viewer's pages, which now come only from the rows of the opened
  message's account (`listRowsOfAccount`), and its mark-read, which
  goes through the store when the list holds that account's row

A key whose row is no longer loaded is ignored rather than guessed onto
the open folder's account. The unified inbox already keys its rows by
account and needed no change.
2026-09-24 20:28:43 +02:00
Linus Rath ef143a4dbb fix: take a conversation's size from the list row's own account
The placeholder cards held for a loading conversation looked its size up
by bare thread id whenever the open folder was the message's account.
Rows of an "All folders" search or a tag view now come from several
accounts and the list keys their conversation sizes by account, so the
viewer looks the size up the same way, and only for pages that are the
list's own rows.
2026-09-24 20:12:17 +02:00
Linus Rath 2a055ddfe2 fix: keep a star set while the mark-as-read delay runs 2026-09-24 20:12:17 +02:00
Linus Rath cd911ada79 fix: show toasts on every screen, including the Unified Inbox and contacts
Only the tab screens and the viewer had a toast host, so a viewer
action that failed after going back to the Unified Inbox or a contact
(both stack screens of their own) raised a toast nobody saw.

Every stack screen now gets a host through the navigator's
`screenLayout`, and only the focused screen's host renders, so the
screen kept live under the top one never shows the same toast a second
time. The tab screens' and the viewer's own hosts are gone.
2026-09-24 20:12:17 +02:00
Linus Rath e0b72883ac fix: show the tapped message when the viewer is already open
Opening the viewer while it was on top, e.g. by tapping a notification,
did not push a new screen: React Navigation handed the new target to
the same route as new params, and the viewer, which fixes its message,
account and page list when it opens, ignored them. The tap did nothing.

The route now mounts the viewer afresh for every new target it is
given, keyed by the message, its account and the open, so the new
message is shown with its own page snapshot. Params that stay the same
keep the viewer as it is.
2026-09-24 20:12:17 +02:00
Linus Rath 54b169c6c7 fix: open a message at its full height and keep the quick reply still
Every body started as a 120 px strip that grew once the page had
measured itself, so an open showed a skeleton, then a clipped strip of
the body, then the full body, and the quick reply box below it jumped
down as the WebView grew. A conversation arriving after the message
swapped the single-message layout for cards, which reloaded the body
and pushed it down under a "Loading conversation" row.

- `lib/body-heights` remembers the last measured height of each body
  (per width); a body shown before starts at that height.
- A page showing one message stretches its body over the space below
  the header until it has measured itself, and the quick reply sits at
  the bottom of the page, so it does not move whether the body turns
  out shorter or longer. Conversation cards start from an estimate
  (most of a screen for HTML, the wrapped line count for plain text).
- A single message is rendered through the conversation's card list,
  so its body stays mounted when the conversation arrives; while it
  loads, the size the folder list knows for it holds the older cards'
  places, and the loading indicator sits in the subject row.
- An open card whose body has not arrived shows its header over
  placeholder lines, with its reply buttons disabled until it has.
2026-09-24 20:12:17 +02:00
Linus Rath 01fc6fbba5 fix: let the pages beside an opened message wait until it has loaded
The pager renders the pages before and after the open one, and each
fetched its message and conversation and mounted its own WebView at the
same time as the message the user had tapped, which competed with it
for the network, the JS thread and the WebView process.

The neighbouring pages now show their header and placeholder lines
until the open message's body has reported its height, the user starts
to swipe, or 1.5 s have passed. Their details are then loaded together
in one request, and their conversations and WebViews follow.
2026-09-24 20:12:17 +02:00
Linus Rath 7cdebc465a fix: stop asking for the identities on every message open
The viewer and the read-receipt banner each sent `Identity/get`
whenever the identity list was empty. Identities are not persisted, so
the first open of every session asked twice, and an account the server
lists no identities for asked on every open.

The settings store now remembers which signed-in account (server,
login, JMAP account) its identities were read for, and the viewer and
the banner only ask when they are not held for the current account.
Concurrent reads share one request. Switching accounts drops the old
account's identities instead of letting the quick reply send through
one the new account does not have.
2026-09-24 20:12:17 +02:00
Linus Rath ad1f053f72 fix: offer archiving in the viewer for a folder named Archive without the role 2026-09-24 20:12:17 +02:00
Linus Rath 1348e7d20a fix: start loading a message on the tap, before the viewer opens
The detail and conversation requests only went out once the viewer had
mounted and its effects ran, after the navigation had rendered.

A tap on a row in the folder list, the Unified Inbox or a contact's
activity, and a notification tap, now start the message's detail and
its conversation (when threading is on) through the shared cache, and
the viewer joins the requests already in flight. It runs on the tap and
not on press-in, because on a phone a touch that starts a scroll
presses rows too.

The Unified Inbox and contact activity also leave their rows in the
cache, so pages handed over by id get their header painted from the row
(and the right thread id) instead of a skeleton.
2026-09-24 20:12:17 +02:00
Linus Rath ac8e3fb2e9 fix: paint the message header from the list row while the body loads
The viewer showed a full skeleton until the message's detail had come
back, although the list row it was opened from already holds the
subject, sender, date and avatar. The header appeared about 0.4 s after
the tap.

A page whose list row is known now renders the real subject and header
from that row straight away, with placeholder lines where the body
goes; the body (and the star, reply and quick reply that need the full
message) follows when the detail arrives. Pages handed over by id only
(notification, deep link) keep the skeleton.
2026-09-24 20:12:17 +02:00
Linus Rath 33e303e248 fix: open a message from the cache and fetch only the open cards' bodies
Every open went to the network for the full message, even for a message
the viewer had just shown, and the conversation fetch then downloaded
every member's full body again, the opened one included. The pager's
three panes each started their own detail and thread fetch, so one open
from the Unified Inbox fetched the body twice and the thread twice.

Messages and conversations now live in a shared cache
(`lib/email-detail-cache`), keyed by server, login and JMAP account:

- a held copy renders at once; bodies never change, so it is only
  checked, with a keywords/folders-only `Email/get`, when the account's
  Email state (from push or our own reads) moved on or the list row the
  message was opened from disagrees with it;
- a copy in the offline cache is shown before anything goes out and is
  checked the same way;
- a conversation is listed from its members' headers (`Thread/get` and
  list properties in one request) and bodies are downloaded only for
  the open cards, in one batched `Email/get`;
- concurrent loads of the same message or thread share one request.

The store's network-first `getEmailDetail` is gone; its offline-copy
refresh moved into the cache.
2026-09-24 20:12:16 +02:00
Linus Rath ddff2f7b66 fix: queue star, tag and unread from the viewer while offline
The viewer's star, tag and mark-unread buttons called the API
directly, so offline they failed and the list row behind the viewer
kept its old state. They now go through the email store's keyword
action like the list's selection actions: offline the change waits in
the outbox, and the list row follows. The store action takes the
viewed message and its account, so it only touches the list when the
list holds that account and never a same-id row of another one.
2026-09-24 19:45:07 +02:00
Linus Rath 9e95273c14 feat: tell the user when a viewer action fails and when mail is sent
The viewer fired its store actions with `void` and went back to the
list, so a refused delete, archive, move, spam report, star, tag or
read change failed silently, and nothing confirmed a sent message.

- each viewer action now reports a failure as an error toast, and a
  refused star, tag or read change is taken back on screen
- the viewer mounts its own toast host, since the list's sits under it
- an immediate send from the composer or the quick reply box shows
  "Email sent"; a send held for the undo delay keeps its undo bar
  (webmail `ec67ab99`, `b03a0c1d`)
2026-09-24 19:06:55 +02:00
Linus Rath 33d26744b6 fix: archive from the viewer when the list does not hold the message
Viewer Archive looked the message up in the loaded list and silently
did nothing when it was not there: for messages opened from the Unified
Inbox, a notification or a deep link, and for any message past the
loaded page. It also resolved the Archive folder from the folder open
in the list, so it was only offered for mail of that folder's account.

`archiveEmail` now takes the viewed message like delete, move and spam
do: it archives the viewer's copy into the Archive folder of the
message's own account (year/month foldering included) and leaves the
open list alone when that is another account.
2026-09-24 19:06:55 +02:00
Linus Rath 5e42c6737e fix: report spam from the viewer the way the list does #695
The viewer's Spam and Not spam buttons were a plain move into Junk or
Inbox: they sent no `$junk`/`$notjunk` flip, so the server's classifier
never learned from them, and they ignored the "trash-and-read" delete
action. They now call the store's `markSpam`/`unmarkSpam`, the path the
list and swipe actions use, which take the viewed message and file it
in its own account's Junk or Inbox, also when the list does not hold
it. Not spam is only offered when that account has an Inbox.
2026-09-24 19:06:55 +02:00
Linus Rath 9c31de2e80 fix: keep the viewer on the opened message when new mail arrives
The viewer's pager followed the store's live list. New mail or a
refresh shifted the rows under the fixed scroll position, so the
screen switched by itself to another message while Star, Delete and
Archive kept acting on the one that had been opened.

The pages are now taken once when the viewer opens, in `viewerPages`,
so the page on screen and the message the toolbar acts on stay the
same. Delete, archive, move and spam leave the viewer as before, so no
page is left showing a message that is gone.
2026-09-24 19:06:54 +02:00
Linus Rath 5c7301b3f1 fix: stop viewer actions from hitting another account's message with the same id
Stalwart ids are only unique per account. When its opener named no
account, the viewer used the account of the folder open in the list,
and it resolved Trash, Junk and move targets from that folder as well.
Deleting a team message from the Unified Inbox therefore sent
`Email/set` to the user's own account and trashed an unrelated message
with the same id, and a message opened from a notification, deep link
or contact while a shared folder was open went to the shared account.

- the list names the open folder's account when it opens the viewer,
  and the viewer takes the account from its opener only, fixed at open
- Trash, Junk, Inbox and move targets come from the message's account,
  and delete and move take the message out of its own folder
- the store's delete and move take the viewed message: they only touch
  the list's row when the list shows that account, and refuse folders
  of any other account
- another account's message is marked read on the server directly
  instead of through the open folder's list
- quick reply, unsubscribe and read receipts file into the message's
  account's folders
- undo no longer slots another account's message into the open list
- Archive is only offered for mail of the open folder's account, since
  it still resolves the archive folder from there
2026-09-24 19:06:54 +02:00
Linus Rath d45c7996d2 fix: keep list previews and subjects on one line
Previews keep the body's paragraph breaks, so a line-clamped row showed
a dangling "…" line and rows came out at uneven heights, and a subject
carrying CR/LF rendered over several lines in the list and the viewer.
Fold every whitespace run to a single space before display with a new
`singleLine` helper, in the mailbox and unified inbox rows, the viewer
subject and the collapsed thread card preview.
2026-09-24 18:50:55 +02:00
Linus Rath 50418971f4 fix: stop mark-read, star, pin and tag from erasing other keywords
Every keyword change sent the message's whole `keywords` map, which the
server takes as a replacement. Opening a message that wasn't in the
loaded list sent `{"$seen": true}` and wiped its star and tags; starring
a listed message sent the list's stale map and reverted keywords another
client had changed since the list loaded.

Keyword changes now go out as `keywords/<name>` patch pointers (`true`
to set, `null` to clear) everywhere:
- mark read/unread, star, pin and tagging in the email store
- spam / not spam, trash-and-read, and the spam undo, which now puts
  back only the keywords the action touched
- star, tag and unread from the message viewer
- the offline outbox, whose keyword ops carry a patch and merge per
  keyword when coalesced

Ops queued by earlier builds still hold a whole map. They replay as a
patch that only sets the keywords in it (clearing the other half of
`$junk`/`$notjunk`), so an unread or unstar queued before the update is
lost but nothing gets erased. The list and the offline cache apply the
same patch locally instead of overwriting their keyword maps.
2026-09-24 18:44:28 +02:00
Linus Rath 5f3f584ca8 fix: auto-send read receipts only for the message on screen and clean up failed ones
With "Always send" the banner's auto-send ran in every page the viewer
pager keeps mounted, so opening one message also answered the receipt
requests of its unseen neighbours. The receipt is imported into Sent
before it is submitted, so a refused submission left a "Read: ..." copy
there, and the failure cleared the per-instance guard, so the next
render or remount imported another one.

- the pager marks the page on screen `active`, and the banner only
  auto-sends there
- an automatic receipt is tried once per message per session, keyed by
  account and id, so a failure or a remounted page does not send again
- `sendReadReceipt` destroys the imported copy when the submission
  fails, which also covers the manual "Send receipt" button
2026-09-24 18:36:15 +02:00
Linus Rath d2ed27fd3b feat: conversation view with collapsible cards, header details, auth chips, unsubscribe and read-receipt banners, attachment preview and zip, embedded-message and TNEF unwrap in the reader (#480, #830, #891) 2026-08-29 18:38:53 +02:00
Linus Rath 8206893860 feat: assess invitation trust, read the iTIP method from the ICS and pick the target calendar in the invitation banner 2026-08-29 18:22:00 +02:00
Linus Rath c8be383c05 fix: page the viewer over the list a unified-inbox message was opened from 2026-08-29 18:01:53 +02:00
Linus Rath 7f956d60f7 fix: reply to Reply-To, drop own addresses on reply-all, thread with the Message-ID and keep attachments on forward (#234, #703) 2026-08-29 17:52:34 +02:00
Linus Rath be82bfe814 feat: show shared mailboxes (group accounts) in the sidebar #37 2026-08-02 20:17:29 +02:00
arnauda-gh 8231a55acb fix(mailboxes): show error notifications when special folders are missing on JMAP server 2026-07-16 10:27:30 +02:00
Linus Rath 752aa719d8 feat: pinch zoom and pan for email body content 2026-06-12 15:26:42 +02:00
Linus Rath 5884a18979 refactor: move date/time/size meta from subject row to sender block 2026-06-11 16:33:59 +02:00
Linus Rath 5dc4ea1a45 refactor: replace three-pane pager with FlatList page-snapping pager 2026-06-11 16:24:43 +02:00
Linus Rath 41793455f7 fix: seamless email swipe transitions with skeleton loading placeholder 2026-06-11 15:11:09 +02:00
Linus Rath 1afd2185d5 feat: three-pane pager for seamless email swipe navigation 2026-06-11 14:39:30 +02:00
Linus Rath ac95a2bc39 feat: QR code sign-in 2026-06-05 19:25:00 +02:00
Linus Rath 70863e0923 feat: swipe navigation between emails and dark-mode emoji/background re-inversion 2026-06-05 16:35:06 +02:00
Linus Rath b6ecf07369 feat: customizable reader quick actions and shrink-to-fit email scaling 2026-06-05 16:21:00 +02:00
Linus Rath 5d47f47c8e feat: add Sieve filters, account security (TOTP), outbox, and EML import 2026-05-30 15:33:41 +02:00
Linus Rath d616943c25 feat: add unified inbox, scheduled send, iCal import/subscriptions, tasks, and contact tagging/import 2026-05-29 15:34:39 +02:00
Linus Rath a656a6b56a feat: enhance email composition and management features 2026-05-03 15:32:59 +02:00
Linus Rath 329c8a2f0b feat: add swipe actions for pinning and moving emails; enhance archive functionality 2026-05-02 14:27:15 +02:00
Linus Rath d5a336519a feat: add rich text editor component and enhance email thread screen with attachment handling
- Implemented a new RichTextEditor component for composing and editing rich text emails.
- Enhanced EmailThreadScreen to support downloading attachments with loading indicators.
- Added functionality to expand/collapse attachment list in EmailThreadScreen.
- Created utility functions for building initial HTML for email composition and converting HTML to plain text.
- Added tests for HTML composition and conversion utilities.
2026-04-27 17:14:22 +02:00
Linus Rath 346a71de84 feat: enhance email composition with markdown support and attachment handling
- Implement markdown formatting in the email composer, allowing users to use bold, italic, lists, and blockquotes.
- Add functionality to upload and manage attachments, including inline images.
- Introduce a new compose-format utility for converting markdown to HTML.
- Update the ComposeScreen to handle attachments and formatting actions.
- Refactor ContactDetailScreen and EmailListScreen for improved code clarity and performance.
- Add tests for markdown formatting and selection wrapping functionalities.
2026-04-27 13:55:16 +02:00
Linus Rath cf3f919fa2 feat: enhance SettingsScreen with dynamic localization and theming
- Integrated useColors hook for dynamic theming in SettingsScreen.
- Updated tab labels and titles to use localized strings.
- Refactored styles to utilize theme palette for consistent styling.
- Improved locale subscription to re-render labels on language change.

feat: implement useSheetDrag hook for bottom sheet gesture handling

- Added useSheetDrag hook to manage drag-to-dismiss functionality for bottom sheets.
- Configured pan responder for handling drag gestures and dismissing the sheet based on distance and velocity.

refactor: restructure color and token management

- Introduced useColors and useResolvedTheme hooks for better theme management.
- Separated light and dark color palettes for improved maintainability.
- Updated tokens to reflect new theming structure and ensure consistency across components.
2026-04-27 13:30:45 +02:00
Linus Rath a95783723a feat: implement keyword management and layout settings for email interactions 2026-04-27 00:16:25 +02:00
Linus Rath 7e42cb85ea feat: add EmailSourceScreen for viewing and sharing email source, update navigation and types 2026-04-26 21:38:07 +02:00
Linus Rath 373c66f2ba feat: implement navigation between emails in EmailThreadScreen with previous and next buttons 2026-04-26 20:55:29 +02:00
Linus Rath fe7d9bdf09 feat: add more actions and move functionality to EmailThreadScreen 2026-04-26 17:40:53 +02:00