diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index 96860ec6b..6e67a9359 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -135,6 +135,8 @@ extension EmailActionTypeExtension on EmailActionType { String getIcon(ImagePaths imagePaths) { switch(this) { + case EmailActionType.markAsRead: + return imagePaths.icRead; case EmailActionType.markAsUnread: return imagePaths.icUnreadEmail; case EmailActionType.unSpam: @@ -177,6 +179,8 @@ extension EmailActionTypeExtension on EmailActionType { String getTitle(AppLocalizations appLocalizations) { switch(this) { + case EmailActionType.markAsRead: + return AppLocalizations.of(context).mark_as_read; case EmailActionType.markAsUnread: return appLocalizations.mark_as_unread; case EmailActionType.unSpam: diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 0d92fe4da..0c227df48 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -132,6 +132,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/da import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -276,7 +277,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (success is GetEmailContentFromCacheSuccess) { _getEmailContentOfflineSuccess(success); } else if (success is MarkAsEmailReadSuccess) { - _handleMarkAsEmailReadCompleted(success.readActions); + _handleMarkAsEmailReadCompleted(success); } else if (success is ExportAttachmentSuccess) { _exportAttachmentSuccessAction(success); } else if (success is ExportAllAttachmentsSuccess) { @@ -334,7 +335,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); if (failure is MarkAsEmailReadFailure) { - _handleMarkAsEmailReadCompleted(failure.readActions); + _handleMarkAsEmailReadFailure(failure); } else if (failure is DownloadAttachmentsFailure) { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { @@ -364,6 +365,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } + void _handleMarkAsEmailReadFailure(MarkAsEmailReadFailure failure) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).an_error_occurred, + ); + } + } + void _handleGetEmailContentFailure(GetEmailContentFailure failure) { emailLoadedViewState.value = Left(failure); showRetryToast(failure); @@ -790,16 +800,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _handleMarkAsEmailReadCompleted(ReadActions readActions) { - if (currentEmail?.id != null) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - [currentEmail!.id!], - readAction: readActions, - ); - } - if (readActions == ReadActions.markAsUnread) { - closeEmailView(context: currentContext); - } + void _handleMarkAsEmailReadCompleted(MarkAsEmailReadSuccess success) { + _threadDetailController?.markCollapsedEmailReadSuccess(success); } void downloadAttachments(BuildContext context, List attachments) async { diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index c5e7afcb5..ef157866f 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -97,6 +97,7 @@ class EmailView extends GetWidget { username: controller.session?.username, handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), additionalActions: [], + emailIsRead: presentationEmail.hasRead, ), supportBackAction: !isInsideThreadDetailView, appBarDecoration: isInsideThreadDetailView @@ -299,6 +300,7 @@ class EmailView extends GetWidget { EmailActionType.deletePermanently, ], ], + emailIsRead: presentationEmail.hasRead, ), onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, diff --git a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart index 3b8e9cc31..7ba95f653 100644 --- a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart +++ b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart @@ -516,6 +516,7 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi EmailActionType action, ) handleEmailAction, required List additionalActions, + required bool emailIsRead, }) { if (currentContext == null) return; @@ -544,7 +545,10 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi _canDeletePermanently(presentationEmail) ? EmailActionType.deletePermanently : EmailActionType.moveToTrash, - EmailActionType.markAsUnread, + if (emailIsRead) + EmailActionType.markAsUnread + else + EmailActionType.markAsRead, if (mailboxContain?.isChildOfTeamMailboxes == false) if (mailboxContain?.isSpam == true) EmailActionType.unSpam diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index ed287969e..f7245c648 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -54,7 +54,7 @@ class _EmailReceiverWidgetState extends State { if (PlatformInfo.isWeb) { if (_isDisplayAll) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: Container( @@ -147,7 +147,7 @@ class _EmailReceiverWidgetState extends State { } else { if (_isDisplayAll) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: Column( @@ -238,8 +238,11 @@ class _EmailReceiverWidgetState extends State { text: index == listEmailAddress.length - 1 ? emailAddress.asString() : '${emailAddress.asString()},', - textStyle: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400), - padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), + textStyle: ThemeUtils.textStyleHeadingHeadingSmall( + color: Colors.black, + fontWeight: FontWeight.w400, + ).copyWith(height: 17), + padding: const EdgeInsetsDirectional.symmetric(vertical: 2, horizontal: 8), backgroundColor: Colors.transparent, onTapActionCallback: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), onLongPressActionCallback: () => AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress), diff --git a/lib/features/email/presentation/widgets/email_sender_builder.dart b/lib/features/email/presentation/widgets/email_sender_builder.dart index 949c3a881..9ae11e662 100644 --- a/lib/features/email/presentation/widgets/email_sender_builder.dart +++ b/lib/features/email/presentation/widgets/email_sender_builder.dart @@ -41,9 +41,10 @@ class EmailSenderBuilder extends StatelessWidget { }, borderRadius: 8, padding: EdgeInsets.zero, - customStyle: ThemeUtils.textStyleHeadingHeadingSmall( + customStyle: ThemeUtils.textStyleHeadingH6( color: Colors.black, - ), + fontWeight: FontWeight.w500, + ).copyWith(height: 20), overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap ), diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index f8d069b5f..28071ac1c 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/icon_utils.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/utils/platform_info.dart'; @@ -41,6 +40,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { final VoidCallback? onToggleThreadDetailCollapseExpand; final OnTapAvatarActionClick? onTapAvatarActionClick; final PresentationMailbox? mailboxContain; + final bool showUnreadVisualization; const InformationSenderAndReceiverBuilder({ Key? key, @@ -59,6 +59,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { this.onToggleThreadDetailCollapseExpand, this.onTapAvatarActionClick, this.mailboxContain, + this.showUnreadVisualization = false, }) : super(key: key); @override @@ -81,12 +82,25 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Row( children: [ + if (showUnreadVisualization && + !emailSelected.hasRead && + responsiveUtils.isMobile(context)) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: SvgPicture.asset( + imagePaths.icUnreadStatus, + width: 9, + height: 9, + fit: BoxFit.fill, + ), + ), if (emailSelected.from?.isNotEmpty == true) Flexible( child: EmailSenderBuilder( @@ -124,6 +138,19 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { emailSelected: emailSelected, padding: const EdgeInsetsDirectional.only(start: 16, top: 2), ), + if (showUnreadVisualization && + !emailSelected.hasRead && + !responsiveUtils.isMobile(context)) + TMailButtonWidget.fromIcon( + icon: imagePaths.icUnreadStatus, + backgroundColor: Colors.transparent, + iconSize: 9, + margin: const EdgeInsetsDirectional.only(start: 8), + onTapActionCallback: () => onEmailActionClick?.call( + emailSelected, + EmailActionType.markAsRead, + ), + ), ], ), ), @@ -160,6 +187,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { ), ], ), + const SizedBox(height: 4), if (responsiveUtils.isMobile(context)) ReceivedTimeBuilder( emailSelected: emailSelected, diff --git a/lib/features/email/presentation/widgets/prefix_recipient_widget.dart b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart index a6593dfd7..5cf76da0a 100644 --- a/lib/features/email/presentation/widgets/prefix_recipient_widget.dart +++ b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart @@ -12,12 +12,9 @@ class PrefixRecipientWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - '${prefixEmailAddress.asName(context)}:', - style: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400), - ), + return Text( + '${prefixEmailAddress.asName(context)}:', + style: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400), ); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart index b8e0fcdda..2ccccd715 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart @@ -92,6 +92,7 @@ extension GetThreadDetailEmailViews on ThreadDetailController { EmailActionType.deletePermanently, ], ], + emailIsRead: presentationEmail.hasRead, ), openEmailAddressDetailAction: (_, emailAddress) { openEmailAddressDetailAction(emailAddress); diff --git a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart index e9023de12..aeae43b09 100644 --- a/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart +++ b/lib/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart @@ -1,3 +1,5 @@ +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/email_in_thread_status.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; @@ -7,12 +9,45 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; extension MarkCollapsedEmailReadSuccess on ThreadDetailController { void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) { + _updateEmailListInMailboxDashboardController(success); + _updateEmailKeywordInThreadDetailController(success); + _updateEmailCollapsedStatusInThreadDetailController(success); + } + + void _updateEmailListInMailboxDashboardController( + MarkAsEmailReadSuccess success, + ) { mailboxDashBoardController.updateEmailFlagByEmailIds( [success.emailId], readAction: success.readActions, ); - if (success.readActions == ReadActions.markAsUnread) { + } + + void _updateEmailKeywordInThreadDetailController( + MarkAsEmailReadSuccess success, + ) { + if (success.readActions == ReadActions.markAsRead) { + emailIdsPresentation[success.emailId] + ?.keywords + ?[KeyWordIdentifier.emailSeen] = true; + } else { + emailIdsPresentation[success.emailId] + ?.keywords?.remove(KeyWordIdentifier.emailSeen); + } + } + + void _updateEmailCollapsedStatusInThreadDetailController( + MarkAsEmailReadSuccess success, + ) { + if (success.readActions == ReadActions.markAsRead) return; + if (emailIdsPresentation.length == 1) { closeThreadDetailAction(currentContext); + } else { + emailIdsPresentation[success.emailId] = emailIdsPresentation + [success.emailId] + ?.copyWith( + emailInThreadStatus: EmailInThreadStatus.collapsed, + ); } } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart index 86f0fa1c2..d77a93720 100644 --- a/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart @@ -83,7 +83,7 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController { } void _markRead(PresentationEmail presentationEmail) { - if (session == null || accountId == null) return; + if (session == null || accountId == null || presentationEmail.hasRead) return; consumeState(emailActionReactor.markAsEmailRead( session!, @@ -94,7 +94,7 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController { } void _unRead(PresentationEmail presentationEmail) { - if (session == null || accountId == null) return; + if (session == null || accountId == null || !presentationEmail.hasRead) return; consumeState(emailActionReactor.markAsEmailRead( session!, @@ -108,7 +108,10 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController { PresentationEmail presentationEmail, MarkStarAction markStarAction, ) { - if (session == null || accountId == null) return; + if (session == null || + accountId == null || + (presentationEmail.hasStarred && markStarAction == MarkStarAction.markStar) || + (!presentationEmail.hasStarred && markStarAction == MarkStarAction.unMarkStar)) return; consumeState(emailActionReactor.markAsStarEmail( session!, diff --git a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart index cc0931aa8..9ed7cd3b9 100644 --- a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart +++ b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart @@ -1,9 +1,11 @@ import 'package:get/get.dart'; +import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:model/email/email_in_thread_status.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; extension ToggleThreadDetailCollapeExpand on ThreadDetailController { @@ -27,6 +29,10 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { EmailBindings(currentEmailId: presentationEmail.id).dependencies(); } else { focusExpandedEmail(emailId); + threadDetailOnEmailActionClick( + presentationEmail, + EmailActionType.markAsRead, + ); } for (var key in emailIdsPresentation.keys) { if (emailIdsPresentation[key] == null) continue; diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 5286ea230..ba89b9fe8 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -62,6 +62,7 @@ class ThreadDetailView extends GetWidget { EmailActionType.printAll, EmailActionType.moveToMailbox, ], + emailIsRead: presentationEmail.hasRead, ), ); }), diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart index 61fd02db0..30a2516b0 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; import 'package:flutter/material.dart'; import 'package:model/email/presentation_email.dart'; @@ -73,6 +74,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget { showRecipients: false, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, mailboxContain: mailboxContain, + showUnreadVisualization: true, ), Padding( padding: const EdgeInsets.all(16), @@ -80,7 +82,11 @@ class ThreadDetailCollapsedEmail extends StatelessWidget { preview, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium, + style: ThemeUtils.textStyleBodyBody1( + fontWeight: presentationEmail.hasRead + ? FontWeight.normal + : FontWeight.w600, + ).copyWith(height: 24), ), ), ],