Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05c0334532 |
+100
@@ -0,0 +1,100 @@
|
||||
## Root cause analysis
|
||||
|
||||
The "Load remote images" button is rendered in two places: `lib/ui/screens/email_detail_screen.dart:228-262` (single mail view) and `lib/ui/screens/thread_detail_screen.dart:203-237` (thread view). Both call the same pattern:
|
||||
|
||||
```dart
|
||||
onPressed: () {
|
||||
setState(() => _loadRemoteImages = true); // 1. schedule rebuild
|
||||
if (senderEmail != null) {
|
||||
unawaited(...addTrustedImageSender(senderEmail)); // 2. fire-and-forget DB write
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(SnackBar( // 3. queue snack bar
|
||||
duration: const Duration(seconds: 3),
|
||||
...
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Although `duration: 3s` is already set, the snack bar fails to auto-dismiss. This mirrors the bug fixed in PR #401 (issue #399): there, a snack bar fired during a navigation transition and the duration timer "didn't start correctly" because the snack bar was queued on an unstable scaffold.
|
||||
|
||||
Here, the analogous instability comes from three rebuilds that all land between `showSnackBar` and the moment the SnackBar's enter-animation would normally complete and start its dismiss timer:
|
||||
|
||||
1. The synchronous `setState` flips `_loadRemoteImages` → `true`, which immediately removes the "Load remote images" button (the very widget whose `onPressed` was running) and swaps the `SecureEmailWebView` into the rebuilt subtree with `loadRemoteImages: true`. The WebView's `didUpdateWidget` then triggers an async `loadHtmlString` reload (see `lib/ui/widgets/secure_email_webview.dart:100-106`), which subsequently calls `setState(() => _height = h)` inside `_measureHeight`.
|
||||
2. The fire-and-forget `addTrustedImageSender` write resolves a moment later, the `trustedImageSendersProvider` stream emits, and `ref.watch(trustedImageSendersProvider)` in `email_detail_screen.dart:197` causes another rebuild of the whole screen body — including the `Scaffold`'s body subtree that hosts the snack bar overlay's host context.
|
||||
3. These rebuilds happen during the SnackBar's enter animation, so the `_SnackBarState` ends up holding stale animation state and the per-snack-bar timer that schedules `hideCurrentSnackBar` after `duration` never fires.
|
||||
|
||||
## Plan
|
||||
|
||||
### Fix
|
||||
|
||||
Queue the snack bar **before** mutating state, so it reaches `ScaffoldMessenger` while the Scaffold subtree is still stable, and defer the state change to a post-frame callback so the snack bar's enter-animation can finish before the WebView reload and the provider-driven rebuild run.
|
||||
|
||||
In `lib/ui/screens/email_detail_screen.dart`, replace the body of `OutlinedButton.icon.onPressed` at lines 231-261 with:
|
||||
|
||||
```dart
|
||||
onPressed: () {
|
||||
if (senderEmail != null) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(userPreferencesRepositoryProvider)
|
||||
.addTrustedImageSender(senderEmail),
|
||||
);
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: const Text(
|
||||
'Images will be loaded automatically for this sender.',
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'View',
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
context.push(
|
||||
'/accounts/trusted-senders',
|
||||
extra: senderEmail,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _loadRemoteImages = true);
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
Apply the same reordering to `lib/ui/screens/thread_detail_screen.dart:206-236`.
|
||||
|
||||
The key changes:
|
||||
- `showSnackBar` runs first, on the still-stable scaffold subtree.
|
||||
- `setState` (which triggers WebView swap-in and subsequent rebuilds) is deferred to a post-frame callback.
|
||||
- When `senderEmail == null` (no trusted-sender to register, so no snack bar), the post-frame callback still flips `_loadRemoteImages` to true — preserving existing behavior of the button working even for unknown senders.
|
||||
|
||||
### Tests
|
||||
|
||||
Add a widget test in `test/widget/email_detail_screen_test.dart` that:
|
||||
|
||||
1. Pumps an `EmailDetailScreen` with an HTML body and a non-empty `From` header.
|
||||
2. Taps the "Load remote images" button.
|
||||
3. Verifies the snack bar with text "Images will be loaded automatically for this sender." appears.
|
||||
4. Calls `tester.pump(const Duration(seconds: 4))` (or uses `tester.pumpAndSettle` after a 3.5s pump).
|
||||
5. Verifies the snack bar is gone (`expect(find.byType(SnackBar), findsNothing)`).
|
||||
6. Verifies `_loadRemoteImages` did flip, by checking that the "Load remote images" button is no longer present.
|
||||
|
||||
Add an analogous test in `test/widget/thread_detail_screen_test.dart` (or wherever thread tests live; create the file if it does not exist yet — use the email_detail test as a template).
|
||||
|
||||
### Out of scope
|
||||
|
||||
- The "First update agent loop, fix search bug" line in the issue body is two unrelated todo notes the reporter jotted down (the search bug is tracked separately). This plan does not address them.
|
||||
- Other `showSnackBar` call sites in `email_detail_screen.dart` (download success/failure, copy-to-clipboard, raw-email errors, etc.) are not affected by the same rebuild pattern and stay unchanged.
|
||||
|
||||
### Verification checklist
|
||||
|
||||
- [ ] `dart test` (or the project's `task test` equivalent) passes, including the two new widget tests.
|
||||
- [ ] Manual: open a single mail in `EmailDetailScreen` with HTML body from a sender not yet trusted; tap "Load remote images"; verify snack bar appears, images load, and snack bar disappears after ~3 seconds.
|
||||
- [ ] Manual: tap "View" on the snack bar before it dismisses; verify it navigates to `/accounts/trusted-senders` and that the snack bar is dismissed by the navigation as expected.
|
||||
- [ ] Manual: repeat in `ThreadDetailScreen`.
|
||||
Reference in New Issue
Block a user