Commit Graph
315 Commits
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 ae239c7758 feat(ux): re-evaluate search and clear it after batch-delete leaves no results (#85)
After deleting all selected emails from a search view, re-run the
search query. If no emails match any more, clear the search bar so
the user returns to the normal thread list view instead of seeing
a stale list of already-deleted messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:22:49 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 1af4fa8cf9 feat(ci): fail early when mock files are out of date (#87)
Add check-mocks task that re-runs build_runner and fails if any
*.mocks.dart file differs from what is committed. Wired into
check-fast (pre-commit) and added as an early CI step so stale
mocks are caught before the full test suite runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:19:28 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 d9e6500cec chore(ci): remove build-windows job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:08:29 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4e6c3d73fe chore: regenerate mocks for fetchRawRfc822
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:40:55 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 0ccf7b51fa fix: fetch original RFC822 from server in Show Raw Email (#84)
Instead of reconstructing the message from the local DB, fetch the
original bytes live from IMAP (BODY.PEEK[]) or JMAP (Email/get blobId
→ downloadBlob) so the view shows the true unmodified message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:27:12 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 62c2c55621 feat: add Show Raw Email option with copy and download (#84)
Adds a new popup menu item below "Show Mail Headers" that displays the
email in RFC-style ASCII format (headers + plain-text body), with a
Copy-to-clipboard button and a Download button that saves a .eml file
and opens it via the system file handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:55:35 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f96f9216cd feat: replace flutter_html with SecureEmailWebView (#21)
Swap the flutter_html renderer for a webview_flutter-based widget that
enforces strict security by default: scripts blocked via CSP
(script-src 'none'), remote images opt-in, and every link click routed
through a confirmation dialog that bolds the registered domain for
phishing detection.  Links open in the system browser via url_launcher.

On Linux (no webview_flutter platform support) the widget falls back to
plain text extracted via the existing htmlToPlain() utility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:18:42 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 902c0a7900 feat(ci): add windows-nightly workflow
Builds and deploys Windows once a day (02:00 UTC) instead of on every
push to main. Skips the build if no commits landed on main in the last
24 hours. Kept disabled (if: false) until a windows-runner is
registered.

Closes #77

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 07:32:36 +02:00
Thomas SharedInbox fd00092b17 ci: re-trigger after runner restart 2026-05-15 07:31:29 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 5e6d770cb5 fix(ci): skip build-windows until windows-runner is registered
timeout-minutes doesn't start until a runner accepts the job, so the
job would queue indefinitely. Disable with if: false for now — change
back to github.ref == 'refs/heads/main' once a windows-runner runner
is set up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:34:10 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ebeb6148b6 fix(ci): reduce build-windows timeout to 5m until runner is registered
60-minute wait blocks every run. 5 minutes lets it fail fast with
continue-on-error, leaving the rest of the workflow unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:24:09 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 dd1425a497 fix(test): fix E2E retry — set -e broke exit-code capture
With set -Eeuo pipefail, a failing fvm flutter test exited the script
before _e2e_exit=$? could run, so the retry-on-new-display logic never
fired. Use the cmd || var=$? pattern to capture the exit code safely,
and add || true to the break guard so set -e doesn't trip on it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:10:46 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 1b7cbdbb4b fix(ci): mark build-windows as continue-on-error with 60m timeout
The windows-runner self-hosted runner doesn't exist yet, so the job
would block the run indefinitely. With continue-on-error + timeout it
fails gracefully once a runner is registered and picks up the job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:05:03 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 00c4de8447 feat(windows): add Windows release build, deploy, and in-app update banner
Adds build-windows-release and deploy-windows-to-server Taskfile tasks,
a build-windows CI job (requires a windows-runner self-hosted runner),
and extends updateInfoProvider to also cover Platform.isWindows.
latest.json is now extended with a 'windows' key; both deploy tasks
preserve the other platform's URL when updating the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:56:01 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 724df4ea37 feat(linux): package Linux release, deploy to server, add in-app update banner
Build task embeds GIT_HASH via --dart-define; new deploy-linux-to-server task
packages a tar.gz and updates latest.json on the server. The account list screen
shows a MaterialBanner when a newer Linux build is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:46:29 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 99c3a1d808 feat(compose): sort address autocomplete by most recently used
Add ORDER BY receivedAt DESC to the searchAddresses query so the first
unique occurrence of each address comes from the newest email. Contacts
from recent conversations float to the top of the suggestions list.

Add a unit test verifying the sort order.

Fixes #83

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:39:38 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 05abc121df fix(test): restore _zOrderIndex filter — Flutter 3.41.6 bug
_RawAutocompleteState.dispose() removes _updateOptionsViewVisibility
from the external FocusNode but forgets to remove _onFocusChange. When
the state is recreated with the same FocusNode both listeners accumulate,
and the second hide() call hits the _zOrderIndex != null assertion in
overlay.dart:1681. This is a Flutter framework bug, not a test deficiency.

Restore the filter with a comment pointing to the root cause so it can
be removed when we upgrade past the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:32:05 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 2795cfe2cc fix(compose): prevent double hide() in RawAutocomplete async optionsBuilder
When focus leaves the To field while the address DB query is in flight,
the optionsBuilder Future completes AFTER RawAutocomplete has already
called hide() on the overlay. The completion triggers a second hide()
call, hitting the _zOrderIndex != null assertion in overlay.dart.

Fix: check focusNode.hasFocus after the await; return [] if focus left,
which prevents RawAutocomplete from calling show()/hide() on a closed
overlay.

Also fixes #81 partially: after undo(), push an inverse UndoAction so
the undo log retains a record and the user can re-apply the operation.

Fixes #79

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:22:20 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4902d82dd6 fix(website): fix /builds/ 404 and show commit datetime in build history
The /builds/ page returned 404 because website/content/builds/ was fully
gitignored — Hugo had no content to generate the section landing page.

Fix:
- Narrow .gitignore to only ignore year-subdirectories (YYYY/) so that
  _index.md can be committed as a static fallback.
- Add website/content/builds/_index.md with section description.
- Enhance generate_build_history.py to fetch and display commit datetime
  alongside title, and render _index.md as a flat list of all builds
  (newest-day first) so the section landing page is useful immediately.

Fixes #82

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:14:50 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 81f9332fb4 fix(test): unfocus To field before Subject to prevent double hide() race
A plain pump() between enterText(To) and enterText(Subject) does not
prevent the _zOrderIndex assertion: hide() is called twice synchronously
during the focus-dispatch triggered by the second enterText().

Fix: explicitly call primaryFocus?.unfocus() after the To field, then
pump(300ms) so RawAutocomplete's OverlayPortal closes via a single
FocusNode notification. By the time Subject takes focus the overlay is
already hidden — no second hide() fires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:13:10 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 9ed85e1c51 fix(test): fix _zOrderIndex race by syncing focus before field/screen transitions
RawAutocomplete's OverlayPortalController.hide() was called twice:
once when focus left the To field and again when ComposeScreen was popped,
triggering the _zOrderIndex assertion in overlay.dart.

Fix by:
1. pump() after entering the To field so the overlay has a frame to close
   before the Subject field takes focus.
2. unfocus() + pump() before tapping Send so the overlay is already hidden
   when the screen pops, preventing a second hide() on unmount.

Remove the _zOrderIndex string-filter from FlutterError.onError — the
root cause is fixed rather than suppressed.

Fixes #79

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:06:57 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ebff60a4d4 fix(ui): guard ref.read with mounted checks in _delete after async gaps
After showDialog and after the two repo awaits (getEmail/deleteEmail),
the widget may have been disposed — calling ref.read on a disposed
ConsumerStatefulElement throws "Cannot use 'ref' after the widget was
disposed." Add if (!mounted) return; at both points.

Fixes #80

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:04:11 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 7aa9ddbe07 fix(ci): make SSH deploy steps continue-on-error
SSH secrets (SSH_USER, SSH_HOST, SSH_PRIVATE_KEY) are not yet configured
as repository secrets. Mark the four SSH-dependent steps continue-on-error
so the Play Store deploy job succeeds while those secrets are pending.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:02:47 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 251e9d051e fix(ci): pass ANDROID_KEYSTORE_PASSWORD to deploy-apk-to-server step
The deploy-apk-to-server task depends on build-android which signs the
APK — it needs the keystore password or the packageRelease Gradle task
fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:52:49 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 c3b814db54 fix(test): restore ErrorWidget.builder immediately after app.main()
_verifyErrorWidgetBuilderUnset is called from _runTestBody after testBody()
returns, but addTearDown callbacks run after _runTestBody — so teardown is
too late for this check. Restore ErrorWidget.builder inline, right after
app.main() sets it, so the binding sees the original value when it verifies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:39:41 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 97cf35a10a fix(test): restore ErrorWidget.builder in E2E teardown
app.main() also sets ErrorWidget.builder to its CrashScreen handler.
The test binding's _verifyErrorWidgetBuilderUnset check fires when
ErrorWidget.builder != its pre-test value after the test completes.
Save and restore ErrorWidget.builder alongside FlutterError.onError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:32:48 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4e5b523ccc fix(test): filter _zOrderIndex overlay assertion in E2E error handler
OverlayPortalController.hide() asserts _zOrderIndex != null before
clearing it. In headless tests without navigation animations, rapid
screen dismissal can trigger hide() twice (once on focus loss, once on
widget unmount) — a Flutter framework race that overlay.dart itself
notes should not happen during rebuilds. Filter it alongside the
existing DEFUNCT/DISPOSED suppressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:26:53 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a4cbe35b0f fix(test): override FlutterError.onError after app.main() to fix E2E hang
app.main() synchronously sets FlutterError.onError to its crash-screen
handler, overwriting the filter the test had registered first. The test
binding's _runTest finally-block checks FlutterError.onError != _recordError
and fires assertion '_pendingExceptionDetails != null', which prevents the
integration test framework from calling exit() — causing the process to hang
for the full 360-second timeout.

Fix: capture the binding's error recorder (bindingError) before app.main(),
call app.main() first, then install the DEFUNCT/DISPOSED filter pointing at
bindingError, and restore to bindingError in teardown. This keeps the crash
handler from interfering with the test binding's error tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:19:11 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 cc108b4788 fix(sync): cancel backoff/idle timers on stop to prevent process hang
Future.any([Future.delayed(N), stopSignal.future]) left unfired Timers
alive after stop() fired the signal — pending Timers kept the Dart event
loop running and prevented the process from exiting, causing the E2E
integration test to time out (exit 124) instead of exiting cleanly.

Replace all four occurrences with an explicit Timer that completes the
stop-signal and is cancelled in a finally block, so the Dart isolate can
exit as soon as the sync loops are stopped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:03:26 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4b83d3e456 feat(cd): continuous delivery — scp APK to server and build Hugo history
- scripts/generate_build_history.py: SSH into server, list APKs under
  public_html/builds/YYYY/MM/DD/, fetch commit titles from Codeberg API,
  and write Hugo content pages to website/content/builds/
- Taskfile: add deploy-apk-to-server and generate-build-history tasks;
  add --exclude='*.apk' to website-deploy rsync so APKs survive redeploy
- CI: after Play Store deploy, set up SSH key, scp APK, generate history,
  then deploy website
- .gitignore: exclude website/content/builds/ (generated at deploy time)
- website/hugo.toml: add Builds nav item

Closes #73

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:46:56 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a29d0e93b4 feat(sync): add View log button to sync error banner
When a sync failure banner appears in the email list screen, a new
'View log' button navigates directly to the account's sync log screen
so the user can see the full error details.

Also creates issue #75 for the first-snooze-in-new-account failure.

Closes #13

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:35:59 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 02b0fec0b6 feat(compose): autocomplete To/Cc from local address history
Adds RawAutocomplete<EmailAddress> to the To and Cc fields in the
compose screen. As the user types (minimum 2 chars), suggestions are
fetched from the local DB by searching from/to/cc columns of cached
emails. Selecting a suggestion appends it to any existing addresses
already in the field (comma-separated).

New repository method searchAddresses() returns deduplicated
EmailAddress objects matching the query string.

Closes #11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:30:17 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 032595d7d5 feat(hooks): add pre-commit check for binary file additions
Blocks accidental commits of build artifacts, databases, and compiled
binaries. Image and font formats (png, jpg, svg, ttf, woff, etc.) are
allowed. Uses git diff --numstat binary detection (-  -  path).

Closes #4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:12:52 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 26a9a5e6f3 feat(crash): add app version and device info to crash reports
Issue reports now include:
- App version (from package_info_plus)
- OS name and version (non-personal, from dart:io Platform)
- Error and stack trace wrapped in triple-backtick code blocks
  so Codeberg renders them as preformatted text

Closes #59

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:52:40 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 6f030213a7 fix(ci): fetch last 50 commits so changelog has all entries
actions/checkout defaults to fetch-depth: 1 (shallow clone).
generate-changelog runs git log -n 50, so only one entry appeared
in the built app. Fetching 50 commits gives a complete changelog.

Closes #64

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:45:21 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 9686785abf docs(issue-template): remove auto-assigned State/Ready label
The user should set State/Ready manually when the issue is ready
to be worked on, not automatically on creation.

Closes #74

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:43:43 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 47bdf3ec35 fix(ci): reduce verbose output in CI jobs
- Add --no-warn-dirty to all nix develop calls to suppress Git dirty-tree warnings
- Switch integration test reporter from expanded to compact (per-test names suppressed on success)
- Show only summary line on integration test success, matching unit/widget test behavior

Closes #8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:42:37 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 d932f59f25 fix(ui): show all SnackBars for 5 seconds instead of Flutter default 4s
Closes #17

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:37:06 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 2985198d9c fix(repo): guard moveEmail/setFlag/deleteEmail against missing rows
getSingle() throws 'Bad state: No element' when the email row is gone
(race condition in batch operations or already deleted). Switch to
getSingleOrNull() and return early so batch moves/flags/deletes on
stale IDs fail silently instead of crashing.

Closes #58

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:32:25 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ca28bd01af fix(imap): fetch full message for attachment download to fix base64 decoding
A partial BODY.PEEK[n] fetch omits the section's MIME headers, so
enough_mail's decodeContentBinary() has no Content-Transfer-Encoding
and returns the raw base64 string instead of the decoded bytes.
Fetching BODY.PEEK[] gives enough_mail the full MIME structure and
getPart(fetchPartId) correctly decodes the attachment.

Also adds an integration test that creates an email with a binary
attachment, syncs it, and asserts the downloaded bytes match the
original — this test failed before the fix.

Closes #70

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:44:09 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 3802ca57ed fix(ui): hide AppBar back button on Android/iOS
Mobile platforms provide OS-level back navigation (swipe gesture),
so the redundant AppBar back button only clutters the toolbar.
Desktop keeps it since there is no system back gesture.

Closes #69

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:29:25 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 53757595c4 revert(android): restore epoch seconds for build-number
The (YY-20)mmddHHMM formula generates ~605M for 2026, which is lower
than existing epoch-second deployments (~1.747B). Google Play rejects
version code regressions at commit time (403 Forbidden).

Blocked — see issue #63 for context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:00:02 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 71a88358f5 docs: add issue template with label workflow reminder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:53:26 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f0c0eeb9a4 docs(agents): add explicit fgj commands for issue label transitions
Makes the InProgress-first rule harder to skip by including the exact
command to run, so there is no ambiguity about how or when to do it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:49:00 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 28b86ec1be fix(android): use human-readable build number (YY-20)mmddHHMM
Replaces epoch seconds with a compact date-based integer so the Play
Store version code is interpretable by humans while staying below the
2 100 000 000 upper bound until ~2040.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:26:43 +02:00
Thomas SharedInbox caa238303e docs 2026-05-14 18:24:07 +02:00
Thomas SharedInbox c0d6699f92 AGENTS: issue workflow 2026-05-14 18:14:19 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 c46de2abb8 fix(android): use epoch seconds for versionCode, not yymmddhhmm
date +%y%m%d%H%M for 2026-05-14 17:17 = 2605141717 which exceeds
Android's 2100000000 versionCode cap, aborting the build.
Epoch seconds (~1.75B today) stay under the cap and remain unique.
Human-readable build-name (yymmddhhmm) is unchanged for issue #63.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:19:50 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 3dbd8f53be fix(ci): move Play Store deploy into ci.yml; drop release.yml
workflow_run is not supported by Forgejo Actions — release.yml never
fired after CI passed.  Port the deploy-playstore job into ci.yml with
needs: check + if: main, matching the pattern already used by build-linux.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:11:00 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f7e75cd5b6 fix(test): update E2E test for onboarding screen + increase timeouts
The U7 onboarding view replaced "No accounts yet." with "Welcome to
SharedInbox", causing the E2E test to spin for the full timeout budget
(pumping slowly in headless CI) before failing. Fix the finder and
bump per-attempt timeout from 240s → 360s and CI job ceiling from
20 min → 30 min to give the full account-add → send → verify flow
room to complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:56:53 +02:00