From fc5954ab1a6c62cd454829d031df788aab2a1bad Mon Sep 17 00:00:00 2001 From: agentloop Date: Mon, 8 Jun 2026 04:55:55 +0000 Subject: [PATCH] plan: refresh plan for issue #533 --- ISSUE-533.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ISSUE-533.md diff --git a/ISSUE-533.md b/ISSUE-533.md new file mode 100644 index 0000000..0073684 --- /dev/null +++ b/ISSUE-533.md @@ -0,0 +1,101 @@ +# Plan: Consolidate Email-List UI + +## Goal + +Three list surfaces — folder view (`EmailListScreen`), combined inbox (`CombinedInboxScreen`), and search results (in `SearchScreen` plus the in-screen search in `EmailListScreen`, plus `AddressEmailsScreen`) — currently duplicate selection state, swipe-dismiss handling, batch actions (archive/delete/spam/move/snooze), and tile rendering. Unify into one widget so behaviour and UI are identical everywhere. Thread detail view is intentionally out of scope. + +## Current duplication + +| Concern | `EmailListScreen` | `CombinedInboxScreen` | `SearchScreen` / `AddressEmailsScreen` | +| --- | --- | --- | --- | +| Tile widget | `EmailThreadTile` + `ThreadTile` (search) | `EmailThreadTile` | `ThreadTile` / inline `ListTile` | +| Selection set | thread + per-email | thread only | none | +| Selection AppBar/BottomBar | full set of 5 actions | archive + delete only | n/a | +| Swipe dismiss | archive/delete + undo | archive/delete + undo (copy) | none | +| Batch actions | archive, delete, spam, move, snooze | archive, delete (re-implemented) | none | + +Tile widgets `lib/ui/widgets/email_thread_tile.dart` and `lib/ui/widgets/thread_tile.dart` render the same fields in slightly different layouts. + +## Target architecture + +### 1. New widget `lib/ui/widgets/email_thread_list.dart` + +A self-contained `ConsumerStatefulWidget` that owns selection state and renders the list. API: + +```dart +EmailThreadList({ + required Stream> threads, // folder + combined inbox + // or: + required List staticThreads, // search / address results + required EmailListContext listContext, // see below + bool showAccountLabel = false, // combined inbox + bool showLocationLabel = false, // search / cross-mailbox + bool enableSwipe = true, + bool enablePagination = true, + List actions = EmailBatchAction.standard, + ValueChanged? onTap, // null → default navigation +}) +``` + +`EmailListContext` carries `accountId?` (nullable for combined/global views) and `mailboxPath?` (nullable for combined/global views). Batch actions read these to scope role lookups and undo-action source paths; when null they fall back to per-thread `t.accountId` / `t.mailboxPath` (this is how `CombinedInboxScreen._batchArchive` already groups by account). + +Encapsulated: +- `_selectedThreadIds` plus toggle/clear/select-all helpers. +- `_currentThreads` (last stream emission). +- `_limit` pagination with `Load more`. +- Selection-mode `AppBar` and `BottomAppBar` rendering — driven by the host scaffold via two builders the widget exposes (`buildSelectionAppBar`, `buildSelectionBottomBar`) so the host keeps Scaffold ownership but doesn't reimplement them. +- Swipe-to-archive / swipe-to-delete + undo push. + +### 2. Shared action layer `lib/ui/screens/email_action_helpers.dart` + +Extend the existing file with the batch ops currently duplicated: + +```dart +enum EmailBatchAction { archive, delete, markSpam, move, snooze } + +Future batchMoveToRole(WidgetRef ref, BuildContext ctx, { + required List threads, + required String role, + required String dialogTitle, + required String createFolderName, +}); + +Future batchDelete(WidgetRef ref, {required List threads}); +Future batchMove(WidgetRef ref, BuildContext ctx, {required List threads}); +Future batchSnooze(WidgetRef ref, BuildContext ctx, {required List threads}); +Future swipeDismiss(WidgetRef ref, EmailThread thread, DismissDirection dir); +``` + +Each function fetches `originalEmails`, runs the repo calls, and pushes a single `UndoAction`. Grouping by `accountId` lives here so combined-inbox-style multi-account selections work for every action (not only archive/delete). `_batchMoveToRole` from `EmailListScreen` and `_batchArchive` from `CombinedInboxScreen` collapse into one function. + +### 3. Unify the tile widgets + +Keep `ThreadTile` (`lib/ui/widgets/thread_tile.dart`) as the single tile. Move the `Dismissible` wrapper out — `EmailThreadList` owns swipe — and add the optional `showAccount` subtitle currently in `EmailThreadTile`. Delete `lib/ui/widgets/email_thread_tile.dart`. + +## Screen refactors + +- `combined_inbox_screen.dart` — drop selection state, swipe handler, batch methods, `_buildThreadList`, `_selectionBottomBar`. Replace body with `EmailThreadList(stream: emailRepo.observeAllInboxThreads(...), listContext: const EmailListContext.allAccounts(), showAccountLabel: true)`. AppBar/drawer/FAB stay. +- `email_list_screen.dart` — keep search-bar, sync banner, folder drawer, `Mark all as read`. Replace `_buildStreamBody` and `_buildEmailList` with `EmailThreadList`. Drop selection state, `_toggleThreadSelection`, `_selectionBottomBar`, `_batch*` methods, `_onSwipeDismissed`. The search path inside this screen (results from `searchEmails`) becomes `EmailThreadList(staticThreads: results.map(EmailThread.fromEmail).toList(), enableSwipe: false)` — the per-email vs per-thread split goes away once everything is treated as a thread of one. +- `search_screen.dart` — Messages section uses `EmailThreadList(staticThreads: ..., enableSwipe: false, showLocationLabel: true, actions: EmailBatchAction.standard)`, so global search results now support the same selection + batch actions. Folders and Addresses sections unchanged. +- `address_emails_screen.dart` — replace inline `ListView.builder` with `EmailThreadList`, gaining selection/swipe/batch parity. + +## Migration steps + +1. Add `EmailThreadList` widget with selection, swipe, pagination, and AppBar/BottomBar builders. Lift the existing logic verbatim from `EmailListScreen` so behaviour is unchanged. +2. Promote the five batch ops + swipe handler into `email_action_helpers.dart`; switch `EmailListScreen` to call them. Keep tile tests passing. +3. Fold `showAccount` and `Dismissible`-out into `ThreadTile`; delete `EmailThreadTile`; update imports. +4. Migrate `CombinedInboxScreen` to `EmailThreadList`. Combined inbox now supports spam/move/snooze (was missing). Verify multi-account batches still group correctly. +5. Migrate the search-result branch inside `EmailListScreen`, then the Messages section of `SearchScreen`, then `AddressEmailsScreen`. +6. Run `flutter analyze` and the integration tests under `integration_test/` (folder, combined inbox, and search exercise the same code path now, so a single regression test set covers all surfaces). + +## Out of scope + +- `ThreadDetailScreen` — single-thread message list, intentionally different. +- Repository / DB code in `lib/core/repositories/` — no changes; the unification is purely on the UI layer. +- Folder drawer, sync banner, search bar — remain owned by their hosting screens. + +## Risks / open questions + +- Combined inbox currently has no `mailboxPath` per selection — confirmed handled by grouping selected threads by `accountId` then looking up archive/junk/etc. per group. The same grouping must work for the move/snooze sheet (the destination picker needs an account; when multiple accounts are selected, prompt per-account or block — recommend block with a SnackBar to mirror existing per-folder constraints; flag for user feedback). +- Snooze for cross-account selection: same constraint as above — implementation should iterate accounts. +- Swipe-dismiss in search results: currently disabled in `SearchScreen` and `AddressEmailsScreen`. Plan keeps `enableSwipe: false` for those to avoid disorienting users when a swiped item disappears from a filtered list; revisit if user wants parity.