From f1ec1d7324f0d96ede698139e924f6b13382d8a5 Mon Sep 17 00:00:00 2001 From: Florent Azavant Date: Fri, 29 Nov 2024 14:29:59 +0100 Subject: [PATCH] TF-3189 new option to copy a folder's subaddress --- assets/images/ic_copy.svg | 3 + .../presentation/resources/image_paths.dart | 1 + .../base/base_mailbox_controller.dart | 12 ++- .../empty_folder_name_exception.dart | 9 +++ .../invalid_mail_format_exception.dart | 9 +++ .../presentation/mailbox_controller.dart | 29 +++++++ .../mixin/mailbox_widget_mixin.dart | 2 + .../presentation/model/mailbox_actions.dart | 6 ++ .../presentation/model/mailbox_tree.dart | 6 +- lib/l10n/intl_messages.arb | 12 +++ lib/main/localizations/app_localizations.dart | 20 +++++ .../mailbox_dashboard_controller_test.dart | 79 +++++++++++++++++++ 12 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 assets/images/ic_copy.svg create mode 100644 lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart create mode 100644 lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart diff --git a/assets/images/ic_copy.svg b/assets/images/ic_copy.svg new file mode 100644 index 000000000..558c78aa8 --- /dev/null +++ b/assets/images/ic_copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index a7c312c35..7f1acc88b 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -223,6 +223,7 @@ class ImagePaths { String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg'); String get icHelp => _getImagePath('ic_help.svg'); String get icDownloadAll => _getImagePath('ic_download_all.svg'); + String get icCopy => _getImagePath('ic_copy.svg'); String get icSubaddressingAllow => _getImagePath('ic_subaddressing_allow.svg'); String get icSubaddressingDisallow => _getImagePath('ic_subaddressing_disallow.svg'); diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index cf1f30969..942ba18fd 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -216,14 +216,18 @@ abstract class BaseMailboxController extends BaseController { return teamMailboxesTree.value.findNode((node) => node.item.id == mailboxId); } - String? findNodePath(MailboxId mailboxId) { - var mailboxNodePath = defaultMailboxTree.value.getNodePath(mailboxId) - ?? personalMailboxTree.value.getNodePath(mailboxId) - ?? teamMailboxesTree.value.getNodePath(mailboxId); + String? findNodePathWithSeparator(MailboxId mailboxId, String pathSeparator) { + var mailboxNodePath = defaultMailboxTree.value.getNodePath(mailboxId, pathSeparator) + ?? personalMailboxTree.value.getNodePath(mailboxId, pathSeparator) + ?? teamMailboxesTree.value.getNodePath(mailboxId, pathSeparator); log('BaseMailboxController::findNodePath():mailboxNodePath: $mailboxNodePath'); return mailboxNodePath; } + String? findNodePath(MailboxId mailboxId) { + return findNodePathWithSeparator(mailboxId, '/'); + } + MailboxNode? findMailboxNodeByRole(Role role) { final mailboxNode = defaultMailboxTree.value.findNode((node) => node.item.role == role); return mailboxNode; diff --git a/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart b/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart new file mode 100644 index 000000000..5a3ff99dd --- /dev/null +++ b/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart @@ -0,0 +1,9 @@ + +class EmptyFolderNameException implements Exception { + final String folderName; + + EmptyFolderNameException(this.folderName); + + @override + String toString() => 'EmptyFolderNameException: Folder name should not be empty: $folderName'; +} diff --git a/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart b/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart new file mode 100644 index 000000000..bd15b87b2 --- /dev/null +++ b/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart @@ -0,0 +1,9 @@ + +class InvalidMailFormatException implements Exception { + final String mail; + + InvalidMailFormatException(this.mail); + + @override + String toString() => 'InvalidMailFormatException: $mail'; +} diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 1958f89d8..b1b719aba 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -5,6 +5,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -30,6 +31,8 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox/domain/constants/mailbox_constants.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/empty_folder_name_exception.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/exceptions/set_mailbox_name_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/exceptions/null_session_or_accountid_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; @@ -1248,6 +1251,14 @@ class MailboxController extends BaseMailboxController case MailboxActions.openInNewTab: openMailboxInNewTabAction(mailbox); break; + case MailboxActions.copySubaddress: + try{ + final subaddress = getSubaddress(mailboxDashBoardController.userEmail, findNodePathWithSeparator(mailbox.id, '.')!); + copySubaddressAction(context, subaddress); + } catch (error) { + appToast.showToastErrorMessage(context, AppLocalizations.of(context).errorWhileFetchingSubaddress); + } + break; case MailboxActions.disableSpamReport: case MailboxActions.enableSpamReport: mailboxDashBoardController.storeSpamReportStateAction(); @@ -1632,4 +1643,22 @@ class MailboxController extends BaseMailboxController return session.getContactSupportCapability(accountId); } + + void copySubaddressAction(BuildContext context, String subaddress) { + Clipboard.setData(ClipboardData(text: subaddress)); + appToast.showToastSuccessMessage(context, AppLocalizations.of(context).emailSubaddressCopiedToClipboard); + } + + String getSubaddress(String userEmail, String folderName) { + if (folderName.isEmpty) { + throw EmptyFolderNameException(folderName); + } + + final atIndex = userEmail.indexOf('@'); + if (atIndex <= 0 || atIndex == userEmail.length - 1) { + throw InvalidMailFormatException(userEmail); + } + + return '${userEmail.substring(0, atIndex)}+$folderName@${userEmail.substring(atIndex + 1)}'; + } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart index d1abfa29b..06f2b5fa7 100644 --- a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart +++ b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart @@ -66,6 +66,8 @@ mixin MailboxWidgetMixin { MailboxActions.disallowSubaddressing else MailboxActions.allowSubaddressing, + if (mailbox.isSubaddressingAllowed) + MailboxActions.copySubaddress, if (mailbox.isSubscribedMailbox) MailboxActions.disableMailbox else diff --git a/lib/features/mailbox/presentation/model/mailbox_actions.dart b/lib/features/mailbox/presentation/model/mailbox_actions.dart index 2cb018b00..e010aec1b 100644 --- a/lib/features/mailbox/presentation/model/mailbox_actions.dart +++ b/lib/features/mailbox/presentation/model/mailbox_actions.dart @@ -16,6 +16,7 @@ enum MailboxActions { markAsRead, selectForRuleAction, openInNewTab, + copySubaddress, disableSpamReport, enableSpamReport, disableMailbox, @@ -50,6 +51,8 @@ extension MailboxActionsExtension on MailboxActions { switch(this) { case MailboxActions.openInNewTab: return AppLocalizations.of(context).openInNewTab; + case MailboxActions.copySubaddress: + return AppLocalizations.of(context).copySubaddress; case MailboxActions.newSubfolder: return AppLocalizations.of(context).newSubfolder; case MailboxActions.disableSpamReport: @@ -89,6 +92,8 @@ extension MailboxActionsExtension on MailboxActions { switch(this) { case MailboxActions.openInNewTab: return imagePaths.icOpenInNewTab; + case MailboxActions.copySubaddress: + return imagePaths.icCopy; case MailboxActions.newSubfolder: return imagePaths.icAddNewFolder; case MailboxActions.disableSpamReport: @@ -181,6 +186,7 @@ extension MailboxActionsExtension on MailboxActions { ContextMenuItemState getContextMenuItemState(PresentationMailbox mailbox) { switch(this) { case MailboxActions.openInNewTab: + case MailboxActions.copySubaddress: case MailboxActions.newSubfolder: case MailboxActions.disableSpamReport: case MailboxActions.enableSpamReport: diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 2b1b54ecc..d13953a5b 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -121,7 +121,7 @@ class MailboxTree with EquatableMixin { return false; } - String? getNodePath(MailboxId mailboxId) { + String? getNodePath(MailboxId mailboxId, String pathSeparator) { final matchedNode = findNode((node) => node.item.id == mailboxId); if (matchedNode == null) { return null; @@ -141,9 +141,9 @@ class MailboxTree with EquatableMixin { break; } if (currentContext != null) { - path = '${parentNode.item.getDisplayName(currentContext!)}/$path'; + path = '${parentNode.item.getDisplayName(currentContext!)}$pathSeparator$path'; } else { - path = '${parentNode.item.name?.name}/$path'; + path = '${parentNode.item.name?.name}$pathSeparator$path'; } parentId = parentNode.item.parentId; } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 9b2862ae5..6c4ed7bb7 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -784,6 +784,12 @@ "placeholders_order": [], "placeholders": {} }, + "emailSubaddressCopiedToClipboard": "Email subaddress copied to clipboard", + "@emailSubaddressCopiedToClipboard": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "minimize": "Minimize", "@minimize": { "type": "text", @@ -2592,6 +2598,12 @@ "placeholders_order": [], "placeholders": {} }, + "copySubaddress": "Copy subaddress", + "@copySubaddress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "regards": "Regards", "@regards": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5c36da0f7..214e47a57 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -800,6 +800,12 @@ class AppLocalizations { name: 'email_address_copied_to_clipboard'); } + String get emailSubaddressCopiedToClipboard { + return Intl.message( + 'Email subaddress copied to clipboard', + name: 'emailSubaddressCopiedToClipboard'); + } + String get minimize { return Intl.message( 'Minimize', @@ -2654,6 +2660,13 @@ class AppLocalizations { ); } + String get copySubaddress { + return Intl.message( + 'Copy subaddress', + name: 'copySubaddress', + ); + } + String get regards { return Intl.message( 'Regards', @@ -3005,6 +3018,13 @@ class AppLocalizations { ); } + String get errorWhileFetchingSubaddress { + return Intl.message( + 'Error while fetching the subaddress', + name: 'errorWhileFetchingSubaddress', + ); + } + String get connectedToTheInternet { return Intl.message( 'Connected to the internet', diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index 7401027b1..ae1465991 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -36,6 +36,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oi import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/empty_folder_name_exception.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; @@ -580,4 +582,81 @@ void main() { expect(spamId, isNull); }); }); + + group('getSubaddress:test', () { + setUp(() { + getEmailsInMailboxInteractor = MockGetEmailsInMailboxInteractor(); + + when(emailReceiveManager.pendingSharedFileInfo).thenAnswer((_) => BehaviorSubject.seeded([])); + + Get.put(mailboxDashboardController); + mailboxDashboardController.onReady(); + + mailboxController = MailboxController( + createNewMailboxInteractor, + deleteMultipleMailboxInteractor, + renameMailboxInteractor, + moveMailboxInteractor, + subscribeMailboxInteractor, + subscribeMultipleMailboxInteractor, + subaddressingInteractor, + createDefaultMailboxInteractor, + treeBuilder, + verifyNameInteractor, + getAllMailboxInteractor, + refreshAllMailboxInteractor); + mailboxController.onReady(); + + threadController = ThreadController( + getEmailsInMailboxInteractor, + refreshChangesEmailsInMailboxInteractor, + loadMoreEmailsInMailboxInteractor, + searchEmailInteractor, + searchMoreEmailInteractor, + getEmailByIdInteractor); + Get.put(threadController); + + advancedFilterController = AdvancedFilterController(); + + mailboxDashboardController.sessionCurrent = testSession; + mailboxDashboardController.filterMessageOption.value = FilterMessageOption.all; + mailboxDashboardController.accountId.value = testAccountId; + }); + + test('should return subaddress with valid email and folder name', () { + const String userEmail = 'user@example.com'; + const String folderName = 'folder'; + final result = mailboxController.getSubaddress(userEmail, folderName); + + expect(result, equals('user+folder@example.com')); + }); + + test('should throw an error if empty local part', () { + const userEmail = '@example.com'; + const folderName = 'folder'; + + expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA())); + }); + + test('should throw an error if empty folder name', () { + const userEmail = 'user@example.com'; + const folderName = ''; + + expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA())); + }); + + test('should throw an error if empty domain', () { + const userEmail = 'user@'; + const folderName = 'folder'; + + expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA())); + }); + + test('should throw an error if absent `@`', () { + const userEmail = 'invalid-email-format'; + const folderName = 'folder'; + + expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA())); + }); + }); }