diff --git a/ISSUE-474.md b/ISSUE-474.md new file mode 100644 index 0000000..12b9a8b --- /dev/null +++ b/ISSUE-474.md @@ -0,0 +1,42 @@ +## Goal + +Make each email row in **Undo Log Detail** (`lib/ui/screens/undo_log_detail_screen.dart`) tappable, navigating to that email's current location in the app. The issue gates this on "when structured search is implemented" — structured search now exists (`lib/core/filter/filter_expression.dart`, `lib/ui/screens/search_screen.dart`), and the repository already exposes `findEmailByMessageId(accountId, messageId)` (`lib/data/repositories/email_repository_impl.dart:1899`), so we have the building block needed to locate an email regardless of moves. + +## Why direct lookup, not a search-screen handoff + +After a Move, the email lives at `destinationMailboxPath` with a *new* UID, so the `Email.id` stored in `UndoAction.originalEmails` is stale. The stable identifier across moves is `messageId`. `findEmailByMessageId` returns the current row (with current `mailboxPath` and `id`), giving us a one-tap deep link to the email detail screen — better UX than dumping the user into search. + +## Implementation + +### 1. `lib/ui/screens/undo_log_detail_screen.dart` + +- Convert `_EmailTile` from `StatelessWidget` to `ConsumerWidget` so it can read `emailRepositoryProvider`. +- Take `accountId` as an additional ctor arg (passed in from the `originalEmails.map` call site so we don't depend on the email's own field, matching how the action scopes lookups). +- Add an `onTap` handler: + 1. If `email.messageId == null` → no-op tap, show `SnackBar('Cannot locate this email — no Message-ID.')`. + 2. Otherwise call `ref.read(emailRepositoryProvider).findEmailByMessageId(action.accountId, email.messageId!)`. + 3. On hit, `context.go('/accounts/${accountId}/mailboxes/${Uri.encodeComponent(found.mailboxPath)}/emails/${Uri.encodeComponent(found.id)}')` — matches the encoding pattern used in `combined_inbox_screen.dart:280` and `email_list_screen.dart:540`. + 4. On miss, show `SnackBar('Email no longer exists at its previous location. Use Undo to restore it.')` — covers the hard-deleted case and the not-yet-resynced case. +- Add `trailing: const Icon(Icons.chevron_right)` to give the row a "navigates" affordance consistent with other tappable `ListTile`s in the app. +- Leave styling otherwise unchanged; the existing `Icons.email_outlined` leading + subject/sender layout stays. + +### 2. No router changes + +The existing email-detail route (`router.dart:153`) is reused as-is. + +### 3. No model / repository changes + +`findEmailByMessageId` is already on `EmailRepository` and scoped per account, which is what we want. + +### 4. Tests — `test/widget/` + +Add a new widget test `test/widget/undo_log_detail_screen_test.dart` covering: +- Tapping a row whose `messageId` resolves via a fake `EmailRepository` navigates to `/accounts//mailboxes//emails/` (assert via a `GoRouter` test harness similar to `test/widget/email_detail_screen_test.dart`). +- Tapping a row when `findEmailByMessageId` returns `null` shows the "no longer exists" SnackBar and does not navigate. +- Tapping a row with `messageId == null` shows the "no Message-ID" SnackBar. + +## Out of scope + +- Adding Message-ID as a structured `FilterField` — not needed for direct navigation; can be revisited if a UI for "search for this email" is ever wanted. +- Changing the Undo Log list screen (`undo_log_screen.dart`) — the issue is specifically about the *detail* screen. +- Persisting/refreshing a stale `originalEmails` list — Move/Snooze update the row in place, so subsequent re-lookups by Message-ID will find them; nothing to maintain.