Persists the font family/size from a sent formatted message to the
settings DataStore (SettingsRepository.setLastFont), taking the
message-wide base style if set, else the last FontFamily/FontSize
span. Brand-new compositions (draftId == null) seed a RichBaseStyle
from the remembered preference so the whole message defaults to it;
resumed drafts and replies/forwards are untouched, and messages with
no remembered font stay plaintext-only.
Closes#78
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inserts a new LicenseScreen ahead of OnboardingWelcomeScreen as the
onboarding graph's start destination: the user must scroll the full
GPL-3.0 text and tap Agree before reaching anything else, or Decline
to exit the app outright. Acceptance is persisted
(SettingsRepository.licenseAccepted) so a user who agrees but exits
before adding an account isn't asked again, and the
NotificationPermissionEffect() request (#151) stays scoped to
OnboardingWelcomeScreen so it never fires on the license screen.
Closes#172
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MailMaintenanceGate serializes only the backfill↔prune pair. The other two
pairs — sync↔backfill and sync↔prune — are deliberately ungated (foreground
sync uses its own syncMutex to stay UI-responsive) and rely on a disjoint-by-UID
argument for safety, with no test covering it (issue #53).
Add MailSyncConcurrencyTest: a real MailSyncer and a real MailBackfiller/
MailPruner wired to one shared in-memory message store, with CompletableDeferred
gates (mirroring MailMaintenanceGateTest) that park one actor mid-critical-
section while the other's whole critical section runs. Each interleaving is
driven to the boundary (window edge / count floor) where an overlap would
surface as a lost, duplicated, or wrongly-deleted row:
- sync's windowed reconcile runs while a backfill is parked mid-paging just
below the window (tightest edge: lowestSyncedUid == minWindowUid);
- a backfill's below-window page lands after a concurrent full sync of the
window (stale-boundary ordering);
- a count-retention prune runs while a foreground sync is parked in its fetch;
- a full foreground sync runs while a prune is parked inside its critical
section, before it touches the message table.
All four pass: the disjointness invariant holds unlocked. No production code
changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds MailProvider.AOL as a guided app-password provider (after iCloud, before
Other), closing the coupled pair of #156 (app-password help content) and #154
(IMAP/SMTP presets):
- appPasswordHelpUrl points at AOL's "Create and manage 3rd-party app
passwords" article. No twoFactorHelpUrl: verified against both AOL's
app-password article and its separate two-step-verification article that
neither treats 2FA as a prerequisite for generating an app password (AOL
mirrors Yahoo here, not Gmail/iCloud).
- IMAP imap.aol.com:993 (SSL/TLS); SMTP smtp.aol.com:465 (SSL/TLS) — AOL's
official docs and the Thunderbird ISPDB autoconfig only document implicit
TLS on 465 for submission, with no STARTTLS/587 alternative, so this
follows Yahoo's rationale rather than Gmail/iCloud's STARTTLS preset.
AppPasswordSetupScreen's two exhaustive `when` blocks (providerIntro,
twoFactorHelpLabel) gain an AOL branch; AccountPickerScreen already lists
providers generically via MailProvider.entries. Test coverage mirrors the
Yahoo/iCloud assertions: presets, help URLs, no twoFactorHelpUrl, fromKey,
forImapHost, and brandFor resolving a manually-configured imap.aol.com
account to the AOL brand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the previously-unused ColorSwatchRow into the compose FormattingToolbar
with two new controls: a font-color button applying RichStyle.FontColor and a
highlight button applying RichStyle.Highlight over the selection. Each opens a
ColorPickerDialog built on the shared ColorSwatchRow (~8 font colors; yellow /
green / cyan / pink highlighter markers), with a "no color"/"none" entry that
clears the style outright via a new clearStyle op. Buttons reflect the current
selection's color and carry accessible onClickLabels; swatches carry their own
contentDescriptions.
Closes#74Closes#75
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #148: opening an already-cached message was still slow. Three fixes
on the cached-open critical path (issue #186).
Fix 1 - WebView renders once. The reader resolved cid: inline images AFTER the
first render, so the AndroidView update key (which included inlineImages.keys)
changed and reloaded the whole document a second time for any inline-image email.
ReaderViewModel now resolves inline images and folds them into the SAME state
update as the body, and HtmlBody drops inline images from the reload key, so the
WebView loads exactly once and a late inline-image change never reloads. The
WebView is also destroyed onRelease so it (and its Context) is not leaked.
Pool/pre-warm is left as a TODO (leak-prone; single-render is the dominant win).
Fix 2 - openMessage does no wasted work for a cached, already-read message. Added
a body-less MessageRouting projection (mirrors MessageSummary, no migration);
the routing/flag callers (openMessage's first read, downloadAttachment, setStarred,
deleteMessage, expunge, moveByRole/moveToFolder, buildReplyDraft, prefetchMessage)
route on it, and getById (SELECT *) is reserved for the single read that returns
the body. imapParamsFor (Keystore decrypt + DataStore read) is resolved lazily,
only in the fetch / SEEN-push branches; the cached+read path also skips the
account lookup. De-duped the inlineImages attachment N+1 via a shared
ensureAttachmentFile helper that takes the already-resolved account/folder.
Fix 3 - repository IO off the main thread. openMessage, inlineImages,
downloadedAttachmentParts, and downloadAttachment now run in
withContext(Dispatchers.IO), so their DB/file/crypto work no longer runs on the
Main.immediate viewModelScope during the open animation.
Reader behavior (content, read/SEEN semantics) is unchanged; does not touch the
async SEEN network push handled separately by #170.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yahoo's guided app-password setup pointed appPasswordHelpUrl at the
generic account-security sign-in page, which assumes the user already
knows to hunt for "Create app password" once there. Point it instead at
Yahoo's own step-by-step "Generate and manage 3rd-party app passwords"
article, mirroring what #153 did for iCloud.
Yahoo does NOT gate app-password creation behind two-step verification
(verified against Yahoo's live help docs, which never list it as a
prerequisite), so — unlike Gmail and iCloud — it keeps twoFactorHelpUrl
null and shows no 2FA button. AppPasswordSetupScreen already renders the
help buttons generically from these provider fields, so no screen change
is needed; the existing PR #152 ordering (2FA link before the
app-password link) is preserved for the providers that have both.
Add a MailProviderTest assertion pinning Yahoo's new app-password URL
(mirroring the iCloud test) and extend the onboarding
yahooSetup_hasNoTwoFactorHelpLink coverage note for #155.
Closes#155
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rich-text engine already fully supported RichStyle.Strikethrough
(HTML serialization, parsing, and rendering); only the toolbar control
was missing. Adds an "S" FormatButton next to Bold/Italic/Underline,
wired the same way (onToggleStyle + isStyled toggle state), plus the
format_strikethrough string resource for its onClickLabel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds friction to the "Report a Problem" form: a required email field (basic
local-part@domain.tld validation), a required consent notice about being
contacted at that address, and a 200-character minimum on the comment field
with a live "x/200" counter that turns red (with the field outline) until the
threshold is met. Submit stays disabled until both the comment and email are
valid, mirroring and extending the existing SUBMITTING gate. The email rides
along on DebugReport (userEmail) so it round-trips through the storage JSON
and the exact payload that's previewed, copied, saved, and POSTed. The new
ViewModel-level guard on submit() also fully integrates with the #161
success-confirmation dialog: invalid attempts never reach SUBMITTING/SUCCEEDED,
so the dialog flow is unaffected.
Closes#159
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MainActivity.onCreate only parsed the incoming intent (pendingCompose /
pendingOpenMessageId) when savedInstanceState == null, on the assumption
that a non-null value always means a config-change recreation (e.g.
rotation), where Android redelivers the same, already-handled intent and
re-parsing would just navigate to a duplicate destination.
But Android also passes a restored, non-null savedInstanceState when it
recreates the activity after the process was killed in the background and
is then relaunched by tapping a notification. There, intent is the new
tap, not a replay, but the guard swallowed it exactly like a rotation, so
pendingOpenMessageId was never set and the tap silently landed wherever
the restored back stack was (typically the mailbox) instead of the
message. That's the #157 regression from the original fix in a6ec00d
(#56). pendingCompose (mailto:/share intents) went through the identical
guard and had the same latent bug.
Replace the savedInstanceState check with IntentHandledMarker, which
marks the Intent instance itself once parsed. A config-change recreation
redelivers that same marked instance, so it's correctly skipped; a
genuinely new intent -- warm via onNewIntent or cold via onCreate after a
process-death relaunch -- is never marked yet, so it's always parsed.
This dedupes on the intent's own identity instead of an unreliable proxy
for it, so it can't confuse the two recreation paths.
Adds IntentHandledMarkerTest covering the marking contract: unhandled on
first look, recognized as handled on a redelivered instance, and still
unhandled on a freshly constructed (but content-equal) instance -- the
process-death case.
Closes#157
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>