feat(detail): drop AppBar subject, surface Mark as spam icon (#531)

## Summary
- Drop the truncated subject preview from the single-mail AppBar title; the full subject is already shown in the body header.
- Replace the popup-menu entry for **Mark as spam** with a direct `IconButton` (`Icons.report_outlined`) in the AppBar actions so the action is reachable without opening the `⋯` menu.
- Update affected widget tests for the new layout (subject is only in the body header; spam action is now a standalone button rather than a popup item).

Closes #528

## Test plan
- [x] `dart format --output=none --set-exit-if-changed lib test` — 0 changed
- [x] `dart analyze --fatal-infos lib test` — no issues
- [x] `flutter test test/widget/email_detail_screen_test.dart test/widget/email_list_screen_test.dart` — 42/42 passing
- [x] Full widget suite (`flutter test test/widget/`) — 172/172 passing

Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/531
This commit was merged in pull request #531.
This commit is contained in:
Bot of Thomas Güttler
2026-06-07 20:05:57 +02:00
committed by guettli
co-authored by guettli
parent 38f7ada8b5
commit 41c8196a97
3 changed files with 24 additions and 22 deletions
+9 -7
View File
@@ -74,10 +74,6 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: !isMobile, automaticallyImplyLeading: !isMobile,
title: Text(
header?.subject ?? '(loading…)',
overflow: TextOverflow.ellipsis,
),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.reply), icon: const Icon(Icons.reply),
@@ -133,12 +129,20 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
if (mounted) setState(() => _isFlagged = next); if (mounted) setState(() => _isFlagged = next);
}, },
), ),
IconButton(
icon: const Icon(Icons.report_outlined),
tooltip: 'Mark as spam',
onPressed: header == null
? null
: () {
unawaited(_markAsSpam(context, header));
},
),
PopupMenuButton<String>( PopupMenuButton<String>(
itemBuilder: (ctx) => [ itemBuilder: (ctx) => [
const PopupMenuItem(value: 'forward', child: Text('Forward')), const PopupMenuItem(value: 'forward', child: Text('Forward')),
const PopupMenuItem(value: 'move', child: Text('Move to folder')), const PopupMenuItem(value: 'move', child: Text('Move to folder')),
const PopupMenuItem(value: 'snooze', child: Text('Snooze')), const PopupMenuItem(value: 'snooze', child: Text('Snooze')),
const PopupMenuItem(value: 'spam', child: Text('Mark as spam')),
const PopupMenuItem( const PopupMenuItem(
value: 'mark_unread', value: 'mark_unread',
child: Text('Mark as unread'), child: Text('Mark as unread'),
@@ -166,8 +170,6 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
unawaited(_moveTo(context, header)); unawaited(_moveTo(context, header));
} else if (value == 'snooze' && header != null) { } else if (value == 'snooze' && header != null) {
unawaited(_snooze(context, header)); unawaited(_snooze(context, header));
} else if (value == 'spam' && header != null) {
unawaited(_markAsSpam(context, header));
} else if (value == 'mark_unread') { } else if (value == 'mark_unread') {
final nextEmailId = await _getNextEmailIdIfNeeded(header); final nextEmailId = await _getNextEmailIdIfNeeded(header);
await repo.setFlag(widget.emailId, seen: false); await repo.setFlag(widget.emailId, seen: false);
+13 -13
View File
@@ -81,7 +81,7 @@ void main() {
expect(find.byType(CircularProgressIndicator), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget);
}); });
testWidgets('shows subject in app bar after data loads', (tester) async { testWidgets('shows subject in email header section', (tester) async {
final email = testEmail(subject: 'Project update'); final email = testEmail(subject: 'Project update');
const body = EmailBody( const body = EmailBody(
emailId: 'acc-1:42', emailId: 'acc-1:42',
@@ -106,8 +106,8 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Subject appears in both the app bar and the email header section. // Subject appears only in the email header section, not in the app bar.
expect(find.text('Project update'), findsAtLeastNWidgets(1)); expect(find.text('Project update'), findsOneWidget);
expect(find.text('See attached slides.'), findsOneWidget); expect(find.text('See attached slides.'), findsOneWidget);
}); });
@@ -266,7 +266,7 @@ void main() {
expect(find.textContaining('carol@example.com'), findsAtLeastNWidgets(1)); expect(find.textContaining('carol@example.com'), findsAtLeastNWidgets(1));
}); });
testWidgets('Mark as spam is in popup menu, not a standalone button', ( testWidgets('Mark as spam is a standalone button, not in popup menu', (
tester, tester,
) async { ) async {
await tester.pumpWidget( await tester.pumpWidget(
@@ -279,19 +279,19 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// No standalone icon button for mark as spam. // Standalone icon button for mark as spam is in the app bar.
expect( expect(
find.byWidgetPredicate( find.byWidgetPredicate(
(w) => w is Tooltip && w.message == 'Mark as spam', (w) => w is Tooltip && w.message == 'Mark as spam',
), ),
findsNothing, findsOneWidget,
); );
// It appears in the popup menu. // It does NOT appear in the popup menu.
await tester.tap(find.byType(PopupMenuButton<String>)); await tester.tap(find.byType(PopupMenuButton<String>));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Mark as spam'), findsOneWidget); expect(find.text('Mark as spam'), findsNothing);
}); });
testWidgets('Mark as spam shows dialog when no junk folder', ( testWidgets('Mark as spam shows dialog when no junk folder', (
@@ -309,11 +309,11 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Open the popup menu first, then tap Mark as spam. await tester.tap(
await tester.tap(find.byType(PopupMenuButton<String>)); find.byWidgetPredicate(
await tester.pumpAndSettle(); (w) => w is Tooltip && w.message == 'Mark as spam',
),
await tester.tap(find.text('Mark as spam')); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('No spam folder found'), findsOneWidget); expect(find.text('No spam folder found'), findsOneWidget);
+2 -2
View File
@@ -446,10 +446,10 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(EmailDetailScreen), findsOneWidget); expect(find.byType(EmailDetailScreen), findsOneWidget);
// The detail AppBar title shows the first email's subject. // The detail body header shows the first email's subject.
expect( expect(
find.descendant( find.descendant(
of: find.byType(AppBar), of: find.byType(EmailDetailScreen),
matching: find.text('Alpha Match'), matching: find.text('Alpha Match'),
), ),
findsOneWidget, findsOneWidget,