Compare commits

...
4 Commits
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 badc449866 feat: configurable next action after single mail view (#300)
Add user preference to control what happens after performing an action
(delete, archive, move, snooze, mark as unread) in the single mail view.
The default is to show the next message in the mailbox; users can opt
to return to the mailbox instead.

- New AfterMailViewAction enum (nextMessage, showMailbox)
- DB schema v36: after_mail_view_action column on user_preferences
- UserPreferences model, repository interface and impl updated
- EmailDetailScreen: reads preference, finds next email before action,
  navigates via context.go() or context.pop() accordingly
- UserPreferencesScreen: new "After mail action" section with radio group
- Tests updated: FakeUserPreferencesRepository, migration test, new
  UserPreferencesScreen tests with scroll-to-reveal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:32:41 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a56847fdb5 feat: configurable back button position for single mail view (#299)
Add a mailViewButtonPosition preference (bottom/top) that controls
where the back button appears in the thread detail screen. Default
is bottom, showing a BottomAppBar with an arrow_back button and
hiding the AppBar's leading back button. Users can switch to top
to restore the standard AppBar back button. Preference is stored
in the UserPreferences DB table (schema v35) and configurable via
the Preferences screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:32:41 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ef3d278c5f fix: wrap bottom bar menu button in Builder to get Scaffold context
Scaffold.of() requires a descendant context of the Scaffold widget.
Using the State's build context (which is the Scaffold's parent) caused
an assertion failure when tapping the 'Open folders' button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:32:41 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 e1ccfabfdd feat: configurable menu bar position for mailbox view (#298)
Move the folder navigation drawer trigger to the bottom by default,
matching the issue request. Add a user preferences DB table (schema v34)
and a settings screen so users can switch back to the top hamburger menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:32:41 +02:00
12 changed files with 448 additions and 22 deletions
+1 -1
View File
@@ -1 +1 @@
const int dbSchemaVersion = 34; const int dbSchemaVersion = 36;
+9 -1
View File
@@ -1,6 +1,14 @@
enum MenuPosition { bottom, top } enum MenuPosition { bottom, top }
enum AfterMailViewAction { nextMessage, showMailbox }
class UserPreferences { class UserPreferences {
const UserPreferences({this.menuPosition = MenuPosition.bottom}); const UserPreferences({
this.menuPosition = MenuPosition.bottom,
this.mailViewButtonPosition = MenuPosition.bottom,
this.afterMailViewAction = AfterMailViewAction.nextMessage,
});
final MenuPosition menuPosition; final MenuPosition menuPosition;
final MenuPosition mailViewButtonPosition;
final AfterMailViewAction afterMailViewAction;
} }
@@ -3,4 +3,6 @@ import 'package:sharedinbox/core/models/user_preferences.dart';
abstract class UserPreferencesRepository { abstract class UserPreferencesRepository {
Stream<UserPreferences> observePreferences(); Stream<UserPreferences> observePreferences();
Future<void> updateMenuPosition(MenuPosition position); Future<void> updateMenuPosition(MenuPosition position);
Future<void> updateMailViewButtonPosition(MenuPosition position);
Future<void> updateAfterMailViewAction(AfterMailViewAction action);
} }
+18
View File
@@ -313,6 +313,12 @@ class UserPreferences extends Table {
IntColumn get id => integer()(); IntColumn get id => integer()();
// 'bottom' (default) | 'top' // 'bottom' (default) | 'top'
TextColumn get menuPosition => text().withDefault(const Constant('bottom'))(); TextColumn get menuPosition => text().withDefault(const Constant('bottom'))();
// Added in schema v35: 'bottom' (default) | 'top'
TextColumn get mailViewButtonPosition =>
text().withDefault(const Constant('bottom'))();
// Added in schema v36: 'nextMessage' (default) | 'showMailbox'
TextColumn get afterMailViewAction =>
text().withDefault(const Constant('nextMessage'))();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
@@ -593,6 +599,18 @@ class AppDatabase extends _$AppDatabase {
if (from < 34) { if (from < 34) {
await m.createTable(userPreferences); await m.createTable(userPreferences);
} }
if (from >= 34 && from < 35) {
await m.addColumn(
userPreferences,
userPreferences.mailViewButtonPosition,
);
}
if (from >= 34 && from < 36) {
await m.addColumn(
userPreferences,
userPreferences.afterMailViewAction,
);
}
}, },
); );
} }
@@ -26,6 +26,28 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
); );
} }
@override
Future<void> updateMailViewButtonPosition(pref.MenuPosition position) async {
await _db.into(_db.userPreferences).insertOnConflictUpdate(
UserPreferencesCompanion(
id: const Value(_rowId),
mailViewButtonPosition: Value(position.name),
),
);
}
@override
Future<void> updateAfterMailViewAction(
pref.AfterMailViewAction action,
) async {
await _db.into(_db.userPreferences).insertOnConflictUpdate(
UserPreferencesCompanion(
id: const Value(_rowId),
afterMailViewAction: Value(action.name),
),
);
}
static pref.UserPreferences _rowToModel(UserPreferencesRow? row) { static pref.UserPreferences _rowToModel(UserPreferencesRow? row) {
if (row == null) return const pref.UserPreferences(); if (row == null) return const pref.UserPreferences();
return pref.UserPreferences( return pref.UserPreferences(
@@ -33,6 +55,14 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
(e) => e.name == row.menuPosition, (e) => e.name == row.menuPosition,
orElse: () => pref.MenuPosition.bottom, orElse: () => pref.MenuPosition.bottom,
), ),
mailViewButtonPosition: pref.MenuPosition.values.firstWhere(
(e) => e.name == row.mailViewButtonPosition,
orElse: () => pref.MenuPosition.bottom,
),
afterMailViewAction: pref.AfterMailViewAction.values.firstWhere(
(e) => e.name == row.afterMailViewAction,
orElse: () => pref.AfterMailViewAction.nextMessage,
),
); );
} }
} }
+53 -6
View File
@@ -13,6 +13,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/email.dart';
import 'package:sharedinbox/core/models/undo_action.dart'; import 'package:sharedinbox/core/models/undo_action.dart';
import 'package:sharedinbox/core/models/user_preferences.dart';
import 'package:sharedinbox/core/utils/format_utils.dart'; import 'package:sharedinbox/core/utils/format_utils.dart';
import 'package:sharedinbox/core/utils/html_utils.dart'; import 'package:sharedinbox/core/utils/html_utils.dart';
import 'package:sharedinbox/di.dart'; import 'package:sharedinbox/di.dart';
@@ -98,6 +99,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
tooltip: 'Delete', tooltip: 'Delete',
onPressed: () async { onPressed: () async {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
final destPath = await repo.deleteEmail(widget.emailId); final destPath = await repo.deleteEmail(widget.emailId);
if (header != null) { if (header != null) {
@@ -116,7 +118,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
); );
} }
if (context.mounted) context.pop(); if (context.mounted) _navigateTo(context, header, nextEmailId);
}, },
), ),
IconButton( IconButton(
@@ -171,8 +173,9 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
], ],
onSelected: (value) async { onSelected: (value) async {
if (value == 'mark_unread') { if (value == 'mark_unread') {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
await repo.setFlag(widget.emailId, seen: false); await repo.setFlag(widget.emailId, seen: false);
if (context.mounted) context.pop(); if (context.mounted) _navigateTo(context, header, nextEmailId);
} else if (value == 'headers' && body != null) { } else if (value == 'headers' && body != null) {
_showHeaders(context, body); _showHeaders(context, body);
} else if (value == 'structure' && body != null) { } else if (value == 'structure' && body != null) {
@@ -252,6 +255,39 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
); );
} }
Future<String?> _getNextEmailIdIfNeeded(Email? header) async {
if (header == null) return null;
final prefs = ref.read(userPreferencesProvider).value;
final action =
prefs?.afterMailViewAction ?? AfterMailViewAction.nextMessage;
if (action != AfterMailViewAction.nextMessage) return null;
final threads = await ref
.read(emailRepositoryProvider)
.observeThreads(header.accountId, header.mailboxPath)
.first;
final currentIndex =
threads.indexWhere((t) => t.emailIds.contains(widget.emailId));
if (currentIndex >= 0 && currentIndex + 1 < threads.length) {
return threads[currentIndex + 1].latestEmailId;
}
return null;
}
void _navigateTo(BuildContext context, Email? header, String? nextEmailId) {
if (!context.mounted) return;
if (nextEmailId != null && header != null) {
context.go(
'/accounts/${header.accountId}'
'/mailboxes/${Uri.encodeComponent(header.mailboxPath)}'
'/emails/${Uri.encodeComponent(nextEmailId)}',
);
} else {
context.pop();
}
}
Future<void> _downloadAndOpen(EmailAttachment att) async { Future<void> _downloadAndOpen(EmailAttachment att) async {
setState(() => _downloading.add(att.filename)); setState(() => _downloading.add(att.filename));
try { try {
@@ -403,6 +439,9 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
} }
Future<void> _archive(BuildContext context, Email header) async { Future<void> _archive(BuildContext context, Email header) async {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
if (!context.mounted) return;
final mailbox = await resolveMailboxByRole( final mailbox = await resolveMailboxByRole(
context, context,
ref.read(mailboxRepositoryProvider), ref.read(mailboxRepositoryProvider),
@@ -432,10 +471,13 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
), ),
); );
if (context.mounted) context.pop(); if (context.mounted) _navigateTo(context, header, nextEmailId);
} }
Future<void> _markAsSpam(BuildContext context, Email header) async { Future<void> _markAsSpam(BuildContext context, Email header) async {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
if (!context.mounted) return;
final mailbox = await resolveMailboxByRole( final mailbox = await resolveMailboxByRole(
context, context,
ref.read(mailboxRepositoryProvider), ref.read(mailboxRepositoryProvider),
@@ -465,7 +507,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
), ),
); );
if (context.mounted) context.pop(); if (context.mounted) _navigateTo(context, header, nextEmailId);
} }
Future<void> _forward( Future<void> _forward(
@@ -490,6 +532,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
} }
Future<void> _moveTo(BuildContext context, Email header) async { Future<void> _moveTo(BuildContext context, Email header) async {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
final mailboxRepo = ref.read(mailboxRepositoryProvider); final mailboxRepo = ref.read(mailboxRepositoryProvider);
final mailboxes = final mailboxes =
await mailboxRepo.observeMailboxes(header.accountId).first; await mailboxRepo.observeMailboxes(header.accountId).first;
@@ -538,10 +582,13 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
), ),
); );
if (context.mounted) context.pop(); if (context.mounted) _navigateTo(context, header, nextEmailId);
} }
Future<void> _snooze(BuildContext context, Email header) async { Future<void> _snooze(BuildContext context, Email header) async {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
if (!context.mounted) return;
final until = await showModalBottomSheet<DateTime>( final until = await showModalBottomSheet<DateTime>(
context: context, context: context,
builder: (ctx) => const SnoozePicker(), builder: (ctx) => const SnoozePicker(),
@@ -569,7 +616,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
), ),
), ),
); );
context.pop(); _navigateTo(context, header, nextEmailId);
} }
} }
+23 -1
View File
@@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/email.dart';
import 'package:sharedinbox/core/models/undo_action.dart'; import 'package:sharedinbox/core/models/undo_action.dart';
import 'package:sharedinbox/core/models/user_preferences.dart';
import 'package:sharedinbox/core/utils/html_utils.dart'; import 'package:sharedinbox/core/utils/html_utils.dart';
import 'package:sharedinbox/di.dart'; import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/widgets/secure_email_webview.dart'; import 'package:sharedinbox/ui/widgets/secure_email_webview.dart';
@@ -28,9 +29,16 @@ class ThreadDetailScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final repo = ref.watch(emailRepositoryProvider); final repo = ref.watch(emailRepositoryProvider);
final prefs =
ref.watch(userPreferencesProvider).value ?? const UserPreferences();
final buttonAtBottom = prefs.mailViewButtonPosition == MenuPosition.bottom;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Thread')), appBar: AppBar(
title: const Text('Thread'),
automaticallyImplyLeading: !buttonAtBottom,
),
bottomNavigationBar: buttonAtBottom ? _buildBackButtonBar(context) : null,
body: StreamBuilder<List<Email>>( body: StreamBuilder<List<Email>>(
stream: repo.observeEmailsInThread(accountId, mailboxPath, threadId), stream: repo.observeEmailsInThread(accountId, mailboxPath, threadId),
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -60,6 +68,20 @@ class ThreadDetailScreen extends ConsumerWidget {
), ),
); );
} }
Widget _buildBackButtonBar(BuildContext context) {
return BottomAppBar(
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Back',
onPressed: () => context.pop(),
),
],
),
);
}
} }
class _EmailMessageCard extends ConsumerStatefulWidget { class _EmailMessageCard extends ConsumerStatefulWidget {
@@ -59,6 +59,84 @@ class UserPreferencesScreen extends ConsumerWidget {
], ],
), ),
), ),
const Divider(),
ListTile(
title: Text(
'Single mail view button position',
style: Theme.of(context).textTheme.titleSmall,
),
subtitle: const Text(
'Where the back button is shown in the single mail view.',
),
),
RadioGroup<MenuPosition>(
groupValue: prefs.mailViewButtonPosition,
onChanged: (value) {
if (value == null) return;
unawaited(
ref
.read(userPreferencesRepositoryProvider)
.updateMailViewButtonPosition(value),
);
},
child: const Column(
children: [
RadioListTile<MenuPosition>(
title: Text('Bottom (default)'),
subtitle: Text(
'Show the back button at the bottom of the screen.',
),
value: MenuPosition.bottom,
),
RadioListTile<MenuPosition>(
title: Text('Top'),
subtitle: Text(
'Show the back button in the top bar.',
),
value: MenuPosition.top,
),
],
),
),
const Divider(),
ListTile(
title: Text(
'After mail action',
style: Theme.of(context).textTheme.titleSmall,
),
subtitle: const Text(
'What to show after deleting, archiving, or otherwise handling a message.',
),
),
RadioGroup<AfterMailViewAction>(
groupValue: prefs.afterMailViewAction,
onChanged: (value) {
if (value == null) return;
unawaited(
ref
.read(userPreferencesRepositoryProvider)
.updateAfterMailViewAction(value),
);
},
child: const Column(
children: [
RadioListTile<AfterMailViewAction>(
title: Text('Next message (default)'),
subtitle: Text(
'Show the next message in the mailbox.',
),
value: AfterMailViewAction.nextMessage,
),
RadioListTile<AfterMailViewAction>(
title: Text('Return to mailbox'),
subtitle: Text(
'Return to the message list.',
),
value: AfterMailViewAction.showMailbox,
),
],
),
),
], ],
), ),
), ),
+23 -2
View File
@@ -14,7 +14,7 @@ void main() {
group('Migration', () { group('Migration', () {
test('schemaVersion matches expected value', () async { test('schemaVersion matches expected value', () async {
final db = AppDatabase(NativeDatabase.memory()); final db = AppDatabase(NativeDatabase.memory());
expect(db.schemaVersion, 34); expect(db.schemaVersion, 36);
await db.close(); await db.close();
}); });
@@ -202,6 +202,13 @@ void main() {
// v34: user_preferences table. // v34: user_preferences table.
await db.customSelect('SELECT count(*) FROM user_preferences').get(); await db.customSelect('SELECT count(*) FROM user_preferences').get();
// v35: mail_view_button_position column on user_preferences.
final userPrefsColumns = await _tableColumns(db, 'user_preferences');
expect(userPrefsColumns, contains('mail_view_button_position'));
// v36: after_mail_view_action column on user_preferences.
expect(userPrefsColumns, contains('after_mail_view_action'));
await db.close(); await db.close();
if (dbFile.existsSync()) dbFile.deleteSync(); if (dbFile.existsSync()) dbFile.deleteSync();
}); });
@@ -397,11 +404,18 @@ void main() {
// v34: user_preferences table. // v34: user_preferences table.
await db.customSelect('SELECT count(*) FROM user_preferences').get(); await db.customSelect('SELECT count(*) FROM user_preferences').get();
// v35: mail_view_button_position column on user_preferences.
final userPrefsColumns = await _tableColumns(db, 'user_preferences');
expect(userPrefsColumns, contains('mail_view_button_position'));
// v36: after_mail_view_action column on user_preferences.
expect(userPrefsColumns, contains('after_mail_view_action'));
await db.close(); await db.close();
if (dbFile.existsSync()) dbFile.deleteSync(); if (dbFile.existsSync()) dbFile.deleteSync();
}); });
test('fresh install creates all tables at schemaVersion 34', () async { test('fresh install creates all tables at schemaVersion 36', () async {
final db = AppDatabase(NativeDatabase.memory()); final db = AppDatabase(NativeDatabase.memory());
await db.select(db.accounts).get(); await db.select(db.accounts).get();
@@ -448,6 +462,13 @@ void main() {
expect(syncLogColumns, contains('error_stack_trace')); expect(syncLogColumns, contains('error_stack_trace'));
expect(syncLogColumns, contains('is_permanent')); expect(syncLogColumns, contains('is_permanent'));
// v35: mail_view_button_position column on user_preferences.
final userPrefsColumns = await _tableColumns(db, 'user_preferences');
expect(userPrefsColumns, contains('mail_view_button_position'));
// v36: after_mail_view_action column on user_preferences.
expect(userPrefsColumns, contains('after_mail_view_action'));
await db.close(); await db.close();
}); });
}); });
+23 -3
View File
@@ -414,6 +414,7 @@ class _NoOpManageSieveProbeService implements ManageSieveProbeService {
Widget buildApp({ Widget buildApp({
required String initialLocation, required String initialLocation,
required List<Override> overrides, required List<Override> overrides,
UserPreferencesRepository? userPreferences,
}) { }) {
final testRouter = GoRouter( final testRouter = GoRouter(
initialLocation: initialLocation, initialLocation: initialLocation,
@@ -523,7 +524,7 @@ Widget buildApp({
const NoOpSyncLogRepository(), const NoOpSyncLogRepository(),
), ),
userPreferencesRepositoryProvider.overrideWithValue( userPreferencesRepositoryProvider.overrideWithValue(
FakeUserPreferencesRepository(), userPreferences ?? FakeUserPreferencesRepository(),
), ),
...overrides, ...overrides,
manageSieveProbeServiceProvider.overrideWith( manageSieveProbeServiceProvider.overrideWith(
@@ -624,18 +625,37 @@ Email testEmail({
class FakeUserPreferencesRepository implements UserPreferencesRepository { class FakeUserPreferencesRepository implements UserPreferencesRepository {
FakeUserPreferencesRepository({ FakeUserPreferencesRepository({
this.menuPosition = MenuPosition.bottom, this.menuPosition = MenuPosition.bottom,
this.mailViewButtonPosition = MenuPosition.bottom,
this.afterMailViewAction = AfterMailViewAction.nextMessage,
}); });
MenuPosition menuPosition; MenuPosition menuPosition;
MenuPosition mailViewButtonPosition;
AfterMailViewAction afterMailViewAction;
@override @override
Stream<UserPreferences> observePreferences() => Stream<UserPreferences> observePreferences() => Stream.value(
Stream.value(UserPreferences(menuPosition: menuPosition)); UserPreferences(
menuPosition: menuPosition,
mailViewButtonPosition: mailViewButtonPosition,
afterMailViewAction: afterMailViewAction,
),
);
@override @override
Future<void> updateMenuPosition(MenuPosition position) async { Future<void> updateMenuPosition(MenuPosition position) async {
menuPosition = position; menuPosition = position;
} }
@override
Future<void> updateMailViewButtonPosition(MenuPosition position) async {
mailViewButtonPosition = position;
}
@override
Future<void> updateAfterMailViewAction(AfterMailViewAction action) async {
afterMailViewAction = action;
}
} }
class FakeSearchHistoryRepository implements SearchHistoryRepository { class FakeSearchHistoryRepository implements SearchHistoryRepository {
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/email.dart';
import 'package:sharedinbox/core/models/user_preferences.dart';
import 'package:sharedinbox/di.dart'; import 'package:sharedinbox/di.dart';
import 'helpers.dart'; import 'helpers.dart';
@@ -142,6 +143,60 @@ void main() {
expect(find.byIcon(Icons.expand_more), findsOneWidget); expect(find.byIcon(Icons.expand_more), findsOneWidget);
}); });
testWidgets('shows bottom app bar with back button by default', (
tester,
) async {
final email = _threadEmail();
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
overrides: [
accountRepositoryProvider.overrideWithValue(
FakeAccountRepository([kTestAccount]),
),
mailboxRepositoryProvider.overrideWithValue(
FakeMailboxRepository(),
),
emailRepositoryProvider.overrideWithValue(
FakeEmailRepository(emails: [email]),
),
],
),
);
await tester.pumpAndSettle();
expect(find.byType(BottomAppBar), findsOneWidget);
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
});
testWidgets('hides bottom app bar when button position is top', (
tester,
) async {
final email = _threadEmail();
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/acc-1/mailboxes/INBOX/threads/thread-1',
userPreferences: FakeUserPreferencesRepository(
mailViewButtonPosition: MenuPosition.top,
),
overrides: [
accountRepositoryProvider.overrideWithValue(
FakeAccountRepository([kTestAccount]),
),
mailboxRepositoryProvider.overrideWithValue(
FakeMailboxRepository(),
),
emailRepositoryProvider.overrideWithValue(
FakeEmailRepository(emails: [email]),
),
],
),
);
await tester.pumpAndSettle();
expect(find.byType(BottomAppBar), findsNothing);
});
testWidgets('flagged email shows star icon', (tester) async { testWidgets('flagged email shows star icon', (tester) async {
final email = _threadEmail(isFlagged: true); final email = _threadEmail(isFlagged: true);
await tester.pumpWidget( await tester.pumpWidget(
+133 -8
View File
@@ -20,11 +20,13 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Menu bar position'), findsOneWidget); expect(find.text('Menu bar position'), findsOneWidget);
expect(find.text('Bottom (default)'), findsOneWidget); expect(find.text('Bottom (default)'), findsNWidgets(2));
expect(find.text('Top'), findsOneWidget); expect(find.text('Top'), findsNWidgets(2));
}); });
testWidgets('bottom option is selected by default', (tester) async { testWidgets('shows single mail view button position section', (
tester,
) async {
await tester.pumpWidget( await tester.pumpWidget(
buildApp( buildApp(
initialLocation: '/accounts/preferences', initialLocation: '/accounts/preferences',
@@ -33,12 +35,15 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final radioGroup = find.byType(RadioGroup<MenuPosition>); expect(
final widget = tester.widget<RadioGroup<MenuPosition>>(radioGroup); find.text('Single mail view button position'),
expect(widget.groupValue, MenuPosition.bottom); findsOneWidget,
);
}); });
testWidgets('tapping Top option updates the repo', (tester) async { testWidgets('menu position bottom option is selected by default', (
tester,
) async {
await tester.pumpWidget( await tester.pumpWidget(
buildApp( buildApp(
initialLocation: '/accounts/preferences', initialLocation: '/accounts/preferences',
@@ -47,7 +52,41 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Top')); final radioGroups = find.byType(RadioGroup<MenuPosition>);
final menuGroup =
tester.widget<RadioGroup<MenuPosition>>(radioGroups.first);
expect(menuGroup.groupValue, MenuPosition.bottom);
});
testWidgets('mail view button position bottom is selected by default', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
final radioGroups = find.byType(RadioGroup<MenuPosition>);
final mailViewGroup =
tester.widget<RadioGroup<MenuPosition>>(radioGroups.last);
expect(mailViewGroup.groupValue, MenuPosition.bottom);
});
testWidgets('tapping Top in menu position section updates the repo', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Top').first);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final repo = ProviderScope.containerOf( final repo = ProviderScope.containerOf(
@@ -57,5 +96,91 @@ void main() {
expect(repo.menuPosition, MenuPosition.top); expect(repo.menuPosition, MenuPosition.top);
}); });
testWidgets(
'tapping Top in mail view button position section updates the repo', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Top').last);
await tester.pumpAndSettle();
final repo = ProviderScope.containerOf(
tester.element(find.byType(UserPreferencesScreen)),
).read(userPreferencesRepositoryProvider)
as FakeUserPreferencesRepository;
expect(repo.mailViewButtonPosition, MenuPosition.top);
});
testWidgets('shows after mail action section', (tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
// Scroll down to reveal the new section below the fold.
await tester.drag(find.byType(ListView), const Offset(0, -500));
await tester.pumpAndSettle();
expect(find.text('After mail action'), findsOneWidget);
expect(find.text('Next message (default)'), findsOneWidget);
expect(find.text('Return to mailbox'), findsOneWidget);
});
testWidgets('after mail action next message is selected by default', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -500));
await tester.pumpAndSettle();
final radioGroups = find.byType(RadioGroup<AfterMailViewAction>);
final group =
tester.widget<RadioGroup<AfterMailViewAction>>(radioGroups.first);
expect(group.groupValue, AfterMailViewAction.nextMessage);
});
testWidgets('tapping Return to mailbox updates the repo', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/preferences',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -500));
await tester.pumpAndSettle();
await tester.tap(find.text('Return to mailbox'));
await tester.pumpAndSettle();
final repo = ProviderScope.containerOf(
tester.element(find.byType(UserPreferencesScreen)),
).read(userPreferencesRepositoryProvider)
as FakeUserPreferencesRepository;
expect(repo.afterMailViewAction, AfterMailViewAction.showMailbox);
});
}); });
} }