TF-3724 Thread Detail add unread indicator collapsed email

TF-3724 Thread Detail Optimize mark read/star call

TF-3724 Thread Detail Organize markCollapsedEmailReadSuccess

TF-3724 Thread Detail show toast on mark read fail

TF-3724 Thread Detail Update sender, recipient and preview style

TF-3724 Thread Detail Revert ThemeUtils changes
This commit is contained in:
DatDang
2025-05-22 14:36:09 +07:00
committed by Dat H. Pham
parent e61ee911de
commit 9878b062d3
14 changed files with 124 additions and 31 deletions
@@ -135,6 +135,8 @@ extension EmailActionTypeExtension on EmailActionType {
String getIcon(ImagePaths imagePaths) { String getIcon(ImagePaths imagePaths) {
switch(this) { switch(this) {
case EmailActionType.markAsRead:
return imagePaths.icRead;
case EmailActionType.markAsUnread: case EmailActionType.markAsUnread:
return imagePaths.icUnreadEmail; return imagePaths.icUnreadEmail;
case EmailActionType.unSpam: case EmailActionType.unSpam:
@@ -177,6 +179,8 @@ extension EmailActionTypeExtension on EmailActionType {
String getTitle(AppLocalizations appLocalizations) { String getTitle(AppLocalizations appLocalizations) {
switch(this) { switch(this) {
case EmailActionType.markAsRead:
return AppLocalizations.of(context).mark_as_read;
case EmailActionType.markAsUnread: case EmailActionType.markAsUnread:
return appLocalizations.mark_as_unread; return appLocalizations.mark_as_unread;
case EmailActionType.unSpam: case EmailActionType.unSpam:
@@ -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/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/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/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/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.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) { } else if (success is GetEmailContentFromCacheSuccess) {
_getEmailContentOfflineSuccess(success); _getEmailContentOfflineSuccess(success);
} else if (success is MarkAsEmailReadSuccess) { } else if (success is MarkAsEmailReadSuccess) {
_handleMarkAsEmailReadCompleted(success.readActions); _handleMarkAsEmailReadCompleted(success);
} else if (success is ExportAttachmentSuccess) { } else if (success is ExportAttachmentSuccess) {
_exportAttachmentSuccessAction(success); _exportAttachmentSuccessAction(success);
} else if (success is ExportAllAttachmentsSuccess) { } else if (success is ExportAllAttachmentsSuccess) {
@@ -334,7 +335,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
void handleFailureViewState(Failure failure) { void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure); super.handleFailureViewState(failure);
if (failure is MarkAsEmailReadFailure) { if (failure is MarkAsEmailReadFailure) {
_handleMarkAsEmailReadCompleted(failure.readActions); _handleMarkAsEmailReadFailure(failure);
} else if (failure is DownloadAttachmentsFailure) { } else if (failure is DownloadAttachmentsFailure) {
_downloadAttachmentsFailure(failure); _downloadAttachmentsFailure(failure);
} else if (failure is ExportAttachmentFailure) { } 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) { void _handleGetEmailContentFailure(GetEmailContentFailure failure) {
emailLoadedViewState.value = Left<Failure, Success>(failure); emailLoadedViewState.value = Left<Failure, Success>(failure);
showRetryToast(failure); showRetryToast(failure);
@@ -790,16 +800,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} }
} }
void _handleMarkAsEmailReadCompleted(ReadActions readActions) { void _handleMarkAsEmailReadCompleted(MarkAsEmailReadSuccess success) {
if (currentEmail?.id != null) { _threadDetailController?.markCollapsedEmailReadSuccess(success);
mailboxDashBoardController.updateEmailFlagByEmailIds(
[currentEmail!.id!],
readAction: readActions,
);
}
if (readActions == ReadActions.markAsUnread) {
closeEmailView(context: currentContext);
}
} }
void downloadAttachments(BuildContext context, List<Attachment> attachments) async { void downloadAttachments(BuildContext context, List<Attachment> attachments) async {
@@ -97,6 +97,7 @@ class EmailView extends GetWidget<SingleEmailController> {
username: controller.session?.username, username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [], additionalActions: [],
emailIsRead: presentationEmail.hasRead,
), ),
supportBackAction: !isInsideThreadDetailView, supportBackAction: !isInsideThreadDetailView,
appBarDecoration: isInsideThreadDetailView appBarDecoration: isInsideThreadDetailView
@@ -299,6 +300,7 @@ class EmailView extends GetWidget<SingleEmailController> {
EmailActionType.deletePermanently, EmailActionType.deletePermanently,
], ],
], ],
emailIsRead: presentationEmail.hasRead,
), ),
onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
@@ -516,6 +516,7 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi
EmailActionType action, EmailActionType action,
) handleEmailAction, ) handleEmailAction,
required List<EmailActionType> additionalActions, required List<EmailActionType> additionalActions,
required bool emailIsRead,
}) { }) {
if (currentContext == null) return; if (currentContext == null) return;
@@ -544,7 +545,10 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi
_canDeletePermanently(presentationEmail) _canDeletePermanently(presentationEmail)
? EmailActionType.deletePermanently ? EmailActionType.deletePermanently
: EmailActionType.moveToTrash, : EmailActionType.moveToTrash,
EmailActionType.markAsUnread, if (emailIsRead)
EmailActionType.markAsUnread
else
EmailActionType.markAsRead,
if (mailboxContain?.isChildOfTeamMailboxes == false) if (mailboxContain?.isChildOfTeamMailboxes == false)
if (mailboxContain?.isSpam == true) if (mailboxContain?.isSpam == true)
EmailActionType.unSpam EmailActionType.unSpam
@@ -54,7 +54,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
if (_isDisplayAll) { if (_isDisplayAll) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Flexible( Flexible(
child: Container( child: Container(
@@ -147,7 +147,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
} else { } else {
if (_isDisplayAll) { if (_isDisplayAll) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Flexible( Flexible(
child: Column( child: Column(
@@ -238,8 +238,11 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
text: index == listEmailAddress.length - 1 text: index == listEmailAddress.length - 1
? emailAddress.asString() ? emailAddress.asString()
: '${emailAddress.asString()},', : '${emailAddress.asString()},',
textStyle: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400), textStyle: ThemeUtils.textStyleHeadingHeadingSmall(
padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), color: Colors.black,
fontWeight: FontWeight.w400,
).copyWith(height: 17),
padding: const EdgeInsetsDirectional.symmetric(vertical: 2, horizontal: 8),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
onTapActionCallback: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), onTapActionCallback: () => widget.openEmailAddressDetailAction?.call(context, emailAddress),
onLongPressActionCallback: () => AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress), onLongPressActionCallback: () => AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress),
@@ -41,9 +41,10 @@ class EmailSenderBuilder extends StatelessWidget {
}, },
borderRadius: 8, borderRadius: 8,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
customStyle: ThemeUtils.textStyleHeadingHeadingSmall( customStyle: ThemeUtils.textStyleHeadingH6(
color: Colors.black, color: Colors.black,
), fontWeight: FontWeight.w500,
).copyWith(height: 20),
overflow: CommonTextStyle.defaultTextOverFlow, overflow: CommonTextStyle.defaultTextOverFlow,
softWrap: CommonTextStyle.defaultSoftWrap softWrap: CommonTextStyle.defaultSoftWrap
), ),
@@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/icon_utils.dart'; import 'package:core/presentation/utils/icon_utils.dart';
import 'package:core/presentation/utils/responsive_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/button/tmail_button_widget.dart';
import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/image/avatar_builder.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
@@ -41,6 +40,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
final VoidCallback? onToggleThreadDetailCollapseExpand; final VoidCallback? onToggleThreadDetailCollapseExpand;
final OnTapAvatarActionClick? onTapAvatarActionClick; final OnTapAvatarActionClick? onTapAvatarActionClick;
final PresentationMailbox? mailboxContain; final PresentationMailbox? mailboxContain;
final bool showUnreadVisualization;
const InformationSenderAndReceiverBuilder({ const InformationSenderAndReceiverBuilder({
Key? key, Key? key,
@@ -59,6 +59,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
this.onToggleThreadDetailCollapseExpand, this.onToggleThreadDetailCollapseExpand,
this.onTapAvatarActionClick, this.onTapAvatarActionClick,
this.mailboxContain, this.mailboxContain,
this.showUnreadVisualization = false,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -81,12 +82,25 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
children: [ 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) if (emailSelected.from?.isNotEmpty == true)
Flexible( Flexible(
child: EmailSenderBuilder( child: EmailSenderBuilder(
@@ -124,6 +138,19 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
emailSelected: emailSelected, emailSelected: emailSelected,
padding: const EdgeInsetsDirectional.only(start: 16, top: 2), 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)) if (responsiveUtils.isMobile(context))
ReceivedTimeBuilder( ReceivedTimeBuilder(
emailSelected: emailSelected, emailSelected: emailSelected,
@@ -12,12 +12,9 @@ class PrefixRecipientWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Text(
padding: const EdgeInsets.only(top: 6), '${prefixEmailAddress.asName(context)}:',
child: Text( style: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400),
'${prefixEmailAddress.asName(context)}:',
style: ThemeUtils.textStyleBodyBody1(color: AppColor.steelGray400),
),
); );
} }
} }
@@ -92,6 +92,7 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
EmailActionType.deletePermanently, EmailActionType.deletePermanently,
], ],
], ],
emailIsRead: presentationEmail.hasRead,
), ),
openEmailAddressDetailAction: (_, emailAddress) { openEmailAddressDetailAction: (_, emailAddress) {
openEmailAddressDetailAction(emailAddress); openEmailAddressDetailAction(emailAddress);
@@ -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: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/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'; 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 { extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) { void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) {
_updateEmailListInMailboxDashboardController(success);
_updateEmailKeywordInThreadDetailController(success);
_updateEmailCollapsedStatusInThreadDetailController(success);
}
void _updateEmailListInMailboxDashboardController(
MarkAsEmailReadSuccess success,
) {
mailboxDashBoardController.updateEmailFlagByEmailIds( mailboxDashBoardController.updateEmailFlagByEmailIds(
[success.emailId], [success.emailId],
readAction: success.readActions, 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); closeThreadDetailAction(currentContext);
} else {
emailIdsPresentation[success.emailId] = emailIdsPresentation
[success.emailId]
?.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed,
);
} }
} }
} }
@@ -83,7 +83,7 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
} }
void _markRead(PresentationEmail presentationEmail) { void _markRead(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return; if (session == null || accountId == null || presentationEmail.hasRead) return;
consumeState(emailActionReactor.markAsEmailRead( consumeState(emailActionReactor.markAsEmailRead(
session!, session!,
@@ -94,7 +94,7 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
} }
void _unRead(PresentationEmail presentationEmail) { void _unRead(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return; if (session == null || accountId == null || !presentationEmail.hasRead) return;
consumeState(emailActionReactor.markAsEmailRead( consumeState(emailActionReactor.markAsEmailRead(
session!, session!,
@@ -108,7 +108,10 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
PresentationEmail presentationEmail, PresentationEmail presentationEmail,
MarkStarAction markStarAction, 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( consumeState(emailActionReactor.markAsStarEmail(
session!, session!,
@@ -1,9 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.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/bindings/email_bindings.dart';
import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.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: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/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'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension ToggleThreadDetailCollapeExpand on ThreadDetailController { extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
@@ -27,6 +29,10 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
EmailBindings(currentEmailId: presentationEmail.id).dependencies(); EmailBindings(currentEmailId: presentationEmail.id).dependencies();
} else { } else {
focusExpandedEmail(emailId); focusExpandedEmail(emailId);
threadDetailOnEmailActionClick(
presentationEmail,
EmailActionType.markAsRead,
);
} }
for (var key in emailIdsPresentation.keys) { for (var key in emailIdsPresentation.keys) {
if (emailIdsPresentation[key] == null) continue; if (emailIdsPresentation[key] == null) continue;
@@ -62,6 +62,7 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
EmailActionType.printAll, EmailActionType.printAll,
EmailActionType.moveToMailbox, EmailActionType.moveToMailbox,
], ],
emailIsRead: presentationEmail.hasRead,
), ),
); );
}), }),
@@ -1,6 +1,7 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/responsive_utils.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:core/presentation/views/text/text_overflow_builder.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.dart'; import 'package:model/email/presentation_email.dart';
@@ -73,6 +74,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
showRecipients: false, showRecipients: false,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
mailboxContain: mailboxContain, mailboxContain: mailboxContain,
showUnreadVisualization: true,
), ),
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -80,7 +82,11 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
preview, preview,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium, style: ThemeUtils.textStyleBodyBody1(
fontWeight: presentationEmail.hasRead
? FontWeight.normal
: FontWeight.w600,
).copyWith(height: 24),
), ),
), ),
], ],