diff --git a/core/lib/domain/extensions/datetime_extension.dart b/core/lib/domain/extensions/datetime_extension.dart index b95223db1..55afce40e 100644 --- a/core/lib/domain/extensions/datetime_extension.dart +++ b/core/lib/domain/extensions/datetime_extension.dart @@ -39,4 +39,15 @@ extension DateTimeNullableExtension on DateTime? { } return 'yMMMd'; } + + String toPatternForEmailView() { + if (this != null) { + if (this!.isThisYear()) { + return 'dd.MM, HH:mm'; + } else { + return 'dd/MM/yyyy'; + } + } + return 'dd/MM/yyyy'; + } } diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 6a4e05506..1a703c6ce 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -90,6 +90,9 @@ extension AppColor on Color { static const colorDividerComposer = Color(0xFFC6C6C8); static const colorDividerEmailView = Color(0xFFD7D8D9); static const colorButton = Color(0xFF959DAD); + static const colorTime = Color(0xFF92A1B4); + static const colorEmailAddressPrefix = Color(0xFF9AA7B6); + static const colorEmailAddressTag = Color(0x146D7885); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/views/image/avatar_builder.dart b/core/lib/presentation/views/image/avatar_builder.dart index 9508f762e..c2fd27eba 100644 --- a/core/lib/presentation/views/image/avatar_builder.dart +++ b/core/lib/presentation/views/image/avatar_builder.dart @@ -12,6 +12,7 @@ class AvatarBuilder { OnTapAvatarActionClick? _onTapAvatarActionClick; List? _avatarColors; List? _boxShadows; + TextStyle? _textStyle; void key(Key key) { _key = key; @@ -41,6 +42,10 @@ class AvatarBuilder { _boxShadows = boxShadows; } + void addTextStyle(TextStyle? textStyle) { + _textStyle = textStyle; + } + void addOnTapActionClick(OnTapAvatarActionClick onTapAvatarActionClick) { _onTapAvatarActionClick = onTapAvatarActionClick; } @@ -81,7 +86,7 @@ class AvatarBuilder { ), child: Text( '${_text ?? ''}', - style: TextStyle(fontSize: 20, color: _textColor ?? AppColor.avatarTextColor, fontWeight: FontWeight.w500) + style: _textStyle ?? TextStyle(fontSize: 20, color: _textColor ?? AppColor.avatarTextColor, fontWeight: FontWeight.w500) ) ), ], diff --git a/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart b/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart index 729ccde4e..b9780fb10 100644 --- a/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart +++ b/lib/features/composer/presentation/extensions/prefix_email_address_extension.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -13,6 +14,21 @@ extension PrefixEmailAddressExtension on PrefixEmailAddress { return AppLocalizations.of(context).cc_email_address_prefix; case PrefixEmailAddress.bcc: return AppLocalizations.of(context).bcc_email_address_prefix; + case PrefixEmailAddress.from: + return AppLocalizations.of(context).from_email_address_prefix; + } + } + + List listEmailAddress(PresentationEmail email) { + switch(this) { + case PrefixEmailAddress.to: + return email.to?.toList() ?? List.empty(); + case PrefixEmailAddress.cc: + return email.cc?.toList() ?? List.empty(); + case PrefixEmailAddress.bcc: + return email.bcc?.toList() ?? List.empty(); + case PrefixEmailAddress.from: + return email.from?.toList() ?? List.empty(); } } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 5b60a9715..c29feeaaa 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -50,6 +50,7 @@ class EmailController extends BaseController { final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; + final isDisplayFullEmailAddress = false.obs; final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; final emailContents = [].obs; final attachments = [].obs; @@ -76,8 +77,8 @@ class EmailController extends BaseController { super.onReady(); mailboxDashBoardController.selectedEmail.listen((presentationEmail) { if (_currentEmailId != presentationEmail?.id) { - _currentEmailId = presentationEmail?.id; _clearEmailContent(); + _currentEmailId = presentationEmail?.id; final accountId = mailboxDashBoardController.accountId.value; if (accountId != null && presentationEmail != null) { _getEmailContentAction(accountId, presentationEmail.id); @@ -146,16 +147,28 @@ class EmailController extends BaseController { } void _clearEmailContent() { - toggleDisplayEmailAddressAction(expandMode: ExpandMode.COLLAPSE); attachmentsExpandMode.value = ExpandMode.COLLAPSE; + emailAddressExpandMode.value = ExpandMode.COLLAPSE; + isDisplayFullEmailAddress.value = false; emailContents.clear(); attachments.clear(); } - void toggleDisplayEmailAddressAction({required ExpandMode expandMode}) { - emailAddressExpandMode.value = expandMode; + void toggleDisplayEmailAddressAction({ExpandMode? expandMode}) { + if (expandMode != null) { + emailAddressExpandMode.value = expandMode; + } else { + final newExpandMode = emailAddressExpandMode.value == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; + emailAddressExpandMode.value = newExpandMode; + } + + if (emailAddressExpandMode.value == ExpandMode.COLLAPSE) { + isDisplayFullEmailAddress.value = false; + } } + bool get isExpandEmailAddress => emailAddressExpandMode.value == ExpandMode.EXPAND; + void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions) async { final accountId = mailboxDashBoardController.accountId.value; final mailboxCurrent = mailboxDashBoardController.selectedMailbox.value; @@ -423,6 +436,10 @@ class EmailController extends BaseController { items: popupMenuItems); } + void showFullEmailAddress() { + isDisplayFullEmailAddress.value = true; + } + void closeMoreMenu() { popBack(); } @@ -433,6 +450,9 @@ class EmailController extends BaseController { && mailboxDashBoardController.selectedEmail.value != null; void backToThreadView() { + attachmentsExpandMode.value = ExpandMode.COLLAPSE; + emailAddressExpandMode.value = ExpandMode.COLLAPSE; + isDisplayFullEmailAddress.value = false; popBack(); } diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index e9cd0fe91..8311a87be 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -6,7 +6,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/app_bar_mail_widget_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart'; @@ -15,7 +17,6 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/bottom_bar_mai import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_content_item_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_content_place_holder_loading_widget.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/sender_and_receiver_information_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:filesize/filesize.dart'; @@ -25,6 +26,8 @@ class EmailView extends GetView { final responsiveUtils = Get.find(); final imagePaths = Get.find(); + static const int LIMIT_ADDRESS_DISPLAY = 1; + @override Widget build(BuildContext context) { return Scaffold( @@ -45,9 +48,9 @@ class EmailView extends GetView { )); } - Widget _buildDivider(){ + Widget _buildDivider({EdgeInsets? edgeInsets}){ return Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: edgeInsets ?? EdgeInsets.symmetric(horizontal: 16), child: Divider(color: AppColor.colorDividerEmailView, height: 0.5)); } @@ -91,26 +94,16 @@ class EmailView extends GetView { color: Colors.white, margin: EdgeInsets.zero, alignment: Alignment.topCenter, - child: Obx(() => emailController.mailboxDashBoardController.selectedEmail.value != null + child: Obx(() => emailController.currentEmail != null ? SingleChildScrollView( physics : ClampingScrollPhysics(), child: Container( margin: EdgeInsets.zero, width: double.infinity, alignment: Alignment.center, - padding: EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - top: 16), + padding: EdgeInsets.zero, color: Colors.white, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - _buildEmailMessage(context), - ]) + child: _buildEmailMessage(context) )) : Center(child: _buildEmailEmpty(context)) ) @@ -126,48 +119,38 @@ class EmailView extends GetView { Widget _buildEmailSubject() { return Padding( - padding: EdgeInsets.only(left: 8, top: 25, bottom: 16), + padding: EdgeInsets.only(right: 16), child: Text( '${emailController.mailboxDashBoardController.selectedEmail.value?.getEmailTitle()}', - style: TextStyle(fontSize: 22, color: AppColor.colorNameEmail, fontWeight: FontWeight.w700) + style: TextStyle(fontSize: 20, color: AppColor.colorNameEmail) )); } Widget _buildEmailMessage(BuildContext context) { return Container( - padding: EdgeInsets.only( - bottom: 16, - left: 16, - right: 16, - top: 10), + padding: EdgeInsets.only(bottom: 16, left: 16, right: 16, top: 10), alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppColor.colorShadowBgContentEmail, - spreadRadius: 3, - blurRadius: 3, - offset: Offset(0, 2), // changes position of shadow - ), - ], - color: Colors.white), + color: Colors.white, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - Obx(() => SenderAndReceiverInformationTileBuilder( - context, - imagePaths, - emailController.mailboxDashBoardController.selectedEmail.value, - emailController.emailAddressExpandMode.value) - .onOpenExpandAddressReceiverActionClick(() => emailController.toggleDisplayEmailAddressAction(expandMode: ExpandMode.EXPAND)) - .build()), - Padding( - padding: EdgeInsets.zero, - child: Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.1)), - _buildEmailSubject(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded(child: _buildEmailSubject()), + _buildEmailTime(context), + ]), + _buildDivider(edgeInsets: EdgeInsets.only(top: 16)), + Obx(() => emailController.currentEmail != null + ? _buildEmailAddress( + context, + emailController.currentEmail!, + emailController.emailAddressExpandMode.value, + emailController.isDisplayFullEmailAddress.value) + : SizedBox.shrink()), + _buildDivider(edgeInsets: EdgeInsets.only(top: 4)), _buildAttachments(context), _buildListEmailContent(), ], @@ -175,6 +158,153 @@ class EmailView extends GetView { ); } + Widget _buildEmailTime(BuildContext context) { + return Transform( + transform: Matrix4.translationValues(0.0, 12.0, 0.0), + child: Text( + '${emailController.currentEmail?.getReceivedAt( + Localizations.localeOf(context).toLanguageTag(), + pattern: emailController.currentEmail?.receivedAt?.value.toLocal().toPatternForEmailView())}', + maxLines: 1, + overflow:TextOverflow.ellipsis, + style: TextStyle(fontSize: 13, color: AppColor.colorTime))); + } + + Widget _buildEmailAddress(BuildContext context, PresentationEmail email, ExpandMode expandMode, bool isDisplayFull) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (email.from.numberEmailAddress() > 0) + _buildEmailAddressByPrefix(context, email, PrefixEmailAddress.from, isDisplayFull), + if (email.to.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND) + _buildDivider(edgeInsets: EdgeInsets.only(top: 4)), + if (email.to.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND) + _buildEmailAddressByPrefix(context, email, PrefixEmailAddress.to, isDisplayFull), + if (email.cc.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND && isDisplayFull) + _buildDivider(edgeInsets: EdgeInsets.only(top: 4)), + if (email.cc.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND && isDisplayFull) + _buildEmailAddressByPrefix(context, email, PrefixEmailAddress.cc, isDisplayFull), + if (email.bcc.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND && isDisplayFull) + _buildDivider(edgeInsets: EdgeInsets.only(top: 4)), + if (email.bcc.numberEmailAddress() > 0 && expandMode == ExpandMode.EXPAND && isDisplayFull) + _buildEmailAddressByPrefix(context, email, PrefixEmailAddress.bcc, isDisplayFull), + ], + ); + } + + Widget _buildEmailAddressByPrefix( + BuildContext context, + PresentationEmail presentationEmail, + PrefixEmailAddress prefixEmailAddress, + bool isDisplayFull, + ) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 20), + child: Text( + '${prefixEmailAddress.asName(context)}:', + style: TextStyle(fontSize: 14, color: AppColor.colorEmailAddressPrefix))), + Expanded(child: _buildEmailAddressWidget( + context, + presentationEmail, + prefixEmailAddress.listEmailAddress(presentationEmail), + prefixEmailAddress, + isDisplayFull + )), + if (prefixEmailAddress == PrefixEmailAddress.from) _buildEmailAddressDetailButton(context), + ] + ); + } + + Widget _buildEmailAddressWidget( + BuildContext context, + PresentationEmail presentationEmail, + List listEmailAddress, + PrefixEmailAddress prefixEmailAddress, + bool isDisplayFull, + ) { + final displayedEmailAddress = isDisplayFull ? listEmailAddress : listEmailAddress.sublist(0, LIMIT_ADDRESS_DISPLAY); + return Padding( + padding: EdgeInsets.only(top: 4), + child: Wrap( + children: [ + ...displayedEmailAddress.map((emailAddress) { + return Padding( + padding: EdgeInsets.only(left: 10), + child: Chip( + labelPadding: EdgeInsets.only(left: 8, right: 8, bottom: 2), + label: Text('${emailAddress.emailAddress}', maxLines: 1, overflow: TextOverflow.ellipsis), + labelStyle: TextStyle(color: AppColor.colorNameEmail, fontSize: 15, fontWeight: FontWeight.normal), + backgroundColor: AppColor.colorEmailAddressTag, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(width: 0, color: AppColor.colorEmailAddressTag), + ), + avatar: (AvatarBuilder() + ..text('${emailAddress.emailAddress.characters.first.toUpperCase()}') + ..addTextStyle(TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w600)) + ..avatarColor(emailAddress.avatarColors)) + .build(), + ) + ); + }).toList(), + if (prefixEmailAddress == PrefixEmailAddress.to + && presentationEmail.numberOfAllEmailAddress() > 1 + && !isDisplayFull) + _buildEmailAddressCounter(context, presentationEmail), + ] + )); + } + + Widget _buildEmailAddressDetailButton(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(12), + color: Colors.transparent, + child: Padding( + padding: EdgeInsets.only(top: 4, left: 16), + child: TextButton( + onPressed: () => emailController.toggleDisplayEmailAddressAction(), + child: Text( + emailController.isExpandEmailAddress + ? AppLocalizations.of(context).hide + : AppLocalizations.of(context).details, + style: TextStyle(fontSize: 15, color: AppColor.colorTextButton, fontWeight: FontWeight.normal), + ), + ) + ) + ); + } + + String _getRemainCountAddressReceiver(PresentationEmail email) { + if (email.numberOfAllEmailAddress() - LIMIT_ADDRESS_DISPLAY >= 999) { + return '999'; + } + return '${email.numberOfAllEmailAddress() - LIMIT_ADDRESS_DISPLAY}'; + } + + Widget _buildEmailAddressCounter(BuildContext context, PresentationEmail email) { + return GestureDetector( + onTap: () => emailController.showFullEmailAddress(), + child: Padding( + padding: EdgeInsets.only(left: 8), + child: Chip( + labelPadding: EdgeInsets.symmetric(horizontal: 8), + label: Text('+${_getRemainCountAddressReceiver(email)}', maxLines: 1, overflow: TextOverflow.ellipsis), + labelStyle: TextStyle(color: AppColor.colorTextButton, fontSize: 15, fontWeight: FontWeight.normal), + backgroundColor: AppColor.colorEmailAddressTag, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(width: 0, color: AppColor.colorEmailAddressTag), + ), + ), + ), + ); + } + Widget _buildAttachments(BuildContext context) { return Obx(() => emailController.viewState.value.fold( (failure) => SizedBox.shrink(), diff --git a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart index 7617f0e97..70e8e302b 100644 --- a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart +++ b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart @@ -45,7 +45,7 @@ class BottomBarMailWidgetBuilder { if (presentationEmail.numberOfAllEmailAddress() > 1) (ButtonBuilder(_imagePaths.icReplyAll) ..key(Key('button_reply_all_message')) - ..size(20) + ..size(15) ..paddingIcon(EdgeInsets.only( top: _responsiveUtils.isMobileDevice(_context) ? 10 : 16, bottom: _responsiveUtils.isMobileDevice(_context) ? 8 : 16, diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index 19a1f0dff..2803f1313 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -211,7 +211,6 @@ class EmailTileBuilder { ..size(GetPlatform.isWeb ? 48 : 56) ..textColor(Colors.white) ..avatarColor(_presentationEmail.avatarColors)) - // .iconStatus(_imagePaths.icOffline) .build(); } } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index e40142b28..eebc6d68e 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -743,4 +743,16 @@ class AppLocalizations { name: 'new_message', ); } + + String get details { + return Intl.message( + 'Details', + name: 'details'); + } + + String get hide { + return Intl.message( + 'Hide', + name: 'hide'); + } } \ No newline at end of file diff --git a/model/lib/email/prefix_email_address.dart b/model/lib/email/prefix_email_address.dart index a4ddba94c..9b93fbec1 100644 --- a/model/lib/email/prefix_email_address.dart +++ b/model/lib/email/prefix_email_address.dart @@ -1,5 +1,6 @@ enum PrefixEmailAddress { + from, to, cc, bcc diff --git a/model/lib/extensions/email_address_extension.dart b/model/lib/extensions/email_address_extension.dart index 9750a2180..6203e5374 100644 --- a/model/lib/extensions/email_address_extension.dart +++ b/model/lib/extensions/email_address_extension.dart @@ -1,6 +1,9 @@ +import 'dart:ui'; + import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/email_address_cache.dart'; import 'package:core/core.dart'; +import 'package:collection/collection.dart'; extension EmailAddressExtension on EmailAddress { @@ -33,4 +36,20 @@ extension EmailAddressExtension on EmailAddress { String get displayName => name ?? ''; EmailAddressCache toEmailAddressCache() => EmailAddressCache(displayName, emailAddress); + + List get avatarColors { + return AppColor.mapGradientColor[_generateIndex()]; + } + + int _generateIndex() { + if (emailAddress.isNotEmpty) { + final codeUnits = emailAddress.codeUnits; + if (codeUnits.isNotEmpty) { + final sumCodeUnits = codeUnits.sum; + final index = sumCodeUnits % AppColor.mapGradientColor.length; + return index; + } + } + return 0; + } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index c18184673..80e7d1407 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -6,24 +6,11 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; -import 'package:collection/collection.dart'; extension PresentationEmailExtension on PresentationEmail { List get avatarColors { - return AppColor.mapGradientColor[_generateIndexFromSender()]; - } - - int _generateIndexFromSender() { - if (from != null && from?.isNotEmpty == true) { - final codeUnits = from?.first.email?.codeUnits ?? List.empty(); - if (codeUnits.isNotEmpty) { - final sumCodeUnits = codeUnits.sum; - final index = sumCodeUnits % AppColor.mapGradientColor.length; - return index; - } - } - return 0; + return from?.first.avatarColors ?? AppColor.mapGradientColor.first; } int numberOfAllEmailAddress() => to.numberEmailAddress() + cc.numberEmailAddress() + bcc.numberEmailAddress();