Files
sharedinbox/ISSUE-533.md

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:

  • _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:

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 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.