diff --git a/lib/core/repositories/mailbox_repository.dart b/lib/core/repositories/mailbox_repository.dart index 16e08de..42a06fa 100644 --- a/lib/core/repositories/mailbox_repository.dart +++ b/lib/core/repositories/mailbox_repository.dart @@ -20,4 +20,8 @@ abstract class MailboxRepository { String name, String role, ); + + /// Creates a new mailbox named [name] for [accountId] without a special role. + /// Returns the newly created [Mailbox]. + Future createMailbox(String accountId, String name); } diff --git a/lib/data/repositories/mailbox_repository_impl.dart b/lib/data/repositories/mailbox_repository_impl.dart index 00b8646..acbb420 100644 --- a/lib/data/repositories/mailbox_repository_impl.dart +++ b/lib/data/repositories/mailbox_repository_impl.dart @@ -343,11 +343,23 @@ class MailboxRepositoryImpl implements MailboxRepository { } } + @override + Future createMailbox(String accountId, String name) async { + final account = (await _accounts.getAccount(accountId))!; + final password = await _accounts.getPassword(accountId); + switch (account.type) { + case account_model.AccountType.imap: + return _createMailboxWithRoleImap(account, password, name, null); + case account_model.AccountType.jmap: + return _createMailboxWithRoleJmap(account, password, name, null); + } + } + Future _createMailboxWithRoleImap( account_model.Account account, String password, String name, - String role, + String? role, ) async { final client = await _imapConnect( account, @@ -380,7 +392,7 @@ class MailboxRepositoryImpl implements MailboxRepository { account_model.Account account, String password, String name, - String role, + String? role, ) async { final jmapUrl = account.jmapUrl; if (jmapUrl == null || jmapUrl.isEmpty) { @@ -398,7 +410,10 @@ class MailboxRepositoryImpl implements MailboxRepository { { 'accountId': jmap.accountId, 'create': { - 'new-mailbox': {'name': name, 'role': role}, + 'new-mailbox': { + 'name': name, + if (role != null) 'role': role, + }, }, }, '0', diff --git a/lib/ui/screens/email_detail_screen.dart b/lib/ui/screens/email_detail_screen.dart index 2e7240a..d37afb8 100644 --- a/lib/ui/screens/email_detail_screen.dart +++ b/lib/ui/screens/email_detail_screen.dart @@ -563,6 +563,42 @@ class _EmailDetailScreenState extends ConsumerState { ); } + Future _promptNewFolderName(BuildContext context) async { + final controller = TextEditingController(); + try { + return await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Create new folder'), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(hintText: 'Folder name'), + textCapitalization: TextCapitalization.words, + onSubmitted: (value) { + if (value.trim().isNotEmpty) Navigator.pop(ctx, value.trim()); + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final name = controller.text.trim(); + if (name.isNotEmpty) Navigator.pop(ctx, name); + }, + child: const Text('Create'), + ), + ], + ), + ); + } finally { + controller.dispose(); + } + } + Future _moveTo(BuildContext context, Email header) async { final nextEmailId = await _getNextEmailIdIfNeeded(header); @@ -576,6 +612,8 @@ class _EmailDetailScreenState extends ConsumerState { if (!context.mounted) return; + const createNewSentinel = '__create_new__'; + final chosen = await showModalBottomSheet( context: context, builder: (ctx) => ListView( @@ -593,13 +631,28 @@ class _EmailDetailScreenState extends ConsumerState { title: Text(m.name), onTap: () => Navigator.pop(ctx, m.path), ), + ListTile( + leading: const Icon(Icons.create_new_folder_outlined), + title: const Text('Create new folder…'), + onTap: () => Navigator.pop(ctx, createNewSentinel), + ), ], ), ); if (chosen == null || !context.mounted) return; - await ref.read(emailRepositoryProvider).moveEmail(widget.emailId, chosen); + String destination = chosen; + if (chosen == createNewSentinel) { + final name = await _promptNewFolderName(context); + if (name == null || !context.mounted) return; + final mailbox = await mailboxRepo.createMailbox(header.accountId, name); + destination = mailbox.path; + } + + await ref + .read(emailRepositoryProvider) + .moveEmail(widget.emailId, destination); unawaited( ref.read(undoServiceProvider.notifier).pushAction( @@ -609,7 +662,7 @@ class _EmailDetailScreenState extends ConsumerState { type: UndoType.move, emailIds: [widget.emailId], sourceMailboxPath: header.mailboxPath, - destinationMailboxPath: chosen, + destinationMailboxPath: destination, ), ), ); diff --git a/test/backend/account_sync_manager_test.dart b/test/backend/account_sync_manager_test.dart index 4aafb9c..8d63b2b 100644 --- a/test/backend/account_sync_manager_test.dart +++ b/test/backend/account_sync_manager_test.dart @@ -169,6 +169,15 @@ class _FakeMailboxes implements MailboxRepository { unreadCount: 0, totalCount: 0, ); + @override + Future createMailbox(String accountId, String name) async => Mailbox( + id: '$accountId:$name', + accountId: accountId, + path: name, + name: name, + unreadCount: 0, + totalCount: 0, + ); } class _FakeEmails implements EmailRepository { diff --git a/test/unit/account_sync_manager_test.dart b/test/unit/account_sync_manager_test.dart index 1b17daa..c8d4261 100644 --- a/test/unit/account_sync_manager_test.dart +++ b/test/unit/account_sync_manager_test.dart @@ -239,6 +239,15 @@ class FakeMailboxRepositoryWithInbox implements MailboxRepository { unreadCount: 0, totalCount: 0, ); + @override + Future createMailbox(String accountId, String name) async => Mailbox( + id: '$accountId:$name', + accountId: accountId, + path: name, + name: name, + unreadCount: 0, + totalCount: 0, + ); } class _AccountRepositoryWithMissingPlugin implements AccountRepository { diff --git a/test/unit/account_sync_manager_test.mocks.dart b/test/unit/account_sync_manager_test.mocks.dart index 481ba08..100fc60 100644 --- a/test/unit/account_sync_manager_test.mocks.dart +++ b/test/unit/account_sync_manager_test.mocks.dart @@ -235,6 +235,31 @@ class MockMailboxRepository extends _i1.Mock implements _i8.MailboxRepository { ), )), ) as _i5.Future<_i2.Mailbox>); + + @override + _i5.Future<_i2.Mailbox> createMailbox( + String? accountId, + String? name, + ) => + (super.noSuchMethod( + Invocation.method( + #createMailbox, + [ + accountId, + name, + ], + ), + returnValue: _i5.Future<_i2.Mailbox>.value(_FakeMailbox_0( + this, + Invocation.method( + #createMailbox, + [ + accountId, + name, + ], + ), + )), + ) as _i5.Future<_i2.Mailbox>); } /// A class which mocks [EmailRepository]. diff --git a/test/unit/reliability_runner_check_now_test.dart b/test/unit/reliability_runner_check_now_test.dart index 86fe5af..899cb32 100644 --- a/test/unit/reliability_runner_check_now_test.dart +++ b/test/unit/reliability_runner_check_now_test.dart @@ -77,6 +77,15 @@ class _FakeMailboxes implements MailboxRepository { unreadCount: 0, totalCount: 0, ); + @override + Future createMailbox(String accountId, String name) async => Mailbox( + id: '$accountId:$name', + accountId: accountId, + path: name, + name: name, + unreadCount: 0, + totalCount: 0, + ); } class _FakeEmails implements EmailRepository { diff --git a/test/unit/reliability_runner_test.dart b/test/unit/reliability_runner_test.dart index f7a8b03..180ab39 100644 --- a/test/unit/reliability_runner_test.dart +++ b/test/unit/reliability_runner_test.dart @@ -67,6 +67,15 @@ class _FakeMailboxes implements MailboxRepository { unreadCount: 0, totalCount: 0, ); + @override + Future createMailbox(String accountId, String name) async => Mailbox( + id: '$accountId:$name', + accountId: accountId, + path: name, + name: name, + unreadCount: 0, + totalCount: 0, + ); } class _CountingEmails implements EmailRepository { diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index 64acf9d..bd90316 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -192,6 +192,20 @@ class FakeMailboxRepository implements MailboxRepository { _mailboxes.add(mailbox); return mailbox; } + + @override + Future createMailbox(String accountId, String name) async { + final mailbox = Mailbox( + id: '$accountId:$name', + accountId: accountId, + path: name, + name: name, + unreadCount: 0, + totalCount: 0, + ); + _mailboxes.add(mailbox); + return mailbox; + } } class FakeEmailRepository implements EmailRepository {