feat: add 'Mark all as read' to mailbox overflow menu (U8) (#48)

This commit was merged in pull request #48.
This commit is contained in:
Bot of Thomas Güttler
2026-05-14 10:58:33 +02:00
parent 132b6aeb9a
commit 499774d1a6
10 changed files with 146 additions and 0 deletions
@@ -27,6 +27,7 @@ abstract class EmailRepository {
Future<EmailBody> getEmailBody(String emailId);
Future<SyncEmailsResult> syncEmails(String accountId, String mailboxPath);
Future<void> setFlag(String emailId, {bool? seen, bool? flagged});
Future<void> markAllAsRead(String accountId, String mailboxPath);
Future<void> moveEmail(String emailId, String destMailboxPath);
/// Deletes the email. Returns the path of the mailbox it was moved to
@@ -1520,6 +1520,63 @@ class EmailRepositoryImpl implements EmailRepository {
);
}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {
final account = (await _accounts.getAccount(accountId))!;
final unread = await (_db.select(_db.emails)
..where(
(t) =>
t.accountId.equals(accountId) &
t.mailboxPath.equals(mailboxPath) &
t.isSeen.equals(false),
))
.get();
if (unread.isEmpty) return;
await _db.transaction(() async {
for (final row in unread) {
if (account.type == account_model.AccountType.jmap) {
await _enqueueChange(
accountId,
row.id,
'flag_seen',
jsonEncode({'seen': true}),
);
} else {
await _enqueueChange(
accountId,
row.id,
'flag_seen',
jsonEncode({
'uid': row.uid,
'mailboxPath': row.mailboxPath,
'seen': true,
}),
);
}
}
// Bulk mark all unread emails in this mailbox as seen.
await (_db.update(_db.emails)
..where(
(t) =>
t.accountId.equals(accountId) &
t.mailboxPath.equals(mailboxPath) &
t.isSeen.equals(false),
))
.write(const EmailsCompanion(isSeen: Value(true)));
// Update all threads in this mailbox to reflect no unread.
await (_db.update(_db.threads)
..where(
(t) =>
t.accountId.equals(accountId) &
t.mailboxPath.equals(mailboxPath),
))
.write(const ThreadsCompanion(hasUnread: Value(false)));
});
}
@override
Future<void> moveEmail(String emailId, String destMailboxPath) async {
final row = await (_db.select(
+16
View File
@@ -193,6 +193,22 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
extra: {'accountId': widget.accountId},
),
),
PopupMenuButton<String>(
onSelected: (value) async {
if (value == 'mark_all_read') {
await emailRepo.markAllAsRead(
widget.accountId,
widget.mailboxPath,
);
}
},
itemBuilder: (_) => const [
PopupMenuItem(
value: 'mark_all_read',
child: Text('Mark all as read'),
),
],
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60),
@@ -190,6 +190,9 @@ class _FakeEmails implements EmailRepository {
@override
Future<void> setFlag(String id, {bool? seen, bool? flagged}) async {}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {}
@override
Future<void> moveEmail(String id, String dest) async {}
+2
View File
@@ -61,6 +61,8 @@ class FakeEmailRepository implements EmailRepository {
@override
Future<void> setFlag(String id, {bool? seen, bool? flagged}) async {}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {}
@override
Future<void> moveEmail(String id, String dest) async {}
@override
@@ -337,6 +337,23 @@ class MockEmailRepository extends _i1.Mock implements _i9.EmailRepository {
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> markAllAsRead(
String? accountId,
String? mailboxPath,
) =>
(super.noSuchMethod(
Invocation.method(
#markAllAsRead,
[
accountId,
mailboxPath,
],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> moveEmail(
String? emailId,
@@ -126,6 +126,35 @@ abstract class EmailRepositoryContract {
expect(email!.isFlagged, isTrue);
});
test('markAllAsRead marks every unread email in the mailbox', () async {
final repo = await makeRepo();
await insertEmail(
repo,
id: 'er-acc:20',
mailboxPath: 'INBOX',
isSeen: false,
);
await insertEmail(
repo,
id: 'er-acc:21',
mailboxPath: 'INBOX',
isSeen: false,
);
await insertEmail(
repo,
id: 'er-acc:22',
mailboxPath: 'Sent',
isSeen: false,
);
await repo.markAllAsRead(_account.id, 'INBOX');
expect((await repo.getEmail('er-acc:20'))!.isSeen, isTrue);
expect((await repo.getEmail('er-acc:21'))!.isSeen, isTrue);
// Email in a different mailbox should be untouched.
expect((await repo.getEmail('er-acc:22'))!.isSeen, isFalse);
});
test('observeThreads starts empty', () async {
final repo = await makeRepo();
expect(
+2
View File
@@ -103,6 +103,8 @@ class _CountingEmails implements EmailRepository {
@override
Future<void> setFlag(String id, {bool? seen, bool? flagged}) async {}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {}
@override
Future<void> moveEmail(String id, String dest) async {}
@override
Future<String?> deleteEmail(String id) async => null;
+17
View File
@@ -197,6 +197,23 @@ class MockEmailRepository extends _i1.Mock implements _i3.EmailRepository {
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> markAllAsRead(
String? accountId,
String? mailboxPath,
) =>
(super.noSuchMethod(
Invocation.method(
#markAllAsRead,
[
accountId,
mailboxPath,
],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> moveEmail(
String? emailId,
+2
View File
@@ -214,6 +214,8 @@ class FakeEmailRepository implements EmailRepository {
@override
Future<void> setFlag(String emailId, {bool? seen, bool? flagged}) async {}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {}
@override
Future<void> moveEmail(String emailId, String destMailboxPath) async {}