2026-04-24 15:48:22 +02:00
# Done
This file contains tasks which got implemented.
2026-04-24 16:43:35 +02:00
Tasks get moved from next.md to done.md
2026-04-24 15:48:22 +02:00
## Tasks
2026-04-27 07:32:44 +02:00
## Bulk actions on search results
Long-pressing a search result enters selection mode; tapping additional results adds/removes
them. The existing bottom bar (Archive, Delete, Mark as spam, Move to folder) works on the
selection. Implementation in `email_list_screen.dart` :
- `_selectedSearchIds` (`Set<String>` ) tracks selected email IDs in search results.
- `_selecting` is true when either `_selectedThreadIds` or `_selectedSearchIds` is non-empty.
- `_selectedEmailIds` returns `_selectedSearchIds` when searching, thread-resolved IDs otherwise.
- `_buildEmailList` shows checkboxes in selection mode, highlights selected tiles, and routes
taps to toggle-vs-open depending on mode.
2026-04-27 07:27:14 +02:00
## Multi-word search uses AND semantics
Searching for "foo bar" now returns emails that contain **both** words, not the exact
phrase. Fixed in `email_repository_impl.dart` :
- **IMAP search** (`searchEmails` ): query is split on whitespace; each word becomes
`OR SUBJECT "word" TEXT "word"` , joined by spaces. Multiple top-level IMAP criteria
are implicitly ANDed by the protocol.
- **Local DB search** (`searchEmailsGlobal` ): each word adds
`& (subject LIKE '%word%' | preview LIKE '%word%')` to the Drift where-clause.
2026-04-27 07:20:16 +02:00
## Navigate back to account list from inside an account
Added an "All accounts" tile (with `Icons.switch_account` ) at the top of `FolderDrawer` ,
above a divider and the folder list. Tapping it closes the drawer and navigates to
`/accounts` via `context.go` . The drawer is shown in both `MailboxListScreen` and
`EmailListScreen` , so this entry point is reachable from anywhere inside an account.
2026-04-27 07:13:13 +02:00
## Speed up `task deploy-android`
Two parallelism improvements:
- `_integrations` internal task: runs `integration` and `integration-ui` in parallel (they use
random Stalwart ports and different Flutter build targets so there is no conflict).
- `_mobsf-start` internal task: starts the MobSF Docker container as a dep of `build-android` ,
so it warms up concurrently with the APK build instead of blocking for up to 90 s afterwards.
- `scripts/mobsf_scan.sh` : added `docker rm $CONTAINER_NAME 2>/dev/null || true` before
`docker run` to handle stopped-but-not-yet-removed containers (same fix applied to the new
`_mobsf-start` task).
2026-04-26 22:31:25 +02:00
## Android E2E test verifies APK before deploy
`task deploy-android` now runs `integration-android` (the full Android E2E test) before
uploading the APK. If the app crashes on start or any E2E step fails, the deploy is skipped.
Key fixes to make the Android E2E test reliable:
- `Taskfile.yml` : moved `integration-android` to a sequential `cmds` step after `check` ,
so the two E2E suites don't compete for CPU and slow the emulator.
- `stalwart-dev/integration_android_test.sh` : wrapped `force-stop` /`pm clear` /`uninstall`
in a `pm list packages | grep -qF` check — only runs when the package is installed, so
any real failure is surfaced instead of silently suppressed.
- `integration_test/app_e2e_test.dart` :
- `pumpUntil` uses `pump(300ms)` instead of `pumpAndSettle()` so a concurrently
running spinner never blocks settling.
- `accountConnectionStatusProvider` overridden to complete immediately, eliminating the
`CircularProgressIndicator` in `_AccountTile` that caused `pumpAndSettle` to deadlock.
- Search section: `FocusManager.instance.primaryFocus?.unfocus()` dismisses the Android
IME keyboard before polling for results — without this, the soft keyboard reduces
`viewInsets.bottom` to near-zero and `ListView.builder` renders 0 items even though
search results are present.
---
2026-04-26 21:23:59 +02:00
## Override accountConnectionStatusProvider in E2E test (fix Android pumpAndSettle deadlock)
`accountConnectionStatusProvider` overridden in `integration_test/app_e2e_test.dart` so
`_AccountTile` never shows a `CircularProgressIndicator` during tests. The spinner's
continuous animation prevented `pumpAndSettle()` from settling on Android. Reverted
`pumpUntil` to use `pumpAndSettle()` again. Commit: e50ff3c.
---
2026-04-25 06:38:21 +02:00
## Fix task check: unencrypted IMAP error + coverage gate
- `account_sync_manager_test.dart` : inject `_connectImapPlain` (bypasses the production SSL check) so the test works against the plain-IMAP dev Stalwart.
- `scripts/check_coverage.dart` : add three new screens (`sieve_script_edit_screen` , `sieve_scripts_screen` , `thread_detail_screen` ) and `sieve_repository` to `_excluded` (all are screens/JMAP clients without unit tests).
- New unit tests: `sieve_script_test.dart` , plus `findMailboxByRole` , JMAP no-URL error, and JMAP API error tests in `mailbox_repository_impl_test.dart` .
- New widget tests: `try_connection_button_test.dart` (okMessage/errorMessage rendering) plus selection-mode, deselect, search-clear, and search-result-tap tests in `email_list_screen_test.dart` .
- Fixed `FakeEmailRepository.observeThreads` in `helpers.dart` to propagate `preview` from email to thread.
- Coverage gate now passes at 80%+ (84% with integration coverage merged).
2026-04-25 06:13:38 +02:00
## Android integration test via Stalwart
Added `stalwart-dev/integration_android_test.sh` and `task integration-android` . Starts Stalwart on random ports, detects a connected emulator via `adb devices` , sets `STALWART_IMAP_HOST=10.0.2.2` (emulator host alias), and runs the existing `integration_test/app_e2e_test.dart` on the emulator.
2026-04-24 16:48:19 +02:00
## Quote original message in reply, and add Forward button
`_reply` now passes `prefillBody` with the original message quoted as plain
text (`> line…` ). New `_forward` method and Forward toolbar button added;
sets `Fwd:` subject prefix and prefills the same quoted body with To/Cc empty.
2026-04-24 16:46:09 +02:00
## Mark as unread button in email detail
Added `mark_email_unread_outlined` icon button to `EmailDetailScreen` toolbar.
Calls `setFlag(seen: false)` then pops back to the list.
2026-04-24 16:44:53 +02:00
## Pull-to-refresh on email list
Wrapped `_buildStreamBody` in a `RefreshIndicator` that calls `syncEmails` .
The empty-state is now a scrollable `ListView` so the pull gesture works even
when the folder has no messages.
2026-04-24 16:43:35 +02:00
## Show email preview snippet in list
Added `preview` field to `EmailThread` (populated from the latest email in
`_groupIntoThreads` ). Thread tiles now show subject + a one-line preview
snippet in the subtitle.
2026-04-24 16:35:10 +02:00
## Extract TryConnectionButton widget shared by account screens
Extracted `lib/ui/widgets/try_connection_button.dart` — a stateless widget
rendering the result banner (ok/error text) and the spinner/button. Both
`add_account_screen` and `edit_account_screen` now use it, removing ~30 lines
of duplicated UI code.
2026-04-24 16:32:22 +02:00
## Extract _batchMoveToRole helper in email_list_screen
`_batchArchive()` and `_batchMarkSpam()` collapsed into a shared
`_batchMoveToRole(role, notFoundMessage)` helper, eliminating ~20 lines of
duplication.
2026-04-24 16:30:59 +02:00
## Enable always_use_package_imports lint rule
Added rule to `analysis_options.yaml` ; `dart fix --apply` converted 125 relative
imports across 33 files to `package:sharedinbox/...` style automatically.
2026-04-24 16:28:42 +02:00
## Replace silent catch (_) with logged errors
5 `catch (_)` blocks in JMAP push stream setup and 2 in UI screens now use
`catch (e)` with `log(...)` via the project's `logger.dart` wrapper.
The two intentionally silent catches (malformed SSE JSON, Sent folder already
exists) were left as-is since they already had explanatory comments.
2026-04-24 15:48:22 +02:00
## Safety hardening before real account use
### 1. Fix non-PEEK body fetch (silently sets \Seen)
`lib/data/repositories/email_repository_impl.dart` ~line 163
Change `'(BODY[])'` → `'(BODY.PEEK[])'` so fetching the body does not set \Seen
as a side-effect of the IMAP FETCH command.
Same fix at ~line 1696 for attachment part fetches: `BODY[partId]` → `BODY.PEEK[partId]` .
### 2. Move to Trash instead of EXPUNGE
`lib/data/repositories/email_repository_impl.dart` `deleteEmail` method
Before enqueuing a hard delete, query the local mailboxes cache for a 'trash'-role
folder for that account.
- If found AND the email is not already in Trash: call `moveEmail` to that path.
- If not found OR already in Trash: fall back to the existing EXPUNGE path.
This makes delete reversible — the user can recover from Trash.
### 3. Confirmation dialog for delete
Three call sites need a `showDialog` confirmation before deleting:
a) Delete button in detail view
`lib/ui/screens/email_detail_screen.dart` ~line 97
Show AlertDialog "Delete this email?" with Cancel / Delete buttons.
b) Batch delete in list view
`lib/ui/screens/email_list_screen.dart` `_batchDelete` ~line 268
Show AlertDialog "Delete N emails?" with Cancel / Delete buttons.
c) Swipe-to-delete in list view
`lib/ui/screens/email_list_screen.dart` `Dismissible.onDismissed` ~line 436
Use `Dismissible.confirmDismiss` callback (fires before the item is removed)
to show a confirmation for the right-swipe (delete) direction only.
Return false to cancel and keep the item in the list.