7.5 KiB
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:
EmailThreadList({
required Stream<List<EmailThread>> threads, // folder + combined inbox
// or:
required List<EmailThread> 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<EmailBatchAction> actions = EmailBatchAction.standard,
ValueChanged<EmailThread>? 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:
_selectedThreadIdsplus toggle/clear/select-all helpers._currentThreads(last stream emission)._limitpagination withLoad more.- Selection-mode
AppBarandBottomAppBarrendering — 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:
enum EmailBatchAction { archive, delete, markSpam, move, snooze }
Future<void> batchMoveToRole(WidgetRef ref, BuildContext ctx, {
required List<EmailThread> threads,
required String role,
required String dialogTitle,
required String createFolderName,
});
Future<void> batchDelete(WidgetRef ref, {required List<EmailThread> threads});
Future<void> batchMove(WidgetRef ref, BuildContext ctx, {required List<EmailThread> threads});
Future<void> batchSnooze(WidgetRef ref, BuildContext ctx, {required List<EmailThread> threads});
Future<void> 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 withEmailThreadList(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_buildStreamBodyand_buildEmailListwithEmailThreadList. Drop selection state,_toggleThreadSelection,_selectionBottomBar,_batch*methods,_onSwipeDismissed. The search path inside this screen (results fromsearchEmails) becomesEmailThreadList(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 usesEmailThreadList(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 inlineListView.builderwithEmailThreadList, gaining selection/swipe/batch parity.
Migration steps
- Add
EmailThreadListwidget with selection, swipe, pagination, and AppBar/BottomBar builders. Lift the existing logic verbatim fromEmailListScreenso behaviour is unchanged. - Promote the five batch ops + swipe handler into
email_action_helpers.dart; switchEmailListScreento call them. Keep tile tests passing. - Fold
showAccountandDismissible-out intoThreadTile; deleteEmailThreadTile; update imports. - Migrate
CombinedInboxScreentoEmailThreadList. Combined inbox now supports spam/move/snooze (was missing). Verify multi-account batches still group correctly. - Migrate the search-result branch inside
EmailListScreen, then the Messages section ofSearchScreen, thenAddressEmailsScreen. - Run
flutter analyzeand the integration tests underintegration_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
mailboxPathper selection — confirmed handled by grouping selected threads byaccountIdthen 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
SearchScreenandAddressEmailsScreen. Plan keepsenableSwipe: falsefor those to avoid disorienting users when a swiped item disappears from a filtered list; revisit if user wants parity.