diff --git a/lib/core/db_schema_version.dart b/lib/core/db_schema_version.dart index 85e2c74..2379cdd 100644 --- a/lib/core/db_schema_version.dart +++ b/lib/core/db_schema_version.dart @@ -1 +1 @@ -const int dbSchemaVersion = 34; +const int dbSchemaVersion = 36; diff --git a/lib/core/models/user_preferences.dart b/lib/core/models/user_preferences.dart index 9a806d5..598ab88 100644 --- a/lib/core/models/user_preferences.dart +++ b/lib/core/models/user_preferences.dart @@ -1,6 +1,14 @@ enum MenuPosition { bottom, top } +enum AfterMailViewAction { nextMessage, showMailbox } + 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 mailViewButtonPosition; + final AfterMailViewAction afterMailViewAction; } diff --git a/lib/core/repositories/user_preferences_repository.dart b/lib/core/repositories/user_preferences_repository.dart index c2f5333..4b26113 100644 --- a/lib/core/repositories/user_preferences_repository.dart +++ b/lib/core/repositories/user_preferences_repository.dart @@ -3,4 +3,6 @@ import 'package:sharedinbox/core/models/user_preferences.dart'; abstract class UserPreferencesRepository { Stream observePreferences(); Future updateMenuPosition(MenuPosition position); + Future updateMailViewButtonPosition(MenuPosition position); + Future updateAfterMailViewAction(AfterMailViewAction action); } diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index 9619849..01164d5 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -313,6 +313,12 @@ class UserPreferences extends Table { IntColumn get id => integer()(); // 'bottom' (default) | 'top' 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 Set get primaryKey => {id}; @@ -593,6 +599,18 @@ class AppDatabase extends _$AppDatabase { if (from < 34) { 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, + ); + } }, ); } diff --git a/lib/data/repositories/user_preferences_repository_impl.dart b/lib/data/repositories/user_preferences_repository_impl.dart index 71535df..ca02c07 100644 --- a/lib/data/repositories/user_preferences_repository_impl.dart +++ b/lib/data/repositories/user_preferences_repository_impl.dart @@ -26,6 +26,28 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository { ); } + @override + Future updateMailViewButtonPosition(pref.MenuPosition position) async { + await _db.into(_db.userPreferences).insertOnConflictUpdate( + UserPreferencesCompanion( + id: const Value(_rowId), + mailViewButtonPosition: Value(position.name), + ), + ); + } + + @override + Future 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) { if (row == null) return const pref.UserPreferences(); return pref.UserPreferences( @@ -33,6 +55,14 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository { (e) => e.name == row.menuPosition, 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, + ), ); } } diff --git a/lib/ui/screens/email_detail_screen.dart b/lib/ui/screens/email_detail_screen.dart index 7a8f4a8..c0246ae 100644 --- a/lib/ui/screens/email_detail_screen.dart +++ b/lib/ui/screens/email_detail_screen.dart @@ -13,6 +13,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:sharedinbox/core/models/email.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/html_utils.dart'; import 'package:sharedinbox/di.dart'; @@ -98,6 +99,7 @@ class _EmailDetailScreenState extends ConsumerState { icon: const Icon(Icons.delete), tooltip: 'Delete', onPressed: () async { + final nextEmailId = await _getNextEmailIdIfNeeded(header); final destPath = await repo.deleteEmail(widget.emailId); if (header != null) { @@ -116,7 +118,7 @@ class _EmailDetailScreenState extends ConsumerState { ); } - if (context.mounted) context.pop(); + if (context.mounted) _navigateTo(context, header, nextEmailId); }, ), IconButton( @@ -171,8 +173,9 @@ class _EmailDetailScreenState extends ConsumerState { ], onSelected: (value) async { if (value == 'mark_unread') { + final nextEmailId = await _getNextEmailIdIfNeeded(header); 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) { _showHeaders(context, body); } else if (value == 'structure' && body != null) { @@ -252,6 +255,39 @@ class _EmailDetailScreenState extends ConsumerState { ); } + Future _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 _downloadAndOpen(EmailAttachment att) async { setState(() => _downloading.add(att.filename)); try { @@ -403,6 +439,9 @@ class _EmailDetailScreenState extends ConsumerState { } Future _archive(BuildContext context, Email header) async { + final nextEmailId = await _getNextEmailIdIfNeeded(header); + if (!context.mounted) return; + final mailbox = await resolveMailboxByRole( context, ref.read(mailboxRepositoryProvider), @@ -432,10 +471,13 @@ class _EmailDetailScreenState extends ConsumerState { ), ); - if (context.mounted) context.pop(); + if (context.mounted) _navigateTo(context, header, nextEmailId); } Future _markAsSpam(BuildContext context, Email header) async { + final nextEmailId = await _getNextEmailIdIfNeeded(header); + if (!context.mounted) return; + final mailbox = await resolveMailboxByRole( context, ref.read(mailboxRepositoryProvider), @@ -465,7 +507,7 @@ class _EmailDetailScreenState extends ConsumerState { ), ); - if (context.mounted) context.pop(); + if (context.mounted) _navigateTo(context, header, nextEmailId); } Future _forward( @@ -490,6 +532,8 @@ class _EmailDetailScreenState extends ConsumerState { } Future _moveTo(BuildContext context, Email header) async { + final nextEmailId = await _getNextEmailIdIfNeeded(header); + final mailboxRepo = ref.read(mailboxRepositoryProvider); final mailboxes = await mailboxRepo.observeMailboxes(header.accountId).first; @@ -538,10 +582,13 @@ class _EmailDetailScreenState extends ConsumerState { ), ); - if (context.mounted) context.pop(); + if (context.mounted) _navigateTo(context, header, nextEmailId); } Future _snooze(BuildContext context, Email header) async { + final nextEmailId = await _getNextEmailIdIfNeeded(header); + if (!context.mounted) return; + final until = await showModalBottomSheet( context: context, builder: (ctx) => const SnoozePicker(), @@ -569,7 +616,7 @@ class _EmailDetailScreenState extends ConsumerState { ), ), ); - context.pop(); + _navigateTo(context, header, nextEmailId); } } diff --git a/lib/ui/screens/thread_detail_screen.dart b/lib/ui/screens/thread_detail_screen.dart index 4178bcb..6f6549c 100644 --- a/lib/ui/screens/thread_detail_screen.dart +++ b/lib/ui/screens/thread_detail_screen.dart @@ -7,6 +7,7 @@ import 'package:intl/intl.dart'; import 'package:sharedinbox/core/models/email.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/di.dart'; import 'package:sharedinbox/ui/widgets/secure_email_webview.dart'; @@ -28,9 +29,16 @@ class ThreadDetailScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final repo = ref.watch(emailRepositoryProvider); + final prefs = + ref.watch(userPreferencesProvider).value ?? const UserPreferences(); + final buttonAtBottom = prefs.mailViewButtonPosition == MenuPosition.bottom; return Scaffold( - appBar: AppBar(title: const Text('Thread')), + appBar: AppBar( + title: const Text('Thread'), + automaticallyImplyLeading: !buttonAtBottom, + ), + bottomNavigationBar: buttonAtBottom ? _buildBackButtonBar(context) : null, body: StreamBuilder>( stream: repo.observeEmailsInThread(accountId, mailboxPath, threadId), 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 { diff --git a/lib/ui/screens/user_preferences_screen.dart b/lib/ui/screens/user_preferences_screen.dart index af18ffe..e1dd6de 100644 --- a/lib/ui/screens/user_preferences_screen.dart +++ b/lib/ui/screens/user_preferences_screen.dart @@ -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( + groupValue: prefs.mailViewButtonPosition, + onChanged: (value) { + if (value == null) return; + unawaited( + ref + .read(userPreferencesRepositoryProvider) + .updateMailViewButtonPosition(value), + ); + }, + child: const Column( + children: [ + RadioListTile( + title: Text('Bottom (default)'), + subtitle: Text( + 'Show the back button at the bottom of the screen.', + ), + value: MenuPosition.bottom, + ), + RadioListTile( + 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( + groupValue: prefs.afterMailViewAction, + onChanged: (value) { + if (value == null) return; + unawaited( + ref + .read(userPreferencesRepositoryProvider) + .updateAfterMailViewAction(value), + ); + }, + child: const Column( + children: [ + RadioListTile( + title: Text('Next message (default)'), + subtitle: Text( + 'Show the next message in the mailbox.', + ), + value: AfterMailViewAction.nextMessage, + ), + RadioListTile( + title: Text('Return to mailbox'), + subtitle: Text( + 'Return to the message list.', + ), + value: AfterMailViewAction.showMailbox, + ), + ], + ), + ), ], ), ), diff --git a/test/unit/migration_test.dart b/test/unit/migration_test.dart index aff972b..ac36bab 100644 --- a/test/unit/migration_test.dart +++ b/test/unit/migration_test.dart @@ -14,7 +14,7 @@ void main() { group('Migration', () { test('schemaVersion matches expected value', () async { final db = AppDatabase(NativeDatabase.memory()); - expect(db.schemaVersion, 34); + expect(db.schemaVersion, 36); await db.close(); }); @@ -202,6 +202,13 @@ void main() { // v34: user_preferences table. 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(); if (dbFile.existsSync()) dbFile.deleteSync(); }); @@ -397,11 +404,18 @@ void main() { // v34: user_preferences table. 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(); 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()); await db.select(db.accounts).get(); @@ -448,6 +462,13 @@ void main() { expect(syncLogColumns, contains('error_stack_trace')); 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(); }); }); diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index 208ab0d..bfb0360 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -414,6 +414,7 @@ class _NoOpManageSieveProbeService implements ManageSieveProbeService { Widget buildApp({ required String initialLocation, required List overrides, + UserPreferencesRepository? userPreferences, }) { final testRouter = GoRouter( initialLocation: initialLocation, @@ -523,7 +524,7 @@ Widget buildApp({ const NoOpSyncLogRepository(), ), userPreferencesRepositoryProvider.overrideWithValue( - FakeUserPreferencesRepository(), + userPreferences ?? FakeUserPreferencesRepository(), ), ...overrides, manageSieveProbeServiceProvider.overrideWith( @@ -624,18 +625,37 @@ Email testEmail({ class FakeUserPreferencesRepository implements UserPreferencesRepository { FakeUserPreferencesRepository({ this.menuPosition = MenuPosition.bottom, + this.mailViewButtonPosition = MenuPosition.bottom, + this.afterMailViewAction = AfterMailViewAction.nextMessage, }); MenuPosition menuPosition; + MenuPosition mailViewButtonPosition; + AfterMailViewAction afterMailViewAction; @override - Stream observePreferences() => - Stream.value(UserPreferences(menuPosition: menuPosition)); + Stream observePreferences() => Stream.value( + UserPreferences( + menuPosition: menuPosition, + mailViewButtonPosition: mailViewButtonPosition, + afterMailViewAction: afterMailViewAction, + ), + ); @override Future updateMenuPosition(MenuPosition position) async { menuPosition = position; } + + @override + Future updateMailViewButtonPosition(MenuPosition position) async { + mailViewButtonPosition = position; + } + + @override + Future updateAfterMailViewAction(AfterMailViewAction action) async { + afterMailViewAction = action; + } } class FakeSearchHistoryRepository implements SearchHistoryRepository { diff --git a/test/widget/thread_detail_screen_test.dart b/test/widget/thread_detail_screen_test.dart index 44fd8f3..e61f19d 100644 --- a/test/widget/thread_detail_screen_test.dart +++ b/test/widget/thread_detail_screen_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sharedinbox/core/models/email.dart'; +import 'package:sharedinbox/core/models/user_preferences.dart'; import 'package:sharedinbox/di.dart'; import 'helpers.dart'; @@ -142,6 +143,60 @@ void main() { 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 { final email = _threadEmail(isFlagged: true); await tester.pumpWidget( diff --git a/test/widget/user_preferences_screen_test.dart b/test/widget/user_preferences_screen_test.dart index 61ff92f..d41db2f 100644 --- a/test/widget/user_preferences_screen_test.dart +++ b/test/widget/user_preferences_screen_test.dart @@ -20,11 +20,13 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Menu bar position'), findsOneWidget); - expect(find.text('Bottom (default)'), findsOneWidget); - expect(find.text('Top'), findsOneWidget); + expect(find.text('Bottom (default)'), findsNWidgets(2)); + 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( buildApp( initialLocation: '/accounts/preferences', @@ -33,12 +35,15 @@ void main() { ); await tester.pumpAndSettle(); - final radioGroup = find.byType(RadioGroup); - final widget = tester.widget>(radioGroup); - expect(widget.groupValue, MenuPosition.bottom); + expect( + find.text('Single mail view button position'), + findsOneWidget, + ); }); - testWidgets('tapping Top option updates the repo', (tester) async { + testWidgets('menu position bottom option is selected by default', ( + tester, + ) async { await tester.pumpWidget( buildApp( initialLocation: '/accounts/preferences', @@ -47,7 +52,41 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Top')); + final radioGroups = find.byType(RadioGroup); + final menuGroup = + tester.widget>(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); + final mailViewGroup = + tester.widget>(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(); final repo = ProviderScope.containerOf( @@ -57,5 +96,91 @@ void main() { 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); + final group = + tester.widget>(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); + }); }); }