diff --git a/labels/test/method/get/get_label_method_test.dart b/labels/test/method/get/get_label_method_test.dart index 7277de527..02e65d36a 100644 --- a/labels/test/method/get/get_label_method_test.dart +++ b/labels/test/method/get/get_label_method_test.dart @@ -206,7 +206,7 @@ void main() { expect(parsed.notFound, isEmpty); }); - test('should throw DioException when server returns 500', () async { + test('should throw DioError when server returns 500', () async { // Arrange final dio = createDio(); final adapter = DioAdapter(dio: dio); diff --git a/labels/test/method/set/set_label_method_test.dart b/labels/test/method/set/set_label_method_test.dart index 1b8e59eb5..54c9dd2df 100644 --- a/labels/test/method/set/set_label_method_test.dart +++ b/labels/test/method/set/set_label_method_test.dart @@ -86,7 +86,7 @@ void main() { // Assert expect(parsed, isNotNull); - expect(parsed!.created![Id('4f29')]!.keyword, equals('important')); + expect(parsed!.created![Id('4f29')]!.keyword?.value, equals('important')); }); test('should process multiple created labels', () async { @@ -143,8 +143,8 @@ void main() { // Assert expect(parsed, isNotNull); expect(parsed!.created!.length, equals(2)); - expect(parsed.created![Id('A')]!.keyword, equals('tagA')); - expect(parsed.created![Id('B')]!.keyword, equals('tagB')); + expect(parsed.created![Id('A')]!.keyword?.value, equals('tagA')); + expect(parsed.created![Id('B')]!.keyword?.value, equals('tagB')); }); test('should throw DioException when backend returns 500', () async { diff --git a/lib/features/base/widget/popup_menu/hover_submenu_controller.dart b/lib/features/base/widget/popup_menu/hover_submenu_controller.dart new file mode 100644 index 000000000..07a9a71d8 --- /dev/null +++ b/lib/features/base/widget/popup_menu/hover_submenu_controller.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +class HoverSubmenuController { + HoverSubmenuController({ + this.exitDelay = const Duration(milliseconds: 120), + }); + + final Duration exitDelay; + + final ValueNotifier isHovering = ValueNotifier(false); + + int _hoverRefCount = 0; + Timer? _exitTimer; + + void enter() { + _exitTimer?.cancel(); + _hoverRefCount++; + if (!isHovering.value) { + isHovering.value = true; + } + } + + void exit() { + _hoverRefCount = (_hoverRefCount - 1).clamp(0, 999); + + if (_hoverRefCount == 0) { + _exitTimer?.cancel(); + _exitTimer = Timer(exitDelay, () { + if (_hoverRefCount == 0) { + isHovering.value = false; + } + }); + } + } + + void dispose() { + _exitTimer?.cancel(); + isHovering.dispose(); + } +} diff --git a/lib/features/base/widget/popup_menu/popup_menu_action_group_widget.dart b/lib/features/base/widget/popup_menu/popup_menu_action_group_widget.dart index a3354ec89..d09790927 100644 --- a/lib/features/base/widget/popup_menu/popup_menu_action_group_widget.dart +++ b/lib/features/base/widget/popup_menu/popup_menu_action_group_widget.dart @@ -37,6 +37,7 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin { child: PopupMenuItemActionWidget( menuAction: menuAction, menuActionClick: (menuAction) { + submenuController?.hide(); Navigator.pop(context); onActionSelected(menuAction); }, @@ -60,7 +61,11 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin { ], ]; - return openPopupMenuAction(context, position, popupMenuItems); + try { + await openPopupMenuAction(context, position, popupMenuItems); + } finally { + submenuController?.hide(); + } } void _showPopupSubmenu({ @@ -69,8 +74,9 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin { required PopupSubmenuController submenuController, required Widget submenu, }) { - final renderBox = itemKey.currentContext?.findRenderObject() as RenderBox?; - if (renderBox == null) return; + final renderObject = itemKey.currentContext?.findRenderObject(); + if (renderObject is! RenderBox) return; + final renderBox = renderObject; final offset = renderBox.localToGlobal(Offset.zero); final rect = offset & renderBox.size; diff --git a/lib/features/base/widget/popup_menu/popup_menu_item_action_widget.dart b/lib/features/base/widget/popup_menu/popup_menu_item_action_widget.dart index 752be30cb..38078e13e 100644 --- a/lib/features/base/widget/popup_menu/popup_menu_item_action_widget.dart +++ b/lib/features/base/widget/popup_menu/popup_menu_item_action_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/email/email_action_type.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_menu/hover_submenu_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; class PopupMenuItemActionWidget extends StatefulWidget { @@ -28,14 +29,15 @@ class PopupMenuItemActionWidget extends StatefulWidget { class _PopupMenuItemActionWidgetState extends State { GlobalKey? _itemKey; - ValueNotifier? _hoverItemNotifier; + HoverSubmenuController? _hoverController; @override void initState() { super.initState(); - if (widget.menuAction is PopupMenuItemEmailAction) { + if (widget.menuAction is PopupMenuItemEmailAction && + widget.menuAction.action == EmailActionType.labelAs) { _itemKey = GlobalKey(); - _hoverItemNotifier = ValueNotifier(false); + _hoverController = HoverSubmenuController(); } } @@ -133,9 +135,8 @@ class _PopupMenuItemActionWidgetState extends State { return PointerInterceptor( child: MouseRegion( onEnter: (_) { - if (_hoverItemNotifier != null) { - _hoverItemNotifier?.value = true; - } + _hoverController?.enter(); + if (_itemKey != null) { widget.onHoverShowSubmenu?.call(_itemKey!); } else { @@ -143,9 +144,7 @@ class _PopupMenuItemActionWidgetState extends State { } }, onExit: (_) { - if (_hoverItemNotifier != null) { - _hoverItemNotifier?.value = false; - } + _hoverController?.exit(); }, child: Material( type: MaterialType.transparency, @@ -172,26 +171,23 @@ class _PopupMenuItemActionWidgetState extends State { ), if (isSelected && selectedIconWidget != null) selectedIconWidget, - if (_hoverItemNotifier != null) + if (_hoverController != null) ValueListenableBuilder( - valueListenable: _hoverItemNotifier!, - builder: (_, value, __) { - if (value) { - return Padding( - padding: - const EdgeInsetsDirectional.only(start: 16), - child: SvgPicture.asset( - specificMenuAction.hoverIcon, - width: specificMenuAction.hoverIconSize, - height: specificMenuAction.hoverIconSize, - colorFilter: specificMenuAction.hoverIconColor - .asFilter(), - fit: BoxFit.fill, - ), - ); - } else { - return const SizedBox.shrink(); - } + valueListenable: _hoverController!.isHovering, + builder: (_, isHovering, __) { + if (!isHovering) return const SizedBox.shrink(); + return Padding( + padding: + const EdgeInsetsDirectional.only(start: 16), + child: SvgPicture.asset( + specificMenuAction.hoverIcon, + width: specificMenuAction.hoverIconSize, + height: specificMenuAction.hoverIconSize, + colorFilter: specificMenuAction.hoverIconColor + .asFilter(), + fit: BoxFit.fill, + ), + ); }, ) ], @@ -210,9 +206,7 @@ class _PopupMenuItemActionWidgetState extends State { onTap: () => widget.menuAction.onClick(widget.menuActionClick), hoverColor: AppColor.popupMenuItemHovered, onHover: (_) { - if (_hoverItemNotifier != null) { - _hoverItemNotifier?.value = false; - } + _hoverController?.exit(); widget.onHoverOtherItem?.call(); }, child: Container( @@ -245,9 +239,9 @@ class _PopupMenuItemActionWidgetState extends State { @override void dispose() { - if (_hoverItemNotifier != null) { - _hoverItemNotifier?.dispose(); - _hoverItemNotifier = null; + if (_hoverController != null) { + _hoverController?.dispose(); + _hoverController = null; } super.dispose(); } diff --git a/lib/features/base/widget/popup_menu/popup_submenu_controller.dart b/lib/features/base/widget/popup_menu/popup_submenu_controller.dart index 101092667..f22df85c4 100644 --- a/lib/features/base/widget/popup_menu/popup_submenu_controller.dart +++ b/lib/features/base/widget/popup_menu/popup_submenu_controller.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; enum SubmenuDirection { left, right, auto } @@ -18,7 +20,12 @@ class PopupSubmenuController { }) { hide(); - final screenWidth = MediaQuery.of(context).size.width; + final overlayState = Overlay.maybeOf(context, rootOverlay: true); + if (overlayState == null) return; + + final mediaSize = MediaQuery.sizeOf(context); + final screenWidth = mediaSize.width; + final screenHeight = mediaSize.height; final rightPosition = anchor.right + offset; final leftPosition = anchor.left - submenuWidth - offset; @@ -42,10 +49,16 @@ class PopupSubmenuController { } } + final clampedLeft = finalLeft + .clamp(0.0, math.max(0.0, screenWidth - submenuWidth)) + .toDouble(); + final availableHeight = math.max(0.0, screenHeight - anchor.top); + final finalHeight = math.min(submenuMaxHeight, availableHeight); + _submenuEntry = OverlayEntry( builder: (_) { return PositionedDirectional( - start: finalLeft, + start: clampedLeft, top: anchor.top, child: MouseRegion( onExit: (_) => hide(), @@ -57,7 +70,7 @@ class PopupSubmenuController { ), child: SizedBox( width: submenuWidth, - height: submenuMaxHeight, + height: finalHeight, child: submenu, ), ), @@ -66,7 +79,7 @@ class PopupSubmenuController { }, ); - Overlay.maybeOf(context)?.insert(_submenuEntry!); + overlayState.insert(_submenuEntry!); } void hide() { diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 0e61281f0..41c3682f6 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -55,12 +55,14 @@ class ComposerView extends GetWidget { @override Widget build(BuildContext context) { final iframeOverlay = Obx(() { + final dialogRouter = DialogRouter(); + bool isOverlayEnabled = controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame || MessageDialogActionManager().isDialogOpened || EmailActionReactor.isDialogOpened || ColorDialogPicker().isOpened.isTrue || - DialogRouter().isRuleFilterDialogOpened.isTrue || - DialogRouter().isDialogOpened; + dialogRouter.isRuleFilterDialogOpened.isTrue || + dialogRouter.isDialogOpened; if (isOverlayEnabled) { return Positioned.fill( diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index a4544dac7..c305a3318 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -206,6 +206,7 @@ abstract class EmailDataSource { Future generateEntireMessageAsDocument(ViewEntireMessageRequest entireMessageRequest); Future addLabelToEmail( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index a67d2da51..17c9f5a5e 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -556,12 +556,18 @@ class EmailDataSourceImpl extends EmailDataSource { @override Future addLabelToEmail( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, ) { return Future.sync(() async { - return await emailAPI.addLabelToEmail(accountId, emailId, labelKeyword); + return await emailAPI.addLabelToEmail( + session, + accountId, + emailId, + labelKeyword, + ); }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index 8f1ef68ea..1ffdecac2 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -577,7 +577,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future addLabelToEmail(AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { + Future addLabelToEmail(Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart index cf2608816..2038731d3 100644 --- a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart @@ -353,7 +353,7 @@ class EmailLocalStorageDataSourceImpl extends EmailDataSource { } @override - Future addLabelToEmail(AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { + Future addLabelToEmail(Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart index 6b30fd585..396f4ed76 100644 --- a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart @@ -267,7 +267,7 @@ class EmailSessionStorageDatasourceImpl extends EmailDataSource { } @override - Future addLabelToEmail(AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { + Future addLabelToEmail(Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword) { throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 5589149b9..4c0968ed5 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -938,6 +938,7 @@ class EmailAPI with HandleSetErrorMixin, MailAPIMixin { } Future addLabelToEmail( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, @@ -948,8 +949,9 @@ class EmailAPI with HandleSetErrorMixin, MailAPIMixin { final builder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final invocation = builder.invocation(method); - final result = - await (builder..usings(method.requiredCapabilities)).build().execute(); + final capabilities = method.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (builder..usings(capabilities)).build().execute(); final response = result.parse( invocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c4ba82573..66ce29367 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -483,11 +483,13 @@ class EmailRepositoryImpl extends EmailRepository { @override Future addLabelToEmail( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, ) { return emailDataSource[DataSourceType.network]!.addLabelToEmail( + session, accountId, emailId, labelKeyword, diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index ee8ab592e..552dcb81d 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -163,6 +163,7 @@ abstract class EmailRepository { Future generateEntireMessageAsDocument(ViewEntireMessageRequest entireMessageRequest); Future addLabelToEmail( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, diff --git a/lib/features/email/domain/usecases/add_a_label_to_an_email_interactor.dart b/lib/features/email/domain/usecases/add_a_label_to_an_email_interactor.dart index a583013a8..4c2d8a0b2 100644 --- a/lib/features/email/domain/usecases/add_a_label_to_an_email_interactor.dart +++ b/lib/features/email/domain/usecases/add_a_label_to_an_email_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -13,6 +14,7 @@ class AddALabelToAnEmailInteractor { AddALabelToAnEmailInteractor(this._emailRepository); Stream> execute( + Session session, AccountId accountId, EmailId emailId, KeyWordIdentifier labelKeyword, @@ -21,6 +23,7 @@ class AddALabelToAnEmailInteractor { try { yield Right(AddingALabelToAnEmail()); await _emailRepository.addLabelToEmail( + session, accountId, emailId, labelKeyword, diff --git a/lib/features/email/presentation/bindings/email_interactor_bindings.dart b/lib/features/email/presentation/bindings/email_interactor_bindings.dart index 6c4e2d1ba..9e4e3a543 100644 --- a/lib/features/email/presentation/bindings/email_interactor_bindings.dart +++ b/lib/features/email/presentation/bindings/email_interactor_bindings.dart @@ -118,7 +118,7 @@ class EmailInteractorBindings extends InteractorsBindings { Get.find(), )); } - Get.lazyPut( + Get.lazyPut( () => AddALabelToAnEmailInteractor(Get.find()), ); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 032397690..62700a798 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -221,7 +221,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { @override void handleSuccessViewState(Success success) { - log('SingleEmailController::handleSuccessViewState(): $success'); if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); } else if (success is GetEmailContentFromCacheSuccess) { @@ -255,7 +254,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { @override void handleFailureViewState(Failure failure) { - logError('SingleEmailController::handleFailureViewState(): $failure'); if (failure is MarkAsEmailReadFailure) { _handleMarkAsEmailReadFailure(failure); } else if (failure is ParseCalendarEventFailure) { @@ -899,6 +897,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { pressEmailAction(actionType, presentationEmail); break; case EmailActionType.labelAs: + if (!isLabelFeatureEnabled) return; openAddLabelToEmailDialogModal(presentationEmail); break; default: diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 0772906d5..058fb34d4 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -596,12 +596,13 @@ class EmailView extends GetWidget { ), ), Obx(() { + final dialogRouter = DialogRouter(); bool isOverlayEnabled = controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame || MessageDialogActionManager().isDialogOpened || EmailActionReactor.isDialogOpened || ColorDialogPicker().isOpened.isTrue || - DialogRouter().isRuleFilterDialogOpened.isTrue || - DialogRouter().isDialogOpened; + dialogRouter.isRuleFilterDialogOpened.isTrue || + dialogRouter.isDialogOpened; if (isOverlayEnabled) { return Positioned.fill( diff --git a/lib/features/email/presentation/extensions/email_extension.dart b/lib/features/email/presentation/extensions/email_extension.dart index 123e24c5a..b0aa6475b 100644 --- a/lib/features/email/presentation/extensions/email_extension.dart +++ b/lib/features/email/presentation/extensions/email_extension.dart @@ -1,11 +1,13 @@ import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/list_email_header_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/smime_signature_status.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/smime_signature_constant.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; extension EmailExtension on Email { @@ -39,7 +41,7 @@ extension EmailExtension on Email { bool fromMe(String ownEmailAddress) { return from?.any( - (emailAdress) => emailAdress.email == ownEmailAddress + (emailAddress) => emailAddress.email == ownEmailAddress ) == true; } @@ -50,7 +52,15 @@ extension EmailExtension on Email { ...bcc ?? {}, }; return recipients.any( - (emailAdress) => emailAdress.email == ownEmailAddress + (emailAddress) => emailAddress.email == ownEmailAddress ) == true; } + + Email toggleKeyword(KeyWordIdentifier keyword, bool remove) { + return copyWith( + keywords: remove + ? keywords.withoutKeyword(keyword) + : keywords.withKeyword(keyword), + ); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/email_loaded_extension.dart b/lib/features/email/presentation/extensions/email_loaded_extension.dart index 033d82d38..982cdbb1c 100644 --- a/lib/features/email/presentation/extensions/email_loaded_extension.dart +++ b/lib/features/email/presentation/extensions/email_loaded_extension.dart @@ -1,31 +1,20 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; -import 'package:model/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; -import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; extension EmailLoadedExtension on EmailLoaded { - EmailLoaded addEmailKeyword({ + EmailLoaded toggleEmailKeyword({ required EmailId emailId, required KeyWordIdentifier keyword, + required bool remove, }) { - if (emailCurrent == null || emailCurrent?.id != emailId) { + final current = emailCurrent; + if (current == null || current.id != emailId) { return this; } - final newKeyword = emailCurrent?.keywords?.withKeyword(keyword); - final updatedEmail = emailCurrent?.copyWith(keywords: newKeyword); - return copyWith(emailCurrent: updatedEmail); - } - - EmailLoaded removeEmailKeyword({ - required EmailId emailId, - required KeyWordIdentifier keyword, - }) { - if (emailCurrent == null || emailCurrent?.id != emailId) { - return this; - } - final newKeyword = emailCurrent?.keywords?.withoutKeyword(keyword); - final updatedEmail = emailCurrent?.copyWith(keywords: newKeyword); - return copyWith(emailCurrent: updatedEmail); + return copyWith( + emailCurrent: current.toggleKeyword(keyword, remove), + ); } } diff --git a/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart b/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart index 1f546336b..5caf995d3 100644 --- a/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart +++ b/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart @@ -2,6 +2,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:labels/extensions/label_extension.dart'; @@ -29,8 +30,10 @@ extension HandleLabelForEmailExtension on SingleEmailController { void toggleLabelToEmail(EmailId emailId, Label label, bool isSelected) { if (isSelected) { final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; _addALabelToAnEmail( + session: session, accountId: accountId, emailId: emailId, label: label, @@ -39,12 +42,25 @@ extension HandleLabelForEmailExtension on SingleEmailController { } void _addALabelToAnEmail({ + required Session? session, required AccountId? accountId, required Label label, required EmailId emailId, }) { final labelDisplay = label.safeDisplayName; + if (session == null) { + consumeState( + Stream.value( + Left(AddALabelToAnEmailFailure( + exception: NotFoundSessionException(), + labelDisplay: labelDisplay, + )), + ), + ); + return; + } + if (accountId == null) { consumeState( Stream.value( @@ -71,6 +87,7 @@ extension HandleLabelForEmailExtension on SingleEmailController { } consumeState(addALabelToAnEmailInteractor.execute( + session, accountId, emailId, labelKeyword, @@ -117,24 +134,27 @@ extension HandleLabelForEmailExtension on SingleEmailController { final controller = threadDetailController; if (controller != null) { controller.emailIdsPresentation.value = - controller.emailIdsPresentation.addEmailKeywordById( + controller.emailIdsPresentation.toggleEmailKeywordById( emailId: emailId, keyword: labelKeyword, + remove: false, ); controller.emailsInThreadDetailInfo.value = - controller.emailsInThreadDetailInfo.addEmailKeywordById( + controller.emailsInThreadDetailInfo.toggleEmailKeywordById( emailId: emailId, keyword: labelKeyword, + remove: false, ); } } final emailLoaded = currentEmailLoaded.value; if (emailLoaded != null && emailLoaded.emailCurrent?.id == emailId) { - currentEmailLoaded.value = emailLoaded.addEmailKeyword( + currentEmailLoaded.value = emailLoaded.toggleEmailKeyword( emailId: emailId, keyword: labelKeyword, + remove: false, ); } @@ -148,6 +168,7 @@ extension HandleLabelForEmailExtension on SingleEmailController { } Future openAddLabelToEmailDialogModal(PresentationEmail email) async { + if (!isLabelFeatureEnabled) return; final labels = mailboxDashBoardController.labelController.labels; final emailLabels = email.getLabelList(labels); final emailId = email.id; diff --git a/lib/features/email/presentation/extensions/presentation_email_extension.dart b/lib/features/email/presentation/extensions/presentation_email_extension.dart index daad75efe..651b4631d 100644 --- a/lib/features/email/presentation/extensions/presentation_email_extension.dart +++ b/lib/features/email/presentation/extensions/presentation_email_extension.dart @@ -170,15 +170,13 @@ extension PresentationEmailExtension on PresentationEmail { .toList(); } - PresentationEmail addKeyword(KeyWordIdentifier keyword) { + PresentationEmail toggleKeyword(KeyWordIdentifier keyword, bool remove) { return copyWith( - keywords: keywords.withKeyword(keyword), - ); - } - - PresentationEmail removeKeyword(KeyWordIdentifier keyword) { - return copyWith( - keywords: keywords.withoutKeyword(keyword), - ); + keywords: remove + ? keywords.withoutKeyword(keyword) + : keywords.withKeyword(keyword), + ) + ..searchSnippetSubject = searchSnippetSubject + ..searchSnippetPreview = searchSnippetPreview; } } diff --git a/lib/features/labels/domain/exceptions/label_exceptions.dart b/lib/features/labels/domain/exceptions/label_exceptions.dart index 5435672f9..c361c1e31 100644 --- a/lib/features/labels/domain/exceptions/label_exceptions.dart +++ b/lib/features/labels/domain/exceptions/label_exceptions.dart @@ -1 +1,4 @@ -class LabelKeywordIsNull implements Exception {} +class LabelKeywordIsNull implements Exception { + @override + String toString() => 'Label keyword is null'; +} diff --git a/lib/features/labels/presentation/label_controller.dart b/lib/features/labels/presentation/label_controller.dart index 5f5736683..62be0b758 100644 --- a/lib/features/labels/presentation/label_controller.dart +++ b/lib/features/labels/presentation/label_controller.dart @@ -71,6 +71,13 @@ class LabelController extends BaseController { } Future openCreateNewLabelModal(AccountId? accountId) async { + if (accountId == null) { + consumeState( + Stream.value(Left(CreateNewLabelFailure(NotFoundAccountIdException()))), + ); + return; + } + await DialogRouter().openDialogModal( child: CreateNewLabelModal( labels: labels, @@ -80,13 +87,9 @@ class LabelController extends BaseController { ); } - void _createNewLabel(AccountId? accountId, Label label) { + void _createNewLabel(AccountId accountId, Label label) { log('LabelController::_createNewLabel:Label: $label'); - if (accountId == null) { - consumeState( - Stream.value(Left(CreateNewLabelFailure(NotFoundAccountIdException()))), - ); - } else if (_createNewLabelInteractor == null) { + if (_createNewLabelInteractor == null) { consumeState( Stream.value(Left(CreateNewLabelFailure(InteractorNotInitialized()))), ); diff --git a/lib/features/labels/presentation/widgets/add_label_to_email_modal.dart b/lib/features/labels/presentation/widgets/add_label_to_email_modal.dart index ee9e5cd07..40a884df8 100644 --- a/lib/features/labels/presentation/widgets/add_label_to_email_modal.dart +++ b/lib/features/labels/presentation/widgets/add_label_to_email_modal.dart @@ -12,7 +12,7 @@ import 'package:tmail_ui_user/features/labels/presentation/widgets/label_item_co import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -typedef OnAddLabelToEmailCallback = Function( +typedef OnAddLabelToEmailCallback = void Function( EmailId emailId, Label label, bool isSelected, diff --git a/lib/features/manage_account/presentation/identities/widgets/signature_builder.dart b/lib/features/manage_account/presentation/identities/widgets/signature_builder.dart index 5db2ce1a1..a6e3e502b 100644 --- a/lib/features/manage_account/presentation/identities/widgets/signature_builder.dart +++ b/lib/features/manage_account/presentation/identities/widgets/signature_builder.dart @@ -29,9 +29,10 @@ class SignatureBuilder extends StatelessWidget { Widget build(BuildContext context) { if (PlatformInfo.isWeb) { final iframeOverlay = Obx(() { + final dialogRouter = DialogRouter(); if (MessageDialogActionManager().isDialogOpened || - DialogRouter().isDialogOpened || - DialogRouter().isRuleFilterDialogOpened.isTrue) { + dialogRouter.isDialogOpened || + dialogRouter.isRuleFilterDialogOpened.isTrue) { return Positioned.fill( child: PointerInterceptor( child: const SizedBox.expand(), diff --git a/lib/features/thread/domain/extensions/presentation_email_map_extension.dart b/lib/features/thread/domain/extensions/presentation_email_map_extension.dart index cf1977152..962b84ef2 100644 --- a/lib/features/thread/domain/extensions/presentation_email_map_extension.dart +++ b/lib/features/thread/domain/extensions/presentation_email_map_extension.dart @@ -4,27 +4,16 @@ import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; extension PresentationEmailMapExtension on Map { - Map addEmailKeywordById({ + Map toggleEmailKeywordById({ required EmailId emailId, required KeyWordIdentifier keyword, + required bool remove, }) { - return map((id, email) { - if (id != emailId || email == null) { - return MapEntry(id, email); - } - return MapEntry(id, email.addKeyword(keyword)); - }); - } - - Map removeEmailKeywordById({ - required EmailId emailId, - required KeyWordIdentifier keyword, - }) { - return map((id, email) { - if (id != emailId || email == null) { - return MapEntry(id, email); - } - return MapEntry(id, email.removeKeyword(keyword)); - }); + final newMap = Map.from(this); + final email = newMap[emailId]; + if (email != null) { + newMap[emailId] = email.toggleKeyword(keyword, remove); + } + return newMap; } } diff --git a/lib/features/thread_detail/domain/extensions/email_in_thread_detail_info_extension.dart b/lib/features/thread_detail/domain/extensions/email_in_thread_detail_info_extension.dart new file mode 100644 index 000000000..09f858986 --- /dev/null +++ b/lib/features/thread_detail/domain/extensions/email_in_thread_detail_info_extension.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart'; + +extension EmailInThreadDetailInfoExtension on EmailInThreadDetailInfo { + EmailInThreadDetailInfo toggleKeyword( + KeyWordIdentifier keyword, + bool remove, + ) { + return copyWith( + keywords: remove + ? keywords.withoutKeyword(keyword) + : keywords.withKeyword(keyword), + ); + } +} diff --git a/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart b/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart index 950f8874d..b7f7e831f 100644 --- a/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart +++ b/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; -import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; +import 'package:tmail_ui_user/features/thread_detail/domain/extensions/email_in_thread_detail_info_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart'; extension ListEmailInThreadDetailInfoExtension on List { @@ -10,29 +10,16 @@ extension ListEmailInThreadDetailInfoExtension on List .map((email) => email.emailId) .toList(); - List addEmailKeywordById({ + List toggleEmailKeywordById({ required EmailId emailId, required KeyWordIdentifier keyword, + required bool remove, }) { - return map((email) { - if (email.emailId != emailId) { - return email; + return map((emailInfo) { + if (emailInfo.emailId != emailId) { + return emailInfo; } - final newKeywords = email.keywords.withKeyword(keyword); - return email.copyWith(keywords: newKeywords); - }).toList(); - } - - List removeEmailKeywordById({ - required EmailId emailId, - required KeyWordIdentifier keyword, - }) { - return map((email) { - if (email.emailId != emailId) { - return email; - } - final newKeywords = email.keywords.withoutKeyword(keyword); - return email.copyWith(keywords: newKeywords); + return emailInfo.toggleKeyword(keyword, remove); }).toList(); } } \ No newline at end of file diff --git a/lib/main/routes/dialog_router.dart b/lib/main/routes/dialog_router.dart index f260a90f9..44d4ed5d6 100644 --- a/lib/main/routes/dialog_router.dart +++ b/lib/main/routes/dialog_router.dart @@ -60,7 +60,9 @@ class DialogRouter { MailboxCreatorBindings().dependencies(); break; case AppRoutes.rulesFilterCreator: - isRuleFilterDialogOpened.value = true; + if (PlatformInfo.isWeb) { + isRuleFilterDialogOpened.value = true; + } RulesFilterCreatorBindings().dependencies(); break; case AppRoutes.identityCreator: @@ -107,7 +109,7 @@ class DialogRouter { await Get.generalDialog( barrierDismissible: true, - barrierLabel: dialogLabel, + barrierLabel: dialogLabel ?? 'dialog-modal', pageBuilder: (_, __, ___) => child, ).whenComplete(() { if (PlatformInfo.isWeb) { diff --git a/model/lib/extensions/keyword_identifier_extension.dart b/model/lib/extensions/keyword_identifier_extension.dart index fb611e7df..3da71e49d 100644 --- a/model/lib/extensions/keyword_identifier_extension.dart +++ b/model/lib/extensions/keyword_identifier_extension.dart @@ -15,13 +15,11 @@ extension KeyWordIdentifierExtension on KeyWordIdentifier { } PatchObject generateReadActionPath(ReadActions action) { - final isRead = action == ReadActions.markAsRead; - return _boolPatch(isRead ? true : null); + return _boolPatch(action == ReadActions.markAsRead ? true : null); } PatchObject generateMarkStarActionPath(MarkStarAction action) { - final isStar = action == MarkStarAction.markStar; - return _boolPatch(isStar ? true : null); + return _boolPatch(action == MarkStarAction.markStar ? true : null); } PatchObject generateAnsweredActionPath() => _boolPatch(true);