Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
badc449866 | ||
|
|
a56847fdb5 | ||
|
|
ef3d278c5f | ||
|
|
e1ccfabfdd |
@@ -1 +1 @@
|
|||||||
const int dbSchemaVersion = 34;
|
const int dbSchemaVersion = 36;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user