diff --git a/contact/pubspec.lock b/contact/pubspec.lock index cbf82dd470..9b5c6e164a 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fk_user_agent: + dependency: transitive + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: transitive description: @@ -563,8 +571,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -671,6 +679,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/contact/pubspec.yaml b/contact/pubspec.yaml index 97466ee95c..04932b60a9 100644 --- a/contact/pubspec.yaml +++ b/contact/pubspec.yaml @@ -15,10 +15,12 @@ dependencies: path: ../model ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/core/lib/core.dart b/core/lib/core.dart index 596285f9c5..9bc6ca3923 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -45,6 +45,9 @@ export 'utils/file_utils.dart'; export 'utils/option_param_mixin.dart'; export 'utils/print_utils.dart'; export 'utils/broadcast_channel/broadcast_channel.dart'; +export 'utils/list_utils.dart'; +export 'utils/mail/domain.dart'; +export 'utils/mail/mail_address.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index adf3581cbe..5540b0ac1c 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -2,6 +2,7 @@ class Constant { static const acceptHeaderDefault = 'application/json'; static const contentTypeHeaderDefault = 'application/json'; static const pdfMimeType = 'application/pdf'; + static const base64Charset = 'base64'; static const textHtmlMimeType = 'text/html'; static const octetStreamMimeType = 'application/octet-stream'; static const pdfExtension = '.pdf'; diff --git a/core/lib/data/network/download/download_client.dart b/core/lib/data/network/download/download_client.dart index 781eeab111..8686739f1c 100644 --- a/core/lib/data/network/download/download_client.dart +++ b/core/lib/data/network/download/download_client.dart @@ -63,7 +63,8 @@ class DownloadClient { 'bytesData': bytesData, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -77,7 +78,8 @@ class DownloadClient { 'bytesData': bytesDataCompressed, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -86,7 +88,8 @@ class DownloadClient { 'bytesData': bytesData, 'mimeType': 'image/$fileExtension', 'cid': cid, - 'fileName': fileName + 'fileName': fileName, + 'maxWidth': maxWidth }); return base64Uri; @@ -103,11 +106,14 @@ class DownloadClient { var mimeType = entryParam['mimeType']; final cid = entryParam['cid']; var fileName = entryParam['fileName']; + var maxWidth = entryParam['maxWidth'] != null + ? '${entryParam['maxWidth']}px' + : '100%'; final base64Data = base64Encode(bytesData); if (fileName.contains('.')) { fileName = fileName.split('.').first; } - final base64Uri = '$fileName'; + final base64Uri = '$fileName'; return base64Uri; } } \ No newline at end of file diff --git a/core/lib/domain/exceptions/address_exception.dart b/core/lib/domain/exceptions/address_exception.dart new file mode 100644 index 0000000000..51889bbf76 --- /dev/null +++ b/core/lib/domain/exceptions/address_exception.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; + +class AddressException with EquatableMixin implements Exception { + final String message; + + AddressException(this.message); + + @override + String toString() => message; + + @override + List get props => [message]; +} diff --git a/core/lib/presentation/action/action_callback_define.dart b/core/lib/presentation/action/action_callback_define.dart index 8bce05143e..95baa0b255 100644 --- a/core/lib/presentation/action/action_callback_define.dart +++ b/core/lib/presentation/action/action_callback_define.dart @@ -2,4 +2,5 @@ import 'package:flutter/material.dart'; typedef OnTapActionCallback = void Function(); -typedef OnTapActionAtPositionCallback = void Function(RelativeRect position); \ No newline at end of file +typedef OnTapActionAtPositionCallback = void Function(RelativeRect position); +typedef OnLongPressActionCallback = void Function(); \ No newline at end of file diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index f1dd580168..14c2f4e1b8 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -160,7 +160,6 @@ extension AppColor on Color { static const colorBackgroundQuotasWarning = Color(0xFFFFC107); static const colorQuotaWarning = Color(0xFFF05C44); static const colorQuotaError = Color(0xffE64646); - static const colorThumbScrollBar = Color(0xFFAEB7C2); static const colorCreateNewIdentityButton = Color(0xFFEBEDF0); static const colorSpamReportBannerBackground = Color(0xFFBFDEFF); static const colorSpamReportBannerStrokeBorder = Color(0x1F000000); diff --git a/core/lib/presentation/extensions/string_extension.dart b/core/lib/presentation/extensions/string_extension.dart index 544db2b584..cd4cd1502d 100644 --- a/core/lib/presentation/extensions/string_extension.dart +++ b/core/lib/presentation/extensions/string_extension.dart @@ -1,5 +1,4 @@ import 'package:core/utils/app_logger.dart'; -import 'package:flutter/material.dart'; extension StringExtension on String { @@ -39,11 +38,4 @@ extension StringExtension on String { return ''; } } - - String get overflow { - return characters - .replaceAll(Characters(''), Characters('\u{200B}')) - .replaceAll(Characters('-'), Characters('\u{2011}')) - .toString(); - } } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index fecf928d13..6c8ad45207 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -43,7 +43,6 @@ class TransformConfiguration { if (PlatformInfo.isWeb) const RemoveTooltipLinkTransformer(), const SignatureTransformer(), - const RemoveLazyLoadingForBackgroundImageTransformer(), const RemoveCollapsedSignatureButtonTransformer(), ]); diff --git a/core/lib/presentation/utils/responsive_utils.dart b/core/lib/presentation/utils/responsive_utils.dart index e19414b05f..6b989d9b68 100644 --- a/core/lib/presentation/utils/responsive_utils.dart +++ b/core/lib/presentation/utils/responsive_utils.dart @@ -19,15 +19,15 @@ class ResponsiveUtils { static const double desktopVerticalMargin = 120.0; static const double desktopHorizontalMargin = 200.0; - bool isScreenWithShortestSide(BuildContext context) => context.mediaQueryShortestSide < minTabletWidth; + bool isScreenWithShortestSide(BuildContext context) => getSizeScreenShortestSide(context) < minTabletWidth; - double getSizeScreenWidth(BuildContext context) => context.width; + double getSizeScreenWidth(BuildContext context) => MediaQuery.sizeOf(context).width; - double getSizeScreenHeight(BuildContext context) => context.height; + double getSizeScreenHeight(BuildContext context) => MediaQuery.sizeOf(context).height; - double getSizeScreenShortestSide(BuildContext context) => context.mediaQueryShortestSide; + double getSizeScreenShortestSide(BuildContext context) => MediaQuery.sizeOf(context).shortestSide; - double getDeviceWidth(BuildContext context) => context.width; + double getDeviceWidth(BuildContext context) => MediaQuery.sizeOf(context).width; bool isMobile(BuildContext context) => getDeviceWidth(context) < minTabletWidth; @@ -46,21 +46,21 @@ class ResponsiveUtils { bool isLandscapeMobile(BuildContext context) => isScreenWithShortestSide(context) && isLandscape(context); bool isLandscapeTablet(BuildContext context) { - return context.mediaQueryShortestSide >= minTabletWidth && - context.mediaQueryShortestSide < minDesktopWidth && + return getSizeScreenShortestSide(context) >= minTabletWidth && + getSizeScreenShortestSide(context) < minDesktopWidth && isLandscape(context); } bool isPortraitMobile(BuildContext context) => isScreenWithShortestSide(context) && isPortrait(context); bool isPortraitTablet(BuildContext context) { - return context.mediaQueryShortestSide >= minTabletWidth && - context.mediaQueryShortestSide < minDesktopWidth && + return getSizeScreenShortestSide(context) >= minTabletWidth && + getSizeScreenShortestSide(context) < minDesktopWidth && isPortrait(context); } bool isHeightShortest(BuildContext context) { - return MediaQuery.of(context).size.shortestSide < heightShortest; + return getSizeScreenShortestSide(context) < heightShortest; } double getMaxWidthToast(BuildContext context) { diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index 7755ba0c0a..2be885946f 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -14,9 +14,9 @@ class ThemeUtils { dividerTheme: _dividerTheme, visualDensity: VisualDensity.adaptivePlatformDensity, scrollbarTheme: ScrollbarThemeData( - thickness: MaterialStateProperty.all(2.0), - radius: const Radius.circular(5.0), - thumbColor: MaterialStateProperty.all(AppColor.colorThumbScrollBar)), + thickness: MaterialStateProperty.all(8.0), + radius: const Radius.circular(8.0), + thumbColor: MaterialStateProperty.all(AppColor.thumbScrollbarColor)), ); } diff --git a/core/lib/presentation/views/button/tmail_button_widget.dart b/core/lib/presentation/views/button/tmail_button_widget.dart index 3f3a645c1d..1a8f8e1516 100644 --- a/core/lib/presentation/views/button/tmail_button_widget.dart +++ b/core/lib/presentation/views/button/tmail_button_widget.dart @@ -10,6 +10,7 @@ class TMailButtonWidget extends StatelessWidget { final OnTapActionCallback? onTapActionCallback; final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + final OnLongPressActionCallback? onLongPressActionCallback; final double borderRadius; final double? width; @@ -44,6 +45,7 @@ class TMailButtonWidget extends StatelessWidget { required this.text, this.onTapActionCallback, this.onTapActionAtPositionCallback, + this.onLongPressActionCallback, this.borderRadius = 20, this.width, this.maxWidth = double.infinity, @@ -77,6 +79,7 @@ class TMailButtonWidget extends StatelessWidget { final Key? key, OnTapActionCallback? onTapActionCallback, OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + OnLongPressActionCallback? onLongPressActionCallback, double borderRadius = 20, double? width, double maxWidth = double.infinity, @@ -99,6 +102,7 @@ class TMailButtonWidget extends StatelessWidget { text: '', onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth : maxWidth, @@ -124,6 +128,7 @@ class TMailButtonWidget extends StatelessWidget { final Key? key, OnTapActionCallback? onTapActionCallback, OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + OnLongPressActionCallback? onLongPressActionCallback, double borderRadius = 20, double? width, double maxWidth = double.infinity, @@ -145,6 +150,7 @@ class TMailButtonWidget extends StatelessWidget { text: text, onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth : maxWidth, @@ -335,6 +341,7 @@ class TMailButtonWidget extends StatelessWidget { return TMailContainerWidget( onTapActionCallback: onTapActionCallback, onTapActionAtPositionCallback: onTapActionAtPositionCallback, + onLongPressActionCallback: onLongPressActionCallback, borderRadius: borderRadius, width: width, maxWidth: maxWidth, diff --git a/core/lib/presentation/views/container/tmail_container_widget.dart b/core/lib/presentation/views/container/tmail_container_widget.dart index 307bdc3df8..525e92f24b 100644 --- a/core/lib/presentation/views/container/tmail_container_widget.dart +++ b/core/lib/presentation/views/container/tmail_container_widget.dart @@ -7,6 +7,7 @@ class TMailContainerWidget extends StatelessWidget { final OnTapActionCallback? onTapActionCallback; final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + final OnLongPressActionCallback? onLongPressActionCallback; final Widget child; final double borderRadius; @@ -26,6 +27,7 @@ class TMailContainerWidget extends StatelessWidget { required this.child, this.onTapActionCallback, this.onTapActionAtPositionCallback, + this.onLongPressActionCallback, this.borderRadius = 20, this.width, this.maxWidth = double.infinity, @@ -58,6 +60,7 @@ class TMailContainerWidget extends StatelessWidget { onTapActionAtPositionCallback!.call(position); } }, + onLongPress: onLongPressActionCallback, borderRadius: BorderRadius.all(Radius.circular(borderRadius)), child: tooltipMessage != null ? Tooltip( diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 036f485238..18dbcf2586 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -22,20 +22,20 @@ class ConfirmDialogBuilder { TextStyle? _styleTitle; TextStyle? _styleContent; double? _radiusButton; - double? heightButton; EdgeInsetsGeometry? _paddingTitle; EdgeInsets? _paddingContent; EdgeInsets? _paddingButton; EdgeInsets? _outsideDialogPadding; - EdgeInsets? _marginIcon; + EdgeInsetsGeometry? _marginIcon; EdgeInsets? _margin; double? _widthDialog; - double? _heightDialog; double maxWith; Alignment? _alignment; Color? _backgroundColor; bool showAsBottomSheet; List? listTextSpan; + int? titleActionButtonMaxLines; + bool isArrangeActionButtonsVertical; OnConfirmButtonAction? _onConfirmButtonAction; OnCancelButtonAction? _onCancelButtonAction; @@ -46,8 +46,9 @@ class ConfirmDialogBuilder { { this.showAsBottomSheet = false, this.listTextSpan, - this.heightButton, this.maxWith = double.infinity, + this.titleActionButtonMaxLines, + this.isArrangeActionButtonsVertical = false, } ); @@ -107,7 +108,7 @@ class ConfirmDialogBuilder { _paddingButton = value; } - void marginIcon(EdgeInsets? value) { + void marginIcon(EdgeInsetsGeometry? value) { _marginIcon = value; } @@ -119,10 +120,6 @@ class ConfirmDialogBuilder { _widthDialog = value; } - void heightDialog(double? value) { - _heightDialog = value; - } - void alignment(Alignment? alignment) { _alignment = alignment; } @@ -170,7 +167,6 @@ class ConfirmDialogBuilder { Widget _bodyContent() { return Container( width: _widthDialog ?? 400, - height: _heightDialog, constraints: BoxConstraints(maxWidth: maxWith), decoration: const BoxDecoration( color: Colors.white, @@ -231,8 +227,33 @@ class ConfirmDialogBuilder { ), ), ), - Padding( - padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + if (isArrangeActionButtonsVertical) + ...[ + if (_cancelText.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(top: 8, start: 16, end: 16), + child: _buildButton( + name: _cancelText, + bgColor: _colorCancelButton, + radius: _radiusButton, + textStyle: _styleTextCancelButton, + action: _onCancelButtonAction), + ), + if (_confirmText.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(top: 8, start: 16, end: 16), + child: _buildButton( + name: _confirmText, + bgColor: _colorConfirmButton, + radius: _radiusButton, + textStyle: _styleTextConfirmButton, + action: _onConfirmButtonAction), + ), + const SizedBox(height: 16), + ] + else + Padding( + padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 16, end: 16), child: Row( children: [ if (_cancelText.isNotEmpty) @@ -240,16 +261,14 @@ class ConfirmDialogBuilder { name: _cancelText, bgColor: _colorCancelButton, radius: _radiusButton, - height: heightButton, textStyle: _styleTextCancelButton, action: _onCancelButtonAction)), - if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 16), + if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 8), if (_confirmText.isNotEmpty) Expanded(child: _buildButton( name: _confirmText, bgColor: _colorConfirmButton, radius: _radiusButton, - height: heightButton, textStyle: _styleTextConfirmButton, action: _onConfirmButtonAction)) ] @@ -264,30 +283,33 @@ class ConfirmDialogBuilder { TextStyle? textStyle, Color? bgColor, double? radius, - double? height, - Function? action + Function()? action }) { return SizedBox( width: double.infinity, - height: height ?? 48, + height: titleActionButtonMaxLines == 1 ? 45 : null, child: ElevatedButton( - onPressed: () => action?.call(), - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) => bgColor ?? AppColor.colorTextButton), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => bgColor ?? AppColor.colorTextButton), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius ?? 8), - side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), - )), - padding: MaterialStateProperty.resolveWith( - (Set states) => const EdgeInsets.symmetric(horizontal: 16)), - elevation: MaterialStateProperty.resolveWith((Set states) => 0)), - child: Text(name ?? '', - textAlign: TextAlign.center, - style: textStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)), - ) + onPressed: action, + style: ElevatedButton.styleFrom( + foregroundColor: bgColor ?? AppColor.colorTextButton, + backgroundColor: bgColor ?? AppColor.colorTextButton, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius ?? 8), + side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), + ), + padding: const EdgeInsets.all(8), + elevation: 0 + ), + child: Text( + name ?? '', + textAlign: TextAlign.center, + maxLines: titleActionButtonMaxLines, + style: textStyle ?? const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.white + )), + ), ); } } diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index 3b0fdacea8..964a841495 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -65,12 +65,12 @@ class _HtmlContentViewState extends State { super.initState(); if (PlatformInfo.isAndroid) { _gestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer()), Factory(() => ScaleGestureRecognizer()), }; } else { _gestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)), }; } if (PlatformInfo.isAndroid) { @@ -211,7 +211,7 @@ class _HtmlContentViewState extends State { if (!isScrollActivated && PlatformInfo.isIOS) { newGestureRecognizers = { - Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDuration)), + Factory(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)), Factory(() => HorizontalDragGestureRecognizer()) }; } @@ -272,7 +272,7 @@ class _HtmlContentViewState extends State { return NavigationActionPolicy.CANCEL; } - Duration? get _longPressGestureDuration => const Duration(milliseconds: 100); + Duration? get _longPressGestureDurationIOS => const Duration(milliseconds: 100); @override void dispose() { diff --git a/core/lib/presentation/views/text/rich_text_builder.dart b/core/lib/presentation/views/text/rich_text_builder.dart index 7c4c2f2a1c..9e1c368793 100644 --- a/core/lib/presentation/views/text/rich_text_builder.dart +++ b/core/lib/presentation/views/text/rich_text_builder.dart @@ -1,66 +1,62 @@ -import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; -class RichTextBuilder { +class RichTextBuilder extends StatelessWidget { - final String _textOrigin; - final String _wordToStyle; - final TextStyle _styleOrigin; - final TextStyle _styleWord; + final String textOrigin; + final String wordToStyle; + final TextStyle styleOrigin; + final TextStyle styleWord; - Key? _key; - int? _maxLines; - TextOverflow? _overflow; + const RichTextBuilder({ + super.key, + required this.textOrigin, + required this.wordToStyle, + required this.styleOrigin, + required this.styleWord, + }); - RichTextBuilder( - this._textOrigin, - this._wordToStyle, - this._styleOrigin, - this._styleWord, - ); - - void key(Key key) { - _key = key; - } - - void maxLines(int maxLines) { - _maxLines = maxLines; - } - - void setOverflow(TextOverflow textOverflow) { - _overflow = textOverflow; - } - - RichText build() { - return RichText( - key: _key, - maxLines: _maxLines ?? 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: _overflow ?? CommonTextStyle.defaultTextOverFlow, - text: TextSpan( - style: _styleOrigin, - children: _getSpans(_textOrigin, _wordToStyle, _styleWord))); + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + style: styleOrigin, + children: _getSpans( + text: textOrigin, + word: wordToStyle, + styleOrigin: styleOrigin, + styleWord: styleWord, + ) + ), + style: styleOrigin, + maxLines: 1, + overflow: TextOverflow.ellipsis + ); } - List _getSpans(String text, String matchWord, TextStyle style) { + List _getSpans({ + required String text, + required String word, + required TextStyle styleOrigin, + required TextStyle styleWord, + }) { List spans = []; int spanBoundary = 0; do { // look for the next match - final startIndex = text.toLowerCase().indexOf(matchWord.toLowerCase(), spanBoundary); + final startIndex = text.toLowerCase().indexOf(word.toLowerCase(), spanBoundary); // if no more matches then add the rest of the string without style if (startIndex == -1) { - spans.add(TextSpan(text: text.substring(spanBoundary))); + spans.add(TextSpan(text: text.substring(spanBoundary), style: styleOrigin)); return spans; } // add any unStyled text before the next match if (startIndex > spanBoundary) { - spans.add(TextSpan(text: text.substring(spanBoundary, startIndex))); + spans.add(TextSpan(text: text.substring(spanBoundary, startIndex), style: styleOrigin)); } // style the matched text - final endIndex = startIndex + matchWord.length; + final endIndex = startIndex + word.length; final spanText = text.substring(startIndex, endIndex); - spans.add(TextSpan(text: spanText, style: style)); + spans.add(TextSpan(text: spanText, style: styleWord)); // mark the boundary to start the next search from spanBoundary = endIndex; // continue until there are no more matches diff --git a/core/lib/presentation/views/text/text_overflow_builder.dart b/core/lib/presentation/views/text/text_overflow_builder.dart index d10030f0dc..46669ef449 100644 --- a/core/lib/presentation/views/text/text_overflow_builder.dart +++ b/core/lib/presentation/views/text/text_overflow_builder.dart @@ -1,7 +1,4 @@ -import 'package:core/presentation/extensions/string_extension.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; class TextOverflowBuilder extends StatelessWidget { @@ -14,22 +11,23 @@ class TextOverflowBuilder extends StatelessWidget { final TextDirection? textDirection; final TextOverflow? overflow; - const TextOverflowBuilder(this.data, { - super.key, - this.style, - this.textAlign, - this.softWrap = CommonTextStyle.defaultSoftWrap, - this.maxLines = 1, - this.textDirection, - this.overflow = CommonTextStyle.defaultTextOverFlow, - }); + const TextOverflowBuilder( + this.data, + { + super.key, + this.style, + this.textAlign, + this.softWrap = true, + this.maxLines = 1, + this.textDirection, + this.overflow = TextOverflow.ellipsis, + } + ); @override Widget build(BuildContext context) { return Text( - DirectionUtils.isDirectionRTLByLanguage(context) - ? data - : data.overflow, + data, style: style, textAlign: textAlign, softWrap: softWrap, diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart new file mode 100644 index 0000000000..5f9c944e7c --- /dev/null +++ b/core/lib/utils/application_manager.dart @@ -0,0 +1,60 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fk_user_agent/fk_user_agent.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class ApplicationManager { + + final DeviceInfoPlugin _deviceInfoPlugin; + + ApplicationManager(this._deviceInfoPlugin); + + Future getPackageInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + log('ApplicationManager::getPackageInto: $packageInfo'); + return packageInfo; + } + + Future getVersion() async { + final version = (await getPackageInfo()).version; + log('ApplicationManager::getVersion: $version'); + return version; + } + + Future getUserAgent() async { + try { + String userAgent = ''; + if (PlatformInfo.isWeb) { + final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo; + userAgent = webBrowserInfo.userAgent ?? ''; + } else if (PlatformInfo.isMobile) { + userAgent = FkUserAgent.userAgent ?? ''; + } + log('ApplicationManager::getUserAgent: $userAgent'); + return userAgent; + } catch(e) { + logError('ApplicationManager::getUserAgent: Exception: $e'); + return ''; + } + } + + Future initUserAgent() async { + if (PlatformInfo.isMobile) { + await FkUserAgent.init(); + } + } + + Future releaseUserAgent() async { + if (PlatformInfo.isMobile) { + FkUserAgent.release(); + } + } + + Future generateApplicationUserAgent() async { + final userAgent = await getUserAgent(); + final version = await getVersion(); + return 'Twake-Mail/$version $userAgent'; + } +} \ No newline at end of file diff --git a/core/lib/utils/list_utils.dart b/core/lib/utils/list_utils.dart new file mode 100644 index 0000000000..9449a9e237 --- /dev/null +++ b/core/lib/utils/list_utils.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; + +Tuple2, List> partition(List list, bool Function(T) predicate) { + List trueList = []; + List falseList = []; + + for (var element in list) { + if (predicate(element)) { + trueList.add(element); + } else { + falseList.add(element); + } + } + + return Tuple2(trueList, falseList); +} \ No newline at end of file diff --git a/core/lib/utils/mail/domain.dart b/core/lib/utils/mail/domain.dart new file mode 100644 index 0000000000..f57d210f37 --- /dev/null +++ b/core/lib/utils/mail/domain.dart @@ -0,0 +1,107 @@ +import 'dart:io'; + +import 'package:core/utils/app_logger.dart'; +import 'package:equatable/equatable.dart'; + +class Domain with EquatableMixin { + static final RegExp _dashMatcher = RegExp(r'[-_]'); + static final RegExp _digitMatcher = RegExp(r'\d'); + static final RegExp _partCharMatcher = RegExp(r'[A-Za-z0-9_\-.]'); + + static final Domain localhost = Domain.of('localhost'); + static const int maximumDomainLength = 253; + + static String _removeBrackets(String domainName) { + if (!(domainName.startsWith('[') && domainName.endsWith(']'))) { + return domainName; + } + return domainName.substring(1, domainName.length - 1); + } + + static bool _allCharactersMatchRegex(String input, RegExp regex) { + for (int i = 0; i < input.length; i++) { + if (!regex.hasMatch(input[i])) { + return false; + } + } + return true; + } + + static Domain of(String? domain) { + assert(domain != null, 'Domain can not be null'); + assert(domain!.isNotEmpty, 'Domain can not be empty'); + assert(domain!.length <= maximumDomainLength, 'Domain name length should not exceed $maximumDomainLength characters'); + + String domainWithoutBrackets = _removeBrackets(domain!); + assert(_allCharactersMatchRegex(domainWithoutBrackets, _partCharMatcher), 'Domain parts ASCII chars must be a-z A-Z 0-9 - or _'); + + int pos = 0; + int nextDot = domainWithoutBrackets.indexOf('.'); + + while (nextDot > -1) { + if (pos + 1 > domainWithoutBrackets.length) { + throw ArgumentError('Last domain part should not be empty'); + } + _assertValidPart(domainWithoutBrackets, pos, nextDot); + pos = nextDot + 1; + nextDot = domainWithoutBrackets.indexOf('.', pos); + } + _assertValidPart(domainWithoutBrackets, pos, domainWithoutBrackets.length); + _assertValidLastPart(domainWithoutBrackets, pos); + + return Domain._(domainWithoutBrackets); + } + + static void _assertValidPart(String domainPart, int begin, int end) { + assert(begin != end, "Domain part should not be empty"); + assert(!_dashMatcher.hasMatch(domainPart[begin]), "Domain part should not start with '-' or '_'"); + assert(!_dashMatcher.hasMatch(domainPart[end - 1]), "Domain part should not end with '-' or '_'"); + assert(end - begin <= 63, "Domain part should not not exceed 63 characters"); + } + + static void _assertValidLastPart(String domainPart, int pos) { + bool onlyDigits = _digitMatcher.hasMatch(domainPart[pos]); + bool invalid = onlyDigits && !_validIPAddress(domainPart); + assert(!invalid, "The last domain part must not start with 0-9"); + } + + static bool _validIPAddress(String value) { + try { + InternetAddress(value); + return true; + } catch (e) { + logError('Domain::validIPAddress: Exception = $e'); + return false; + } + } + + final String domainName; + final String normalizedDomainName; + + Domain._(this.domainName) : normalizedDomainName = _removeBrackets(domainName.toLowerCase()); + + String name() => domainName; + + String asString() => normalizedDomainName; + + @override + bool operator ==(Object other) { + if (other is Domain) { + return normalizedDomainName == other.normalizedDomainName; + } + return false; + } + + @override + int get hashCode { + return normalizedDomainName.hashCode; + } + + @override + String toString() { + return "Domain : $domainName"; + } + + @override + List get props => [domainName]; +} \ No newline at end of file diff --git a/core/lib/utils/mail/mail_address.dart b/core/lib/utils/mail/mail_address.dart new file mode 100644 index 0000000000..a791220063 --- /dev/null +++ b/core/lib/utils/mail/mail_address.dart @@ -0,0 +1,421 @@ +import 'package:core/domain/exceptions/address_exception.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/mail/domain.dart'; +import 'package:equatable/equatable.dart'; + +class MailAddress with EquatableMixin { + static final List SPECIAL = [ + '<', + '>', + '(', + ')', + '[', + ']', + '\\', + '.', + ',', + ';', + ':', + '@', + '\"' + ]; + + final String localPart; + final Domain domain; + + MailAddress({required this.localPart, required this.domain}); + + factory MailAddress.validateAddress(String address) { + log('MailAddress::validate: Address = $address'); + String localPart; + Domain domain; + + address = address.trim(); + if (address.isEmpty) { + throw AddressException('Addresses should not be empty'); + } + int pos = 0; + + // Test if mail address has source routing information (RFC-821) and get rid of it!! + // must be called first!! (or at least prior to updating pos) + _stripSourceRoute(address, pos); + + StringBuffer localPartSB = StringBuffer(); + StringBuffer domainSB = StringBuffer(); + // Begin parsing + // ::= "@" + + try { + // parse local-part + // ::= | + if (address[pos] == '\"') { + pos = _parseQuotedLocalPartOrThrowException(localPartSB, address, pos); + } else { + pos = parseUnquotedLocalPartOrThrowException(localPartSB, address, pos); + } + // find @ + if (pos >= address.length || address[pos] != '@') { + throw AddressException('Did not find @ between local-part and domain at position ${pos + 1} in "$address"'); + } + pos++; + // parse domain + // ::= | "." + // ::= | "#" | "[" "]" + while (true) { + if (pos >= address.length) { + break; + } + var postChar = address[pos]; + if (postChar == '#') { + pos = _parseNumber(domainSB, address, pos); + } else if (postChar == '[') { + pos = _parseDomainLiteral(domainSB, address, pos); + } else { + pos = _parseDomain(domainSB, address, pos); + } + if (pos >= address.length) { + break; + } + postChar = address[pos]; + if (postChar == '.') { + var lastChar = address[pos - 1]; + if (lastChar == '@' || lastChar == '.') { + throw AddressException('Subdomain expected before "." or duplicate "." in "address"'); + } + domainSB.write('.'); + pos++; + continue; + } + break; + } + if (domainSB.length == 0) { + throw AddressException('No domain found at position ${pos + 1} in "$address"'); + } + } catch (e) { + logError('MailAddress::validate: Exception = $e'); + if (e is AddressException) { + rethrow; + } else { + throw AddressException('Out of data at position ${pos + 1} in "$address"'); + } + } + + localPart = localPartSB.toString(); + + if (localPart.startsWith('.') || + localPart.endsWith('.') || + _haveDoubleDot(localPart)) { + throw AddressException('Addresses cannot start end with "." or contain two consecutive dots'); + } + + domain = _createDomain(domainSB.toString()); + + return MailAddress(localPart: localPart, domain: domain); + } + + factory MailAddress.validateLocalPartAndDomain({required String localPart, required dynamic domain}) { + if (domain is Domain) { + return MailAddress.validateAddress('$localPart@${domain.name()}'); + } else { + return MailAddress.validateAddress('$localPart@$domain'); + } + } + + String asString() { + return '$localPart@${domain.asString()}'; + } + + String asPrettyString() { + return '<${asString()}>'; + } + + Domain getDomain() { + return domain; + } + + String getLocalPart() { + return localPart; + } + + @override + String toString() { + return '$localPart@${domain.asString()}'; + } + + static bool _haveDoubleDot(String localPart) { + return localPart.contains('..'); + } + + static Domain _createDomain(String domain) { + try { + return Domain.of(domain); + } catch (e) { + throw AddressException(e.toString()); + } + } + + static int _parseNumber(StringBuffer dSB, String address, int pos) { + // ::= | + + // we were passed the string with pos pointing the the # char. + // take the first char (#), put it in the result buffer and increment pos + var postChar = address[pos]; + dSB.write(postChar); + pos++; + // We keep the position from the class level pos field + while (true) { + if (pos >= address.length) { + break; + } + // ::= any one of the ten digits 0 through 9 + var d = address[pos]; + if (d == '.') { + break; + } + if (d.compareTo('0') < 0 || d.compareTo('9') > 0) { + throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"'); + } + dSB.write(d); + pos++; + } + if (dSB.length < 2) { + throw AddressException('In domain, did not find a number in # address at position ${pos + 1} in "$address"'); + } + return pos; + } + + static int _parseDomainLiteral(StringBuffer dSB, String address, int pos) { + // we were passed the string with pos pointing the the [ char. + // take the first char ([), put it in the result buffer and increment pos + var posChar = address[pos]; + dSB.write(posChar); + pos++; + + // ::= "." "." "." + for (int octet = 0; octet < 4; octet++) { + // ::= one, two, or three digits representing a decimal + // integer value in the range 0 through 255 + // ::= any one of the ten digits 0 through 9 + StringBuffer snumSB = StringBuffer(); + for (int digits = 0; digits < 3; digits++) { + String currentChar = address[pos]; + if (currentChar == '.' || currentChar == ']') { + break; + } else if (currentChar.compareTo('0') < 0 || + currentChar.compareTo('9') > 0) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + snumSB.write(currentChar); + pos++; + } + if (snumSB.length == 0) { + throw AddressException('Number not found at position ${pos + 1} in "$address"'); + } + try { + int snum = int.parse(snumSB.toString()); + if (snum > 255) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + } catch (e) { + throw AddressException('Invalid number at position ${pos + 1} in "$address"'); + } + dSB.write(snumSB.toString()); + var posChar = address[pos]; + if (posChar == ']') { + if (octet < 3) { + throw AddressException('End of number reached too quickly at ${pos + 1} in "$address"'); + } + break; + } + if (posChar == '.') { + dSB.write('.'); + pos++; + } + } + posChar = address[pos]; + if (posChar != ']') { + throw AddressException('Did not find closing bracket \"]\" in domain at position ${pos + 1} in "$address"'); + } + dSB.write(']'); + pos++; + return pos; + } + + static int _parseDomain(StringBuffer dSB, String address, int pos) { + StringBuffer resultSB = StringBuffer(); + // ::= + // ::= | + // ::= | + // ::= | | "-" + // ::= any one of the 52 alphabetic characters A through Z + // in upper case and a through z in lower case + // ::= any one of the ten digits 0 through 9 + + // basically, this is a series of letters, digits, and hyphens, + // but it can't start with a digit or hypthen + // and can't end with a hyphen + + // in practice though, we should relax this as domain names can start + // with digits as well as letters. So only check that doesn't start + // or end with hyphen. + while (true) { + if (pos >= address.length) { + break; + } + var ch = address[pos]; + if ((ch.compareTo('0') >= 0 && ch.compareTo('9') <= 0) || + (ch.compareTo('a') >= 0 && ch.compareTo('z') <= 0) || + (ch.compareTo('A') >= 0 && ch.compareTo('Z') <= 0) || + (ch == '-')) { + resultSB.write(ch); + pos++; + continue; + } + if (ch == '.') { + break; + } + throw AddressException('Invalid character at $pos in "$address"'); + } + String result = resultSB.toString(); + if (result.startsWith('-') || result.endsWith('-')) { + throw AddressException('Domain name cannot begin or end with a hyphen \"-\" at position ${pos + 1} in "$address"'); + } + dSB.write(result); + return pos; + } + + static int _stripSourceRoute(String address, int pos) { + var posChar = address[pos]; + if (pos < address.length && posChar == '@') { + int i = address.indexOf(':'); + if (i != -1) { + pos = i + 1; + } + } + return pos; + } + + static int _parseUnquotedLocalPart(StringBuffer lpSB, String address, int pos) { + // ::= | "." + bool lastCharDot = false; + while (true) { + if (pos >= address.length) { + break; + } + // ::= | + // ::= | "\" + var postChar = address[pos]; + if (postChar == '\\') { + lpSB.write('\\'); + pos++; + // ::= any one of the 128 ASCII characters (no exceptions) + var x = address[pos]; + if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) { + throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"'); + } + lpSB.write(x); + pos++; + lastCharDot = false; + } else if (postChar == '.') { + if (pos == 0) { + throw AddressException('Local part must not start with a "."'); + } + lpSB.write('.'); + pos++; + lastCharDot = true; + } else if (postChar == '@') { + // End of local-part + break; + } else { + // ::= any one of the 128 ASCII characters, but not any + // or + // ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "." + // | "," | ";" | ":" | "@" """ | the control + // characters (ASCII codes 0 through 31 inclusive and + // 127) + // ::= the space character (ASCII code 32) + var c = address[pos]; + if (c.codeUnitAt(0) <= 31 || c.codeUnitAt(0) >= 127 || c == ' ') { + throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"'); + } + int i = 0; + while (i < SPECIAL.length) { + if (c == SPECIAL[i]) { + throw AddressException('Invalid character in local-part (user account) at position ${pos + 1} in "$address"'); + } + i++; + } + lpSB.write(c); + pos++; + lastCharDot = false; + } + } + if (lastCharDot) { + throw AddressException('local-part (user account) ended with a \".\", which is invalid in address "$address"'); + } + return pos; + } + + static int parseUnquotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) { + pos = _parseUnquotedLocalPart(localPartSB, address, pos); + if (localPartSB.length == 0) { + throw AddressException('No local-part (user account) found at position ${pos + 1} in "$address"'); + } + return pos; + } + + static int _parseQuotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos) { + pos = _parseQuotedLocalPart(localPartSB, address, pos); + if (localPartSB.length == 2) { + throw AddressException('No quoted local-part (user account) found at position ${pos + 2} in "$address"'); + } + return pos; + } + + static int _parseQuotedLocalPart(StringBuffer lpSB, String address, int pos) { + lpSB.write('\"'); + pos++; + // ::= """ """ + // ::= "\" | "\" | | + while (true) { + if (pos >= address.length) { + break; + } + var postChar = address[pos]; + if (postChar == '\"') { + lpSB.write('\"'); + // end of quoted string... move forward + pos++; + break; + } + if (postChar == '\\') { + lpSB.write('\\'); + pos++; + // ::= any one of the 128 ASCII characters (no exceptions) + var x = address[pos]; + if (x.codeUnitAt(0) < 0 || x.codeUnitAt(0) > 127) { + throw AddressException('Invalid \\ syntax character at position ${pos + 1} in "$address"'); + } + lpSB.write(x); + pos++; + } else { + // ::= any one of the 128 ASCII characters except , + // , quote ("), or backslash (\) + var q = address[pos]; + if (q.codeUnitAt(0) <= 0 || + q == '\n' || + q == '\r' || + q == '\"' || + q == '\\') { + throw AddressException('Unquoted local-part (user account) must be one of the 128 ASCII characters exception , , quote (\"), or backslash (\\) at position ${pos + 1} in "$address"'); + } + lpSB.write(q); + pos++; + } + } + return pos; + } + + @override + List get props => [localPart, domain]; +} diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart index 6978807b34..adebd0cad1 100644 --- a/core/lib/utils/platform_info.dart +++ b/core/lib/utils/platform_info.dart @@ -1,17 +1,15 @@ -import 'dart:io'; - import 'package:core/utils/app_logger.dart'; import 'package:core/utils/web_renderer/canvas_kit.dart'; import 'package:flutter/foundation.dart'; abstract class PlatformInfo { static const bool isWeb = kIsWeb; - static bool get isLinux => !kIsWeb && Platform.isLinux; - static bool get isWindows => !kIsWeb && Platform.isWindows; - static bool get isMacOS => !kIsWeb && Platform.isMacOS; - static bool get isFuchsia => !kIsWeb && Platform.isFuchsia; - static bool get isIOS => !kIsWeb && Platform.isIOS; - static bool get isAndroid => !kIsWeb && Platform.isAndroid; + static bool get isLinux => !kIsWeb && defaultTargetPlatform == TargetPlatform.linux; + static bool get isWindows => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows; + static bool get isMacOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; + static bool get isFuchsia => !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia; + static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; static bool get isMobile => isAndroid || isIOS; static bool get isDesktop => isLinux || isWindows || isMacOS; static bool get isCanvasKit => isRendererCanvasKit; diff --git a/core/pubspec.lock b/core/pubspec.lock index 994ccee134..1082651361 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + fk_user_agent: + dependency: "direct main" + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: "direct main" description: @@ -440,6 +448,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 8c8531d11a..6f94e72e02 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -75,6 +75,10 @@ dependencies: printing: 5.12.0 + package_info_plus: 4.2.0 + + fk_user_agent: 2.1.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/core/test/utils/domain_test.dart b/core/test/utils/domain_test.dart new file mode 100644 index 0000000000..17c804dbed --- /dev/null +++ b/core/test/utils/domain_test.dart @@ -0,0 +1,79 @@ + +import 'package:core/utils/mail/domain.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Domain.of(args) should not be case sensitive', () { + expect(Domain.of('Domain'), equals(Domain.of('domain'))); + }); + + group('Domain.of(arg) should throw an AssertionError with a list of invalid domains', () { + final listDomainInValid = [ + 'domain\$bad.com', + '', + 'aab..ddd', + 'aab.cc.1com', + 'abc.abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcd.com', + 'domain\$bad.com', + 'domain/bad.com', + 'domain\\bad.com', + 'domain@bad.com', + 'domain@bad.com', + 'domain%bad.com', + '#domain.com', + 'bad-.com', + 'bad_.com', + '-bad.com', + 'bad_.com', + '[domain.tld', + 'domain.tld]', + 'a[aaa]a', + '[aaa]a', + 'a[aaa]', + '[]' + ]; + for (var arg in listDomainInValid) { + test(arg, () { + expect(() => Domain.of(arg), throwsA(const TypeMatcher())); + }); + } + }); + + group('Domain.of(arg) should not throw any exceptions with the list of valid domains', () { + final listDomainValid = [ + '127.0.0.1', + 'domain.tld', + 'do-main.tld', + 'do_main.tld', + 'ab.dc.de.fr', + '123.456.789.a23', + 'acv.abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc.fr', + 'ab--cv.fr', + 'ab__cd.fr', + 'domain', + '[domain]', + '127.0.0.1' + ]; + for (var arg in listDomainValid) { + test(arg, () { + expect(() => Domain.of(arg), returnsNormally); + }); + } + }); + + test('Domain.of(args) should remove brackets', () { + expect(Domain.of('[domain]'), equals(Domain.of('domain'))); + }); + + test('Domain.of(args) should throw AssertionError when args is null', () { + expect(() => Domain.of(null), throwsA(const TypeMatcher())); + }); + + test('Domain.of(args) should allow 253 long domain', () { + expect(Domain.of('${'aaaaaaaaa.' * 25}aaa').domainName.length, 253); + }); + + test('Domain.of(args) should throw AssertionError when too long', () { + expect(() => Domain.of('a' * 254), throwsA(const TypeMatcher())); + }); +} diff --git a/core/test/utils/mail_address_test.dart b/core/test/utils/mail_address_test.dart new file mode 100644 index 0000000000..2265458a8c --- /dev/null +++ b/core/test/utils/mail_address_test.dart @@ -0,0 +1,169 @@ +import 'package:core/domain/exceptions/address_exception.dart'; +import 'package:core/utils/mail/domain.dart'; +import 'package:core/utils/mail/mail_address.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const String GOOD_LOCAL_PART = "\"quoted@local part\""; + const String GOOD_QUOTED_LOCAL_PART = "\"quoted@local part\"@james.apache.org"; + const String GOOD_ADDRESS = "server-dev@james.apache.org"; + final Domain GOOD_DOMAIN = Domain.of("james.apache.org"); + + final List goodAddresses = [ + GOOD_ADDRESS, + GOOD_QUOTED_LOCAL_PART, + "server-dev@james-apache.org", + "server-dev@[127.0.0.1]", + "server.dev@james.apache.org", + "\\.server-dev@james.apache.org", + "Abc@10.42.0.1", + "Abc.123@example.com", + "user+mailbox/department=shipping@example.com", + "user+mailbox@example.com", + "\"Abc@def\"@example.com", + "\"Fred Bloggs\"@example.com", + "\"Joe.\\Blow\"@example.com", + "!#\$%&'*+-/=?^_`.{|}~@example.com" + ]; + + final List badAddresses = [ + "", + "server-dev", + "server-dev@", + "[]", + "server-dev@[]", + "server-dev@#", + "quoted local-part@james.apache.org", + "quoted@local-part@james.apache.org", + "local-part.@james.apache.org", + ".local-part@james.apache.org", + "local-part@.james.apache.org", + "local-part@james.apache.org.", + "local-part@james.apache..org", + "server-dev@-james.apache.org", + "server-dev@james.apache.org-", + "server-dev@#james.apache.org", + "server-dev@#123james.apache.org", + "server-dev@#-123.james.apache.org", + "server-dev@james. apache.org", + "server-dev@james\\.apache.org", + "server-dev@[300.0.0.1]", + "server-dev@[127.0.1]", + "server-dev@[0127.0.0.1]", + "server-dev@[127.0.1.1a]", + "server-dev@[127\\.0.1.1]", + "server-dev@#123", + "server-dev@#123.apache.org", + "server-dev@[127.0.1.1.1]", + "server-dev@[127.0.1.-1]", + "\"a..b\"@domain.com", // jakarta.mail is unable to handle this so we better reject it + "server-dev\\.@james.apache.org", // jakarta.mail is unable to handle this so we better reject it + "a..b@domain.com", + // According to wikipedia these addresses are valid but as jakarta.mail is unable + // to work with them we shall rather reject them (note that this is not breaking retro-compatibility) + "Loïc.Accentué@voilà.fr8", + "pelé@exemple.com", + "δοκιμή@παράδειγμα.δοκιμή", + "我買@屋企.香港", + "二ノ宮@黒川.日本", + "медведь@с-балалайкой.рф", + "संपर्क@डाटामेल.भारत" + ]; + + group('MailAddress simple test', () { + test('MailAddress.validateAddress() should be return MailAddress when address valid', () { + MailAddress mailAddress = MailAddress.validateAddress('user@example.com'); + expect(mailAddress.localPart, equals('user')); + expect(mailAddress.domain.name(), equals('example.com')); + }); + + test('MailAddress.validateAddress() should be throw AddressException when address missing @', () { + expect( + () => MailAddress.validateAddress('userexample.com'), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Did not find @ between local-part and domain at position 16 in "userexample.com"') + ) + ); + }); + + test('MailAddress.validateAddress() should be throw AddressException when address empty local-part', () { + expect( + () => MailAddress.validateAddress('@example.com'), + throwsA(isA().having( + (e) => e.message, + 'message', + 'No local-part (user account) found at position 1 in "@example.com"') + ) + ); + }); + + test('MailAddress.validateAddress() should be throw AddressException when address empty domain', () { + expect( + () => MailAddress.validateAddress('user@'), + throwsA(isA().having( + (e) => e.message, + 'message', + 'No domain found at position 6 in "user@"') + ) + ); + }); + + test('MailAddress.validateAddress() should be throw AddressException when address with a hyphen "-"', () { + expect( + () => MailAddress.validateAddress('user@-example.com'), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Domain name cannot begin or end with a hyphen "-" at position 14 in "user@-example.com"') + ) + ); + }); + }); + + group('MailAddress advanced test', () { + group('MailAddress.validateAddress() should not throw any exceptions with the list of good address', () { + for (var arg in goodAddresses) { + test(arg, () { + expect(() => MailAddress.validateAddress(arg), returnsNormally); + }); + } + }); + + group('MailAddress.validateAddress() should throw an AddressException with a list of bad address', () { + for (var arg in badAddresses) { + test(arg, () { + expect(() => MailAddress.validateAddress(arg), throwsA(const TypeMatcher())); + }); + } + }); + + test('MailAddress.validateLocalPartAndDomain() should not throw any exceptions with good address have LocalPart and Domain', () { + expect(() => MailAddress.validateLocalPartAndDomain(localPart: 'local-part', domain: 'domain'), returnsNormally); + }); + + test('MailAddress.validateLocalPartAndDomain() should throw an AddressException with bad address have LocalPart and Domain', () { + expect(() => MailAddress.validateLocalPartAndDomain(localPart: 'local-part', domain: '-domain'), throwsA(const TypeMatcher())); + }); + + test('MailAddress.validateAddress() should not throw any exceptions with mail address is GOOD_QUOTED_LOCAL_PART', () { + expect(() => MailAddress.validateAddress(GOOD_QUOTED_LOCAL_PART), returnsNormally); + }); + + test('MailAddress.getDomain() should return GOOD_DOMAIN with address is GOOD_ADDRESS', () { + final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS); + expect(mailAddress.getDomain(), equals(GOOD_DOMAIN)); + }); + + test('MailAddress.getLocalPart() should return GOOD_LOCAL_PART with address is GOOD_QUOTED_LOCAL_PART', () { + final mailAddress = MailAddress.validateAddress(GOOD_QUOTED_LOCAL_PART); + expect(mailAddress.getLocalPart(), equals(GOOD_LOCAL_PART)); + }); + + test('MailAddress.toString() should return GOOD_ADDRESS with address is GOOD_ADDRESS', () { + final mailAddress = MailAddress.validateAddress(GOOD_ADDRESS); + expect(mailAddress.toString(), equals(GOOD_ADDRESS)); + }); + }); +} \ No newline at end of file diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock index 3924f67c6b..5047fd12e7 100644 --- a/email_recovery/pubspec.lock +++ b/email_recovery/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/email_recovery/pubspec.yaml b/email_recovery/pubspec.yaml index 1cb39b9c3c..c5bb9b1c8b 100644 --- a/email_recovery/pubspec.yaml +++ b/email_recovery/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/fcm/pubspec.yaml b/fcm/pubspec.yaml index fc1464a52a..bc1e35188b 100644 --- a/fcm/pubspec.yaml +++ b/fcm/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/forward/pubspec.lock b/forward/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/forward/pubspec.yaml b/forward/pubspec.yaml index 94181ea0ca..351f8916cb 100644 --- a/forward/pubspec.yaml +++ b/forward/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 97b14ba252..eacf5b8cb3 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -8,6 +8,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/toast/tmail_toast.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:core/utils/fps_manager.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; @@ -76,6 +77,7 @@ abstract class BaseController extends GetxController final ImagePaths imagePaths = Get.find(); final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); + final ApplicationManager applicationManager = Get.find(); bool _isFcmEnabled = false; diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 88cf77dac1..69c2337c4d 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -15,11 +15,15 @@ mixin MessageDialogActionMixin { { Function? onConfirmAction, Function? onCancelAction, + OnCloseButtonAction? onCloseButtonAction, String? title, String? cancelTitle, bool hasCancelButton = true, bool showAsBottomSheet = false, bool alignCenter = false, + bool outsideDismissible = true, + bool autoPerformPopBack = true, + bool usePopScope = false, List? listTextSpan, Widget? icon, TextStyle? titleStyle, @@ -28,95 +32,123 @@ mixin MessageDialogActionMixin { TextStyle? cancelStyle, Color? actionButtonColor, Color? cancelButtonColor, + EdgeInsetsGeometry? marginIcon, + PopInvokedCallback? onPopInvoked, + bool isArrangeActionButtonsVertical = false, + int? titleActionButtonMaxLines, } ) async { final responsiveUtils = Get.find(); final imagePaths = Get.find(); if (alignCenter) { + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder( + imagePaths, + listTextSpan: listTextSpan, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical + ) + ..key(const Key('confirm_dialog_action')) + ..title(title ?? '') + ..content(message) + ..addIcon(icon) + ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) + ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? (marginIcon ?? const EdgeInsets.only(top: 24)) : null) + ..paddingTitle(icon != null + ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) + : const EdgeInsetsDirectional.symmetric(horizontal: 24) + ) + ..radiusButton(12) + ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(actionName, () { + if (autoPerformPopBack) { + popBack(); + } + onConfirmAction?.call(); + }) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + if (autoPerformPopBack) { + popBack(); + } + onCancelAction?.call(); + } + ) + ..onCloseButtonAction(onCloseButtonAction) + ).build() + ); return await Get.dialog( - PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan, heightButton: 44) + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + barrierDismissible: outsideDismissible + ); + } else { + if (responsiveUtils.isMobile(context)) { + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder( + imagePaths, + showAsBottomSheet: true, + listTextSpan: listTextSpan, + maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical + ) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) ..addIcon(icon) + ..margin(const EdgeInsets.only(bottom: 42)) + ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) ..paddingTitle(icon != null ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) : const EdgeInsetsDirectional.symmetric(horizontal: 24) ) - ..radiusButton(12) - ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..marginIcon(EdgeInsets.zero) + ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { + if (autoPerformPopBack) { popBack(); - onConfirmAction?.call(); + } + onConfirmAction?.call(); }) ..onCancelButtonAction( hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { - popBack(); + () { + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack()) ).build() - ), - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - ); - } else { - if (responsiveUtils.isMobile(context)) { + ); if (showAsBottomSheet) { return await Get.bottomSheet( - PointerInterceptor( - child: (ConfirmDialogBuilder( - imagePaths, - showAsBottomSheet: true, - listTextSpan: listTextSpan, - maxWith: responsiveUtils.getSizeScreenShortestSide(context) - 16 - ) - ..key(const Key('confirm_dialog_action')) - ..title(title ?? '') - ..content(message) - ..addIcon(icon) - ..margin(const EdgeInsets.only(bottom: 42)) - ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) - ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) - ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24) - ) - ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) - ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) - ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) - ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { - popBack(); - onConfirmAction?.call(); - }) - ..onCancelButtonAction( - hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { - popBack(); - onCancelAction?.call(); - } - ) - ..onCloseButtonAction(() => popBack())) - .build() - ), + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, isScrollControlled: true, barrierColor: AppColor.colorDefaultCupertinoActionSheet, backgroundColor: Colors.transparent, + isDismissible: outsideDismissible, enableDrag: true, ignoreSafeArea: false, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(18))), @@ -130,51 +162,68 @@ mixin MessageDialogActionMixin { ..onCancelAction( cancelTitle ?? AppLocalizations.of(context).cancel, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onCancelAction?.call(); } ) ..onConfirmAction(actionName, () { - popBack(); + if (autoPerformPopBack) { + popBack(); + } onConfirmAction?.call(); })).show(); } } else { - return await Get.dialog( - PointerInterceptor( - child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) - ..key(const Key('confirm_dialog_action')) - ..title(title ?? '') - ..content(message) - ..addIcon(icon) - ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) - ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24)) - ..marginIcon(EdgeInsets.zero) - ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) - ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) - ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) - ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) - ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) - ..onConfirmButtonAction(actionName, () { + final childWidget = PointerInterceptor( + child: (ConfirmDialogBuilder( + imagePaths, + listTextSpan: listTextSpan, + titleActionButtonMaxLines: titleActionButtonMaxLines, + isArrangeActionButtonsVertical: isArrangeActionButtonsVertical + ) + ..key(const Key('confirm_dialog_action')) + ..title(title ?? '') + ..content(message) + ..addIcon(icon) + ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) + ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) + ..paddingTitle(icon != null + ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) + : const EdgeInsetsDirectional.symmetric(horizontal: 24)) + ..marginIcon(EdgeInsets.zero) + ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) + ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(actionName, () { + if (autoPerformPopBack) { popBack(); - onConfirmAction?.call(); - }) - ..onCancelButtonAction( - hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', - () { + } + onConfirmAction?.call(); + }) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + if (autoPerformPopBack) { popBack(); - onCancelAction?.call(); } - ) - ..onCloseButtonAction(() => popBack())) - .build() - ), + onCancelAction?.call(); + } + ) + ..onCloseButtonAction(onCloseButtonAction ?? () => popBack()) + ).build() + ); + return await Get.dialog( + usePopScope && PlatformInfo.isMobile + ? PopScope(onPopInvoked: onPopInvoked, canPop: false, child: childWidget) + : childWidget, barrierColor: AppColor.colorDefaultCupertinoActionSheet, + barrierDismissible: outsideDismissible ); } } diff --git a/lib/features/base/widget/application_version_widget.dart b/lib/features/base/widget/application_version_widget.dart new file mode 100644 index 0000000000..8a05bd0407 --- /dev/null +++ b/lib/features/base/widget/application_version_widget.dart @@ -0,0 +1,58 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:flutter/material.dart'; + +class ApplicationVersionWidget extends StatefulWidget { + + final ApplicationManager applicationManager; + final EdgeInsetsGeometry? padding; + final String? title; + final TextStyle? textStyle; + + const ApplicationVersionWidget({ + super.key, + required this.applicationManager, + this.title, + this.textStyle, + this.padding, + }); + + @override + State createState() => _ApplicationVersionWidgetState(); +} + +class _ApplicationVersionWidgetState extends State { + + Future? _versionStream; + + @override + void initState() { + super.initState(); + _versionStream = widget.applicationManager.getVersion(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _versionStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Padding( + padding: widget.padding ?? const EdgeInsets.only(top: 6), + child: Text( + '${widget.title ?? 'v.'}${snapshot.data}', + textAlign: TextAlign.center, + style: widget.textStyle ?? Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w500 + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/base/widget/hyper_link_widget.dart b/lib/features/base/widget/hyper_link_widget.dart index a06d0ac5c5..55cf8d4122 100644 --- a/lib/features/base/widget/hyper_link_widget.dart +++ b/lib/features/base/widget/hyper_link_widget.dart @@ -11,8 +11,8 @@ class HyperLinkWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( text: urlString, style: const TextStyle( color: HyperLinkWidgetStyles.textColor, diff --git a/lib/features/composer/data/repository/composer_repository_impl.dart b/lib/features/composer/data/repository/composer_repository_impl.dart index 196e4de47d..2f502bd40c 100644 --- a/lib/features/composer/data/repository/composer_repository_impl.dart +++ b/lib/features/composer/data/repository/composer_repository_impl.dart @@ -1,19 +1,37 @@ - +import 'package:core/data/constants/constant.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/application_manager.dart'; +import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; +import 'package:uuid/uuid.dart'; class ComposerRepositoryImpl extends ComposerRepository { final AttachmentUploadDataSource _attachmentUploadDataSource; final ComposerDataSource _composerDataSource; + final HtmlDataSource _htmlDataSource; + final ApplicationManager _applicationManager; + final Uuid _uuid; ComposerRepositoryImpl( this._attachmentUploadDataSource, - this._composerDataSource); + this._composerDataSource, + this._htmlDataSource, + this._applicationManager, + this._uuid, + ); @override Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}) { @@ -24,4 +42,64 @@ class ComposerRepositoryImpl extends ComposerRepository { Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}) { return _composerDataSource.downloadImageAsBase64(url, cid, fileInfo, maxWidth: maxWidth, compress: compress); } + + @override + Future generateEmail(CreateEmailRequest createEmailRequest) async { + String emailContent = createEmailRequest.emailContent; + Set emailAttachments = Set.from(createEmailRequest.createAttachments()); + + if (createEmailRequest.inlineAttachments?.isNotEmpty == true) { + final tupleContentInlineAttachments = await _replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: createEmailRequest.inlineAttachments! + ); + + emailContent = tupleContentInlineAttachments.value1; + emailAttachments.addAll(tupleContentInlineAttachments.value2); + } + + if (createEmailRequest.draftsMailboxId == null) { + emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + } + + final userAgent = await _applicationManager.generateApplicationUserAgent(); + final emailBodyPartId = PartId(_uuid.v1()); + + final emailObject = createEmailRequest.generateEmail( + newEmailContent: emailContent, + newEmailAttachments: emailAttachments, + userAgent: userAgent, + partId: emailBodyPartId + ); + + return emailObject; + } + + Future>> _replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + try { + return _htmlDataSource.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments); + } catch (e) { + logError('ComposerRepositoryImpl::_replaceImageBase64ToImageCID: Exception: $e'); + return Future.value( + Tuple2( + emailContent, + inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset) + ) + ); + } + } + + Future _removeCollapsedExpandedSignatureEffect({required String emailContent}) { + try { + return _htmlDataSource.removeCollapsedExpandedSignatureEffect(emailContent: emailContent); + } catch (e) { + logError('ComposerRepositoryImpl::_removeCollapsedExpandedSignatureEffect: Exception: $e'); + return Future.value(emailContent); + } + } } \ No newline at end of file diff --git a/lib/features/composer/domain/exceptions/compose_email_exception.dart b/lib/features/composer/domain/exceptions/compose_email_exception.dart new file mode 100644 index 0000000000..b0d29ed9d2 --- /dev/null +++ b/lib/features/composer/domain/exceptions/compose_email_exception.dart @@ -0,0 +1,3 @@ +class SendingEmailCanceledException implements Exception {} + +class SavingEmailToDraftsCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/composer/domain/extensions/email_request_extension.dart b/lib/features/composer/domain/extensions/email_request_extension.dart index 41591c0c61..83274d99c2 100644 --- a/lib/features/composer/domain/extensions/email_request_extension.dart +++ b/lib/features/composer/domain/extensions/email_request_extension.dart @@ -24,7 +24,6 @@ extension EmailRequestExtension on EmailRequest { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxRequest?.newName, - creationIdRequest: mailboxRequest?.creationId, sendingState: newState, previousEmailId: previousEmailId ); diff --git a/lib/features/composer/domain/repository/composer_repository.dart b/lib/features/composer/domain/repository/composer_repository.dart index 33c9cf4fd7..8ba787980c 100644 --- a/lib/features/composer/domain/repository/composer_repository.dart +++ b/lib/features/composer/domain/repository/composer_repository.dart @@ -1,8 +1,12 @@ import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; abstract class ComposerRepository { + Future generateEmail(CreateEmailRequest createEmailRequest); + Future uploadAttachment(FileInfo fileInfo, Uri uploadUri, {CancelToken? cancelToken}); Future downloadImageAsBase64(String url, String cid, FileInfo fileInfo, {double? maxWidth, bool? compress}); diff --git a/lib/features/composer/domain/state/download_image_as_base64_state.dart b/lib/features/composer/domain/state/download_image_as_base64_state.dart index af801f1a28..9370e13a62 100644 --- a/lib/features/composer/domain/state/download_image_as_base64_state.dart +++ b/lib/features/composer/domain/state/download_image_as_base64_state.dart @@ -10,15 +10,11 @@ class DownloadImageAsBase64Success extends UIState { final String base64Uri; final String cid; final FileInfo fileInfo; - final bool fromFileShared; DownloadImageAsBase64Success( this.base64Uri, this.cid, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -26,7 +22,6 @@ class DownloadImageAsBase64Success extends UIState { base64Uri, cid, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/composer/domain/state/generate_email_state.dart b/lib/features/composer/domain/state/generate_email_state.dart new file mode 100644 index 0000000000..f41c7e300d --- /dev/null +++ b/lib/features/composer/domain/state/generate_email_state.dart @@ -0,0 +1,9 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class GenerateEmailLoading extends LoadingState {} + +class GenerateEmailFailure extends FeatureFailure { + + GenerateEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index e92f9d7192..ae82f2d468 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -4,14 +4,14 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class SaveEmailAsDraftsLoading extends UIState {} +class SaveEmailAsDraftsLoading extends LoadingState {} class SaveEmailAsDraftsSuccess extends UIActionState { - final Email emailAsDrafts; + final EmailId emailId; SaveEmailAsDraftsSuccess( - this.emailAsDrafts, + this.emailId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -19,10 +19,12 @@ class SaveEmailAsDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailAsDrafts, ...super.props]; + List get props => [emailId, ...super.props]; } class SaveEmailAsDraftsFailure extends FeatureFailure { SaveEmailAsDraftsFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file +} + +class CancelSavingEmailToDrafts extends LoadingState {} \ No newline at end of file diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 3aa23b7f00..814e6c3cb6 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -8,7 +8,7 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; -class SendEmailLoading extends UIState {} +class SendEmailLoading extends LoadingState {} class SendEmailSuccess extends UIActionState { @@ -29,17 +29,17 @@ class SendEmailSuccess extends UIActionState { class SendEmailFailure extends FeatureFailure { - final Session session; - final AccountId accountId; - final EmailRequest emailRequest; + final Session? session; + final AccountId? accountId; + final EmailRequest? emailRequest; final CreateNewMailboxRequest? mailboxRequest; final SendingEmailActionType? sendingEmailActionType; SendEmailFailure({ dynamic exception, - required this.session, - required this.accountId, - required this.emailRequest, + this.session, + this.accountId, + this.emailRequest, this.mailboxRequest, this.sendingEmailActionType }) : super(exception: exception); @@ -53,4 +53,6 @@ class SendEmailFailure extends FeatureFailure { mailboxRequest, sendingEmailActionType, ]; -} \ No newline at end of file +} + +class CancelSendingEmail extends LoadingState {} \ No newline at end of file diff --git a/lib/features/composer/domain/state/update_email_drafts_state.dart b/lib/features/composer/domain/state/update_email_drafts_state.dart index af7ae68776..47ca09cca3 100644 --- a/lib/features/composer/domain/state/update_email_drafts_state.dart +++ b/lib/features/composer/domain/state/update_email_drafts_state.dart @@ -4,14 +4,14 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class UpdatingEmailDrafts extends UIState {} +class UpdatingEmailDrafts extends LoadingState {} class UpdateEmailDraftsSuccess extends UIActionState { - final Email emailAsDrafts; + final EmailId emailId; UpdateEmailDraftsSuccess( - this.emailAsDrafts, + this.emailId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -19,7 +19,7 @@ class UpdateEmailDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailAsDrafts, ...super.props]; + List get props => [emailId, ...super.props]; } class UpdateEmailDraftsFailure extends FeatureFailure { diff --git a/lib/features/composer/domain/state/upload_attachment_state.dart b/lib/features/composer/domain/state/upload_attachment_state.dart index c58d04d11e..08f4e9f9a1 100644 --- a/lib/features/composer/domain/state/upload_attachment_state.dart +++ b/lib/features/composer/domain/state/upload_attachment_state.dart @@ -1,46 +1,25 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; class UploadAttachmentSuccess extends UIState { final UploadAttachment uploadAttachment; - final bool isInline; - final bool fromFileShared; - UploadAttachmentSuccess( - this.uploadAttachment, - { - this.isInline = false, - this.fromFileShared = false, - } - ); + UploadAttachmentSuccess(this.uploadAttachment); @override - List get props => [ - uploadAttachment, - isInline, - fromFileShared, - ]; + List get props => [uploadAttachment]; } class UploadAttachmentFailure extends FeatureFailure { - final bool isInline; - final bool fromFileShared; - UploadAttachmentFailure( - dynamic exception, - { - this.isInline = false, - this.fromFileShared = false, - } - ) : super(exception: exception); + final FileInfo fileInfo; + + UploadAttachmentFailure(dynamic exception, this.fileInfo) : super(exception: exception); @override - List get props => [ - isInline, - fromFileShared, - ...super.props - ]; + List get props => [...super.props, fileInfo]; } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart new file mode 100644 index 0000000000..0c20eeff1a --- /dev/null +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -0,0 +1,133 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.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/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; + +class CreateNewAndSaveEmailToDraftsInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ComposerRepository _composerRepository; + + CreateNewAndSaveEmailToDraftsInteractor( + this._emailRepository, + this._mailboxRepository, + this._composerRepository, + ); + + Stream> execute({ + required CreateEmailRequest createEmailRequest, + CancelToken? cancelToken, + }) async* { + try { + yield dartz.Right(GenerateEmailLoading()); + + final listCurrentState = await _getStoredCurrentState( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId + ); + + final emailCreated = await _createEmailObject(createEmailRequest); + + if (emailCreated != null) { + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Right(SaveEmailAsDraftsLoading()); + + final emailDraftSaved = await _emailRepository.saveEmailAsDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + cancelToken: cancelToken + ); + + yield dartz.Right( + SaveEmailAsDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); + } else { + yield dartz.Right(UpdatingEmailDrafts()); + + final emailDraftSaved = await _emailRepository.updateEmailDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + createEmailRequest.draftsEmailId!, + cancelToken: cancelToken + ); + + yield dartz.Right( + UpdateEmailDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); + } + } else { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + } + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::execute: Exception: $e'); + if (e is UnknownError && e.message is List) { + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Left(SaveEmailAsDraftsFailure(SavingEmailToDraftsCanceledException())); + } else { + yield dartz.Left(UpdateEmailDraftsFailure(SavingEmailToDraftsCanceledException())); + } + } else { + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Left(SaveEmailAsDraftsFailure(e)); + } else { + yield dartz.Left(UpdateEmailDraftsFailure(e)); + } + } + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + return emailCreated; + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_createEmailObject: Exception: $e'); + return null; + } + } + + Future?> _getStoredCurrentState({ + required Session session, + required AccountId accountId + }) async { + try { + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ]); + + final mailboxState = listState.first; + final emailState = listState.last; + + return dartz.Tuple2(mailboxState, emailState); + } catch (e) { + logError('CreateNewAndSaveEmailToDraftsInteractor::_getStoredCurrentState: Exception: $e'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart new file mode 100644 index 0000000000..cbff76b4db --- /dev/null +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -0,0 +1,148 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.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/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/create_email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; + +class CreateNewAndSendEmailInteractor { + final EmailRepository _emailRepository; + final MailboxRepository _mailboxRepository; + final ComposerRepository _composerRepository; + + CreateNewAndSendEmailInteractor( + this._emailRepository, + this._mailboxRepository, + this._composerRepository, + ); + + Stream> execute({ + required CreateEmailRequest createEmailRequest, + CancelToken? cancelToken + }) async* { + SendingEmailArguments? sendingEmailArguments; + try { + yield dartz.Right(GenerateEmailLoading()); + + final listCurrentState = await _getStoredCurrentState( + session: createEmailRequest.session, + accountId: createEmailRequest.accountId + ); + + sendingEmailArguments = await _createEmailObject(createEmailRequest); + + if (sendingEmailArguments != null) { + yield dartz.Right(SendEmailLoading()); + + await _emailRepository.sendEmail( + sendingEmailArguments.session, + sendingEmailArguments.accountId, + sendingEmailArguments.emailRequest, + mailboxRequest: sendingEmailArguments.mailboxRequest, + cancelToken: cancelToken + ); + + if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { + await _deleteOldDraftsEmail( + session: sendingEmailArguments.session, + accountId: sendingEmailArguments.accountId, + draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed!, + cancelToken: cancelToken + ); + } + + yield dartz.Right( + SendEmailSuccess( + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2, + emailRequest: sendingEmailArguments.emailRequest + ) + ); + } else { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + } + } catch (e) { + logError('CreateNewAndSendEmailInteractor::execute: Exception: $e'); + if (e is UnknownError && e.message is List) { + yield dartz.Left(SendEmailFailure( + exception: SendingEmailCanceledException(), + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } else { + yield dartz.Left(SendEmailFailure( + exception: e, + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); + } + } + } + + Future _createEmailObject(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + final sendingEmailArgument = createEmailRequest.toSendingEmailArguments(emailObject: emailCreated); + return sendingEmailArgument; + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_createEmailObject: Exception: $e'); + return null; + } + } + + Future?> _getStoredCurrentState({ + required Session session, + required AccountId accountId + }) async { + try { + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ]); + + final mailboxState = listState.first; + final emailState = listState.last; + + return dartz.Tuple2(mailboxState, emailState); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_getStoredCurrentState: Exception: $e'); + return null; + } + } + + Future _deleteOldDraftsEmail({ + required Session session, + required AccountId accountId, + required EmailId draftEmailId, + CancelToken? cancelToken + }) async { + try { + await _emailRepository.deleteEmailPermanently( + session, + accountId, + draftEmailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('CreateNewAndSendEmailInteractor::_deleteOldDraftsEmail: Exception: $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart index b3fb5d0ac5..b3417daa17 100644 --- a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart +++ b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart @@ -16,7 +16,6 @@ class DownloadImageAsBase64Interactor { { double? maxWidth, bool? compress, - bool fromFileShared = false, } ) async* { try { @@ -33,7 +32,6 @@ class DownloadImageAsBase64Interactor { result!, cid, fileInfo, - fromFileShared: fromFileShared )); } else { yield Left(DownloadImageAsBase64Failure(null)); diff --git a/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart new file mode 100644 index 0000000000..cbb6c924d1 --- /dev/null +++ b/lib/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart @@ -0,0 +1,27 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart'; + +class SaveComposerCacheOnWebInteractor { + final ComposerCacheRepository _composerCacheRepository; + final ComposerRepository _composerRepository; + + SaveComposerCacheOnWebInteractor( + this._composerCacheRepository, + this._composerRepository, + ); + + Future> execute(CreateEmailRequest createEmailRequest) async { + try { + final emailCreated = await _composerRepository.generateEmail(createEmailRequest); + _composerCacheRepository.saveComposerCacheOnWeb(emailCreated); + return Right(SaveComposerCacheSuccess()); + } catch (exception) { + return Left(SaveComposerCacheFailure(exception)); + } + } +} diff --git a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart deleted file mode 100644 index 511f3d181c..0000000000 --- a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:core/core.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:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; - -class SaveEmailAsDraftsInteractor { - final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - - SaveEmailAsDraftsInteractor(this._emailRepository, this._mailboxRepository); - - Stream> execute(Session session, AccountId accountId, Email email) async* { - try { - yield Right(SaveEmailAsDraftsLoading()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - - final emailAsDrafts = await _emailRepository.saveEmailAsDrafts(session, accountId, email); - yield Right( - SaveEmailAsDraftsSuccess( - emailAsDrafts, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState - ) - ); - } catch (e) { - yield Left(SaveEmailAsDraftsFailure(e)); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/send_email_interactor.dart b/lib/features/composer/domain/usecases/send_email_interactor.dart index 218d9b71a9..83ade78aca 100644 --- a/lib/features/composer/domain/usecases/send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/send_email_interactor.dart @@ -38,34 +38,24 @@ class SendEmailInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.sendEmail( + await _emailRepository.sendEmail( session, accountId, emailRequest, mailboxRequest: mailboxRequest ); - if (result) { - if (emailRequest.emailIdDestroyed != null) { - await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); - } - - yield Right( - SendEmailSuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState, - emailRequest: emailRequest - ) - ); - } else { - yield Left(SendEmailFailure( - session: session, - accountId: accountId, - emailRequest: emailRequest, - mailboxRequest: mailboxRequest, - sendingEmailActionType: sendingEmailActionType, - )); + if (emailRequest.emailIdDestroyed != null) { + await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); } + + yield Right( + SendEmailSuccess( + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState, + emailRequest: emailRequest + ) + ); } catch (e) { yield Left(SendEmailFailure( exception: e, diff --git a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart deleted file mode 100644 index 77c74ae594..0000000000 --- a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart +++ /dev/null @@ -1,41 +0,0 @@ -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:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; - -class UpdateEmailDraftsInteractor { - final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - - UpdateEmailDraftsInteractor(this._emailRepository, this._mailboxRepository); - - Stream> execute(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) async* { - try { - yield Right(UpdatingEmailDrafts()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - - final newEmailDrafts = await _emailRepository.updateEmailDrafts(session, accountId, newEmail, oldEmailId); - yield Right( - UpdateEmailDraftsSuccess( - newEmailDrafts, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState - ) - ); - } catch (e) { - yield Left(UpdateEmailDraftsFailure(e)); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart index 6abdcf8018..ad1b69da53 100644 --- a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart +++ b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart @@ -15,8 +15,6 @@ class UploadAttachmentInteractor { FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, - bool isInline = false, - bool fromFileShared = false, }) async* { try { final uploadAttachment = await _composerRepository.uploadAttachment( @@ -24,17 +22,9 @@ class UploadAttachmentInteractor { uploadUri, cancelToken: cancelToken ); - yield Right(UploadAttachmentSuccess( - uploadAttachment, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Right(UploadAttachmentSuccess(uploadAttachment)); } catch (e) { - yield Left(UploadAttachmentFailure( - e, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Left(UploadAttachmentFailure(e, fileInfo)); } } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index dda6062d2f..b600963b6e 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -1,5 +1,5 @@ import 'package:core/core.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; @@ -10,9 +10,10 @@ import 'package:tmail_ui_user/features/composer/data/repository/composer_reposit import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; @@ -43,7 +44,6 @@ import 'package:tmail_ui_user/features/mailbox/data/repository/mailbox_repositor import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_ import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart'; import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -103,7 +104,7 @@ class ComposerBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), @@ -125,7 +126,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find())); Get.lazyPut(() => RemoteServerSettingsDataSourceImpl( - Get.find(), + Get.find(), Get.find())); } @@ -146,8 +147,12 @@ class ComposerBindings extends BaseBindings { @override void bindingsRepositoryImpl() { Get.lazyPut(() => ComposerRepositoryImpl( - Get.find(), - Get.find())); + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); Get.lazyPut(() => ContactRepositoryImpl(Get.find())); Get.lazyPut(() => MailboxRepositoryImpl( { @@ -182,47 +187,62 @@ class ComposerBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => LocalFilePickerInteractor()); + Get.lazyPut(() => LocalImagePickerInteractor()); Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); - Get.lazyPut(() => SaveEmailAsDraftsInteractor( - Get.find(), - Get.find())); Get.lazyPut(() => GetEmailContentInteractor(Get.find())); - Get.lazyPut(() => UpdateEmailDraftsInteractor( - Get.find(), - Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); + Get.lazyPut(() => SaveComposerCacheOnWebInteractor( + Get.find(), + Get.find(), + )); Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); Get.lazyPut(() => GetAlwaysReadReceiptSettingInteractor(Get.find())); + Get.lazyPut(() => CreateNewAndSendEmailInteractor( + Get.find(), + Get.find(), + Get.find(), + )); + Get.lazyPut(() => CreateNewAndSaveEmailToDraftsInteractor( + Get.find(), + Get.find(), + Get.find(), + )); IdentityInteractorsBindings().dependencies(); } @override void bindingsController() { - Get.lazyPut(() => RichTextMobileTabletController()); + if (PlatformInfo.isWeb) { + Get.lazyPut(() => RichTextWebController()); + } else { + Get.lazyPut(() => RichTextMobileTabletController()); + } Get.lazyPut(() => UploadController(Get.find())); - Get.lazyPut(() => RichTextWebController()); Get.lazyPut(() => ComposerController( - Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); } void dispose() { + if (PlatformInfo.isWeb) { + Get.delete(); + } else { + Get.delete(); + } Get.delete(); - Get.delete(); - Get.delete(); Get.delete(); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 53770fffd2..ab5a517352 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1,101 +1,102 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; -import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; -import 'package:http_parser/http_parser.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; -import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.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/model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/button_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; -import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; -import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; -import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:universal_html/html.dart' as html; -class ComposerController extends BaseController { +class ComposerController extends BaseController with DragDropFileMixin { final mailboxDashBoardController = Get.find(); - final richTextMobileTabletController = Get.find(); final networkConnectionController = Get.find(); final _dynamicUrlInterceptors = Get.find(); - final expandModeAttachments = ExpandMode.EXPAND.obs; final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; final isInitialRecipient = false.obs; @@ -109,21 +110,21 @@ class ComposerController extends BaseController { final fromRecipientState = PrefixRecipientState.disabled.obs; final ccRecipientState = PrefixRecipientState.disabled.obs; final bccRecipientState = PrefixRecipientState.disabled.obs; - final isSendEmailLoading = false.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); final LocalFilePickerInteractor _localFilePickerInteractor; - final DeviceInfoPlugin _deviceInfoPlugin; + final LocalImagePickerInteractor _localImagePickerInteractor; final GetEmailContentInteractor _getEmailContentInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final UploadController uploadController; final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; - final RichTextWebController richTextWebController; final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; + final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; + final CreateNewAndSaveEmailToDraftsInteractor _createNewAndSaveEmailToDraftsInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -152,8 +153,18 @@ class ComposerController extends BaseController { FocusNode? ccAddressFocusNode; FocusNode? bccAddressFocusNode; FocusNode? searchIdentitiesFocusNode; + FocusNode? toAddressFocusNodeKeyboard; + FocusNode? ccAddressFocusNodeKeyboard; + FocusNode? bccAddressFocusNodeKeyboard; - final RichTextController keyboardRichTextController = RichTextController(); + StreamSubscription? _subscriptionOnBeforeUnload; + StreamSubscription? _subscriptionOnDragEnter; + StreamSubscription? _subscriptionOnDragOver; + StreamSubscription? _subscriptionOnDragLeave; + StreamSubscription? _subscriptionOnDrop; + + RichTextMobileTabletController? richTextMobileTabletController; + RichTextWebController? richTextWebController; final ScrollController scrollController = ScrollController(); final ScrollController scrollControllerEmailAddress = ScrollController(); @@ -168,40 +179,41 @@ class ComposerController extends BaseController { bool isAttachmentCollapsed = false; ButtonState _closeComposerButtonState = ButtonState.enabled; ButtonState _saveToDraftButtonState = ButtonState.enabled; + ButtonState _sendButtonState = ButtonState.enabled; late Worker uploadInlineImageWorker; late Worker dashboardViewStateWorker; late bool _isEmailBodyLoaded; ComposerController( - this._deviceInfoPlugin, this._localFilePickerInteractor, + this._localImagePickerInteractor, this._getEmailContentInteractor, this._getAllIdentitiesInteractor, this.uploadController, this._removeComposerCacheOnWebInteractor, this._saveComposerCacheOnWebInteractor, - this.richTextWebController, this._downloadImageAsBase64Interactor, this._transformHtmlEmailContentInteractor, this._getAlwaysReadReceiptSettingInteractor, + this._createNewAndSendEmailInteractor, + this._createNewAndSaveEmailToDraftsInteractor, ); @override void onInit() { super.onInit(); - createFocusNodeInput(); - scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); - _listenStreamEvent(); - if (PlatformInfo.isMobile) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await FkUserAgent.init(); - }); - } else { + if (PlatformInfo.isWeb) { WidgetsBinding.instance.addPostFrameCallback((_) { _listenBrowserTabRefresh(); }); + richTextWebController = getBinding(); + } else { + richTextMobileTabletController = getBinding(); } + createFocusNodeInput(); + scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); + _listenStreamEvent(); _getAlwaysReadReceiptSetting(); } @@ -223,9 +235,12 @@ class ComposerController extends BaseController { emailContentsViewState.value = Right(UIClosedState()); identitySelected.value = null; listFromIdentities.clear(); - if (PlatformInfo.isMobile) { - FkUserAgent.release(); - } + _subscriptionOnBeforeUnload?.cancel(); + _subscriptionOnDragEnter?.cancel(); + _subscriptionOnDragOver?.cancel(); + _subscriptionOnDragLeave?.cancel(); + _subscriptionOnDrop?.cancel(); + subjectEmailInputFocusNode?.removeListener(_subjectEmailInputFocusListener); super.onClose(); } @@ -239,6 +254,12 @@ class ComposerController extends BaseController { ccAddressFocusNode = null; bccAddressFocusNode?.dispose(); bccAddressFocusNode = null; + toAddressFocusNodeKeyboard?.dispose(); + toAddressFocusNodeKeyboard = null; + ccAddressFocusNodeKeyboard?.dispose(); + ccAddressFocusNodeKeyboard = null; + bccAddressFocusNodeKeyboard?.dispose(); + bccAddressFocusNodeKeyboard = null; searchIdentitiesFocusNode?.dispose(); searchIdentitiesFocusNode = null; subjectEmailInputController.dispose(); @@ -247,7 +268,6 @@ class ComposerController extends BaseController { bccEmailAddressController.dispose(); uploadInlineImageWorker.dispose(); dashboardViewStateWorker.dispose(); - keyboardRichTextController.dispose(); scrollController.dispose(); scrollControllerEmailAddress.removeListener(_scrollControllerEmailAddressListener); scrollControllerEmailAddress.dispose(); @@ -264,7 +284,9 @@ class ComposerController extends BaseController { success is TransformHtmlEmailContentSuccess) { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { - _pickFileSuccess(success); + _handlePickFileSuccess(success); + } else if (success is LocalImagePickerSuccess) { + _handlePickImageSuccess(success); } else if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); } else if (success is GetEmailContentFromCacheSuccess) { @@ -272,23 +294,11 @@ class ComposerController extends BaseController { } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); } else if (success is DownloadImageAsBase64Success) { + final inlineImage = InlineImage(fileInfo: success.fileInfo, base64Uri: success.base64Uri); if (PlatformInfo.isWeb) { - richTextWebController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri)); + richTextWebController?.insertImage(inlineImage); } else { - richTextMobileTabletController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri - ), - fromFileShare: success.fromFileShared - ); + richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { @@ -299,14 +309,13 @@ class ComposerController extends BaseController { @override void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); - if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { - _pickFileFailure(failure); + if (failure is LocalFilePickerFailure) { + _handlePickFileFailure(failure); + } else if (failure is LocalImagePickerFailure) { + _handlePickImageFailure(failure); } else if (failure is GetEmailContentFailure || failure is TransformHtmlEmailContentFailure) { emailContentsViewState.value = Left(failure); - if (isSendEmailLoading.isTrue) { - isSendEmailLoading.value = false; - } } else if (failure is GetAllIdentitiesFailure) { if (identitySelected.value == null) { _autoFocusFieldWhenLauncher(); @@ -333,37 +342,62 @@ class ComposerController extends BaseController { } }); }); - - dashboardViewStateWorker = ever(mailboxDashBoardController.viewState, (state) { - state.fold( - (failure) { - if (failure is SaveEmailAsDraftsFailure || - failure is UpdateEmailDraftsFailure) { - _saveToDraftButtonState = ButtonState.enabled; - } - }, - (success) { - if (success is SaveEmailAsDraftsSuccess) { - _emailIdEditing = success.emailAsDrafts.id; - _saveToDraftButtonState = ButtonState.enabled; - log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:SaveEmailAsDraftsSuccess:emailIdEditing: $_emailIdEditing'); - } else if (success is UpdateEmailDraftsSuccess) { - _emailIdEditing = success.emailAsDrafts.id; - _saveToDraftButtonState = ButtonState.enabled; - log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:UpdateEmailDraftsSuccess:emailIdEditing: $_emailIdEditing'); - } - }); - }); } void _listenBrowserTabRefresh() { - html.window.onBeforeUnload.listen((event) async { - final userProfile = mailboxDashBoardController.userProfile.value; - _removeComposerCacheOnWebInteractor.execute(); - if (userProfile != null) { - final draftEmail = await _generateEmail(currentContext!, userProfile); - _saveComposerCacheOnWebInteractor.execute(draftEmail); + _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { + await _removeComposerCacheOnWebInteractor.execute(); + + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null + ) { + log('ComposerController::_listenBrowserTabRefresh: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + return; } + + final emailContent = await _getContentInEditor(); + + await _saveComposerCacheOnWebInteractor.execute(CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: _getDraftEmailId(), + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + )); + }); + + _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragOver = html.window.onDragOver.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragLeave = html.window.onDragLeave.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; + }); + + _subscriptionOnDrop = html.window.onDrop.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; }); } @@ -381,35 +415,43 @@ class ComposerController extends BaseController { void createFocusNodeInput() { toAddressFocusNode = FocusNode(); - subjectEmailInputFocusNode = FocusNode( - onKey: (focus, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - richTextWebController.editorController.setFocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - ); ccAddressFocusNode = FocusNode(); bccAddressFocusNode = FocusNode(); searchIdentitiesFocusNode = FocusNode(); + toAddressFocusNodeKeyboard = FocusNode(); + ccAddressFocusNodeKeyboard = FocusNode(); + bccAddressFocusNodeKeyboard = FocusNode(); - subjectEmailInputFocusNode?.addListener(() { - log('ComposerController::createFocusNodeInput():subjectEmailInputFocusNode: ${subjectEmailInputFocusNode?.hasFocus}'); - if (subjectEmailInputFocusNode?.hasFocus == true) { - if (PlatformInfo.isMobile) { - htmlEditorApi?.unfocus(); - } - _collapseAllRecipient(); - _autoCreateEmailTag(); + subjectEmailInputFocusNode = FocusNode( + onKey: PlatformInfo.isWeb ? _subjectEmailInputOnKeyListener : null + ); + subjectEmailInputFocusNode?.addListener(_subjectEmailInputFocusListener); + } + + void _subjectEmailInputFocusListener() { + if (subjectEmailInputFocusNode?.hasFocus == true) { + if (PlatformInfo.isMobile) { + htmlEditorApi?.unfocus(); } - }); + _collapseAllRecipient(); + _autoCreateEmailTag(); + } + } + + KeyEventResult _subjectEmailInputOnKeyListener(FocusNode node, RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + richTextWebController?.editorController.setFocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } void onCreatedMobileEditorAction(BuildContext context, HtmlEditorApi editorApi, String? content) { - initTextEditor(content); - richTextMobileTabletController.htmlEditorApi = editorApi; - keyboardRichTextController.onCreateHTMLEditor( + if (identitySelected.value != null) { + initTextEditor(content); + } + richTextMobileTabletController?.htmlEditorApi = editorApi; + richTextMobileTabletController?.richTextController.onCreateHTMLEditor( editorApi, onEnterKeyDown: _onEnterKeyDown, context: context, @@ -424,10 +466,13 @@ class ComposerController extends BaseController { subjectEmailInputFocusNode?.unfocus(); } - void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) { + void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) async { _isEmailBodyLoaded = true; if (identitySelected.value == null) { _getAllIdentities(); + } else { + await _selectIdentity(identitySelected.value); + _autoFocusFieldWhenLauncher(); } } @@ -439,6 +484,8 @@ class ComposerController extends BaseController { if (arguments is ComposerArguments) { composerArguments.value = arguments; + _initIdentities(arguments.identities); + injectAutoCompleteBindings( mailboxDashBoardController.sessionCurrent, mailboxDashBoardController.accountId.value @@ -566,7 +613,15 @@ class ComposerController extends BaseController { } } + void _initIdentities(List? identities) { + if (identities?.isNotEmpty == true) { + listFromIdentities.value = identities!; + identitySelected.value = identities.first; + } + } + void _getAllIdentities() { + log('ComposerController::_getAllIdentities: Fetch again identity !'); final accountId = mailboxDashBoardController.accountId.value; final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { @@ -595,17 +650,17 @@ class ComposerController extends BaseController { emailActionType: actionType, mailboxRole: mailboxRole ); - final userProfile = mailboxDashBoardController.userProfile.value; - if (userProfile != null) { - final isSender = presentationEmail.from.asList().every((element) => element.email == userProfile.email); + final userName = mailboxDashBoardController.sessionCurrent?.username; + if (userName != null) { + final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value); if (isSender) { listToEmailAddress = List.from(recipients.value1.toSet()); listCcEmailAddress = List.from(recipients.value2.toSet()); listBccEmailAddress = List.from(recipients.value3.toSet()); } else { - listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userProfile.email)); - listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userProfile.email)); - listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userProfile.email)); + listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userName.value)); + listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userName.value)); + listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userName.value)); } } else { listToEmailAddress = List.from(recipients.value1.toSet()); @@ -661,153 +716,15 @@ class ComposerController extends BaseController { } } - Future _generateEmail( - BuildContext context, - UserProfile userProfile, - { - bool asDrafts = false, - MailboxId? draftMailboxId, - MailboxId? outboxMailboxId, - ComposerArguments? arguments, - } - ) async { - Set listFromEmailAddress = {EmailAddress(null, userProfile.email)}; - if (identitySelected.value?.email?.isNotEmpty == true) { - listFromEmailAddress = { - EmailAddress( - identitySelected.value?.name, - identitySelected.value?.email - ) - }; - } - Set listReplyToEmailAddress = {EmailAddress(null, userProfile.email)}; - if (identitySelected.value?.replyTo?.isNotEmpty == true) { - listReplyToEmailAddress = identitySelected.value!.replyTo!; - } - - final attachments = {}; - attachments.addAll(uploadController.generateAttachments() ?? []); - - var emailBodyText = await _getEmailBodyText(context, asDrafts: asDrafts); - if (uploadController.mapInlineAttachments.isNotEmpty) { - final mapContents = await _getMapContent(emailBodyText); - emailBodyText = mapContents.value1; - final listInlineAttachment = mapContents.value2; - final listInlineEmailBodyPart = listInlineAttachment - .map((attachment) => attachment.toEmailBodyPart(charset: 'base64')) - .toSet(); - attachments.addAll(listInlineEmailBodyPart); - } - - final userAgent = await userAgentPlatform; - log('ComposerController::_generateEmail(): userAgent: $userAgent'); - - Map mailboxIds = {}; - if (asDrafts && draftMailboxId != null) { - mailboxIds[draftMailboxId] = true; - } - if (outboxMailboxId != null) { - mailboxIds[outboxMailboxId] = true; - } - - Map? mapKeywords = {}; - if (asDrafts) { - mapKeywords[KeyWordIdentifier.emailDraft] = true; - mapKeywords[KeyWordIdentifier.emailSeen] = true; - } - - final inReplyTo = _generateInReplyTo(arguments); - final references = _generateReferences(arguments); - - final generatePartId = PartId(uuid.v1()); - - return Email( - mailboxIds: mailboxIds.isNotEmpty ? mailboxIds : null, - from: listFromEmailAddress, - to: listToEmailAddress.toSet(), - cc: listCcEmailAddress.toSet(), - bcc: listBccEmailAddress.toSet(), - replyTo: listReplyToEmailAddress, - inReplyTo: inReplyTo, - references: references, - keywords: mapKeywords.isNotEmpty ? mapKeywords : null, - subject: subjectEmail.value, - htmlBody: { - EmailBodyPart( - partId: generatePartId, - type: MediaType.parse('text/html') - )}, - bodyValues: { - generatePartId: EmailBodyValue(emailBodyText, false, false) - }, - headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgent}, - attachments: attachments.isNotEmpty ? attachments : null, - headerMdn: hasRequestReadReceipt.value ? { IndividualHeaderIdentifier.headerMdn: getEmailAddressSender() } : {}, - ); - } - - MessageIdsHeaderValue? _generateInReplyTo(ComposerArguments? arguments) { - if (arguments?.emailActionType == EmailActionType.reply || - arguments?.emailActionType == EmailActionType.replyAll) { - return arguments?.messageId; - } - return null; - } - - MessageIdsHeaderValue? _generateReferences(ComposerArguments? arguments) { - if (arguments?.emailActionType == EmailActionType.reply || - arguments?.emailActionType == EmailActionType.replyAll || - arguments?.emailActionType == EmailActionType.forward) { - Set ids = {}; - if (arguments?.messageId?.ids.isNotEmpty == true) { - ids.addAll(arguments!.messageId!.ids); - } - if (arguments?.references?.ids.isNotEmpty == true) { - ids.addAll(arguments!.references!.ids); - } - if (ids.isNotEmpty) { - return MessageIdsHeaderValue(ids); - } - } - return null; - } - - Future>> _getMapContent(String emailBodyText) async { - if (kIsWeb) { - return await richTextWebController.refactorContentHasInlineImage( - emailBodyText, - uploadController.mapInlineAttachments); - } else { - return await richTextMobileTabletController.refactorContentHasInlineImage( - emailBodyText, - uploadController.mapInlineAttachments); - } - } - - Future get userAgentPlatform async { - String userAgent; - try { - if (kIsWeb) { - final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo; - userAgent = webBrowserInfo.userAgent ?? ''; - } else { - userAgent = FkUserAgent.userAgent ?? ''; - } - } catch (e) { - userAgent = ''; - } - return 'Team-Mail/${mailboxDashBoardController.appInformation.value?.version} $userAgent'; - } - - void validateInformationBeforeSending(BuildContext context) async { - if (isSendEmailLoading.isTrue) { + void handleClickSendButton(BuildContext context) async { + if (_sendButtonState == ButtonState.disabled) { + log('ComposerController::handleClickSendButton: SENDING EMAIL'); return; } + _sendButtonState = ButtonState.disabled; clearFocus(context); - isSendEmailLoading.value = true; - if (toEmailAddressController.text.isNotEmpty || ccEmailAddressController.text.isNotEmpty || bccEmailAddressController.text.isNotEmpty) { @@ -819,18 +736,17 @@ class ComposerController extends BaseController { showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_recipient, AppLocalizations.of(context).add_recipients, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false, showAsBottomSheet: true, - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress; final listEmailAddressInvalid = allListEmailAddress - .where((emailAddress) => !GetUtils.isEmail(emailAddress.emailAddress)) + .where((emailAddress) => !EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) .toList(); if (listEmailAddressInvalid.isNotEmpty) { showConfirmDialogAction(context, @@ -840,13 +756,12 @@ class ComposerController extends BaseController { toAddressExpandMode.value = ExpandMode.EXPAND; ccAddressExpandMode.value = ExpandMode.EXPAND; bccAddressExpandMode.value = ExpandMode.EXPAND; - isSendEmailLoading.value = false; }, showAsBottomSheet: true, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -855,11 +770,11 @@ class ComposerController extends BaseController { AppLocalizations.of(context).message_dialog_send_email_without_a_subject, AppLocalizations.of(context).send_anyway, onConfirmAction: () => _handleSendMessages(context), - onCancelAction: () => isSendEmailLoading.value = false, + autoPerformPopBack: false, title: AppLocalizations.of(context).empty_subject, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icEmpty, fit: BoxFit.fill), - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } @@ -868,94 +783,168 @@ class ComposerController extends BaseController { context, AppLocalizations.of(context).messageDialogSendEmailUploadingAttachment, AppLocalizations.of(context).got_it, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, showAsBottomSheet: true, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } - if (!uploadController.hasEnoughMaxAttachmentSize()) { + if (uploadController.isExceededMaxSizeAttachmentsPerEmail()) { showConfirmDialogAction( context, AppLocalizations.of(context).message_dialog_send_email_exceeds_maximum_size( filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), AppLocalizations.of(context).got_it, - onConfirmAction: () => isSendEmailLoading.value = false, title: AppLocalizations.of(context).sending_failed, icon: SvgPicture.asset(imagePaths.icSendToastError, fit: BoxFit.fill), hasCancelButton: false - ).whenComplete(() => isSendEmailLoading.value = false); + ).whenComplete(() => _sendButtonState = ButtonState.enabled); return; } _handleSendMessages(context); } - bool get _isParamUserNull { + Future _getContentInEditor() async { + final htmlTextEditor = PlatformInfo.isWeb + ? _textEditorWeb + : await htmlEditorApi?.getText(); + if (htmlTextEditor?.isNotEmpty == true) { + return htmlTextEditor!.removeEditorStartTag(); + } else { + return ''; + } + } + + void _handleSendMessages(BuildContext context) async { if (composerArguments.value == null || - mailboxDashBoardController.userProfile.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null ) { - logError('ComposerController::isParamUserNotNull: Param is NULL'); - return true; + log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _sendButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + return; } - return false; - } - void _handleSendMessages(BuildContext context) async { - if (_isParamUserNull) { - logError('ComposerController::_handleSendMessages: Param is NULL'); - _closeComposerAction(); - return; + if (Get.isDialogOpen == true) { + popBack(); } - final sendingArgs = await _createSendingEmailArguments(context); - _closeComposerAction(result: sendingArgs); + final emailContent = await _getContentInEditor(); + final cancelToken = CancelToken(); + final resultState = await _showSendingMessageDialog( + emailContent: emailContent, + cancelToken: cancelToken + ); + log('ComposerController::_handleSendMessages: resultState = $resultState'); + if (resultState is SendEmailSuccess || mailboxDashBoardController.validateSendingEmailFailedWhenNetworkIsLostOnMobile(resultState)) { + _sendButtonState = ButtonState.enabled; + _closeComposerAction(result: resultState); + } else if (resultState is SendEmailFailure && resultState.exception is SendingEmailCanceledException) { + _sendButtonState = ButtonState.enabled; + } else if ((resultState is SendEmailFailure || resultState is GenerateEmailFailure) && context.mounted) { + await _showConfirmDialogWhenSendMessageFailure( + context: context, + failure: resultState + ); + } else { + _sendButtonState = ButtonState.enabled; + } } - Future _createSendingEmailArguments(BuildContext context) async { - final session = mailboxDashBoardController.sessionCurrent!; - final arguments = composerArguments.value!; - final accountId = mailboxDashBoardController.accountId.value!; - final userProfile = mailboxDashBoardController.userProfile.value!; + Future _showSendingMessageDialog({ + required String emailContent, + CancelToken? cancelToken + }) { + final childWidget = PointerInterceptor( + child: SendingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsEmailId: composerArguments.value!.emailActionType == EmailActionType.editDraft + ? composerArguments.value!.presentationEmail?.id + : null, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ), + createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, + onCancelSendingEmailAction: _handleCancelSendingMessage, + cancelToken: cancelToken, + ), + ); - final createdEmail = await _generateEmail( - context, - userProfile, - outboxMailboxId: mailboxDashBoardController.outboxMailbox?.id, - arguments: arguments + return Get.dialog( + PlatformInfo.isMobile + ? PopScope(canPop: false, child: childWidget) + : childWidget, + barrierDismissible: false, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, ); + } - final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail - ? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail) - : EmailRequest( - email: createdEmail, - sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], - identityId: identitySelected.value?.id, - emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft - ? arguments.presentationEmail?.id - : null, - emailIdAnsweredOrForwarded: arguments.presentationEmail?.id, - emailActionType: arguments.emailActionType, - previousEmailId: arguments.previousEmailId, - ); - - final mailboxRequest = mailboxDashBoardController.outboxMailbox?.id == null - ? CreateNewMailboxRequest( - Id(uuid.v1()), - MailboxName(PresentationMailbox.outboxRole.inCaps) - ) - : null; - - return SendingEmailArguments( - session, - accountId, - emailRequest, - mailboxRequest, + void _handleCancelSendingMessage({CancelToken? cancelToken}) { + cancelToken?.cancel([SendingEmailCanceledException()]); + } + + Future _showConfirmDialogWhenSendMessageFailure({ + required BuildContext context, + required FeatureFailure failure + }) async { + await showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSendEmailFailure, + AppLocalizations.of(context).edit, + cancelTitle: AppLocalizations.of(context).closeAnyway, + alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, + onConfirmAction: () { + _sendButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: () { + _sendButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) ); } @@ -1062,39 +1051,47 @@ class ComposerController extends BaseController { consumeState(_localFilePickerInteractor.execute(fileType: fileType)); } - void _pickFileFailure(Failure failure) { - if (failure is LocalFilePickerFailure) { - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); - } + void _handlePickFileFailure(LocalFilePickerFailure failure) { + if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).thisFileCannotBePicked); } } - void _pickFileSuccess(LocalFilePickerSuccess success) { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo(success.pickedFiles))) { - _uploadAttachmentsAction(success.pickedFiles); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {isSendEmailLoading.value = false}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false); - } + void _handlePickImageFailure(LocalImagePickerFailure failure) { + if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).cannotSelectThisImage); } } - void _uploadAttachmentsAction(List pickedFiles) async { + void _handlePickFileSuccess(LocalFilePickerSuccess success) { + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: success.pickedFiles.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: success.pickedFiles) + ); + } + + void _handlePickImageSuccess(LocalImagePickerSuccess success) { + uploadController.validateTotalSizeInlineAttachmentsBeforeUpload( + totalSizePreparedFiles: success.fileInfo.fileSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) + ); + } + + void _uploadAttachmentsAction({required List pickedFiles}) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); + uploadController.justUploadAttachmentsAction( + uploadFiles: pickedFiles, + uploadUri: uploadUri, + ); + } else { + log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } } @@ -1108,8 +1105,9 @@ class ComposerController extends BaseController { PresentationEmail? presentationEmail, Role? mailboxRole, }) async { - final newEmailBody = await _getEmailBodyText(context, asDrafts: true); + final newEmailBody = await _getContentInEditor(); final oldEmailBody = _initTextEditor ?? ''; + log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody'); final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); final newEmailSubject = subjectEmail.value ?? ''; @@ -1147,177 +1145,77 @@ class ComposerController extends BaseController { return false; } - Future _generateSaveAsDraftsArguments(BuildContext context) async { - log('ComposerController::_generateSaveAsDraftsArguments:'); - final arguments = composerArguments.value; - final userProfile = mailboxDashBoardController.userProfile.value; - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; - - if (arguments == null || - draftMailboxId == null || - userProfile == null || - session == null || - accountId == null - ) { - return null; - } - - if (_emailIdEditing != null && _emailIdEditing != arguments.presentationEmail?.id) { - final newEmail = await _generateEmail( - context, - userProfile, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: arguments, - ); - - return SaveToDraftArguments( - session: session, - accountId: accountId, - newEmail: newEmail, - oldEmailId: _emailIdEditing!); - } else { - final isChanged = await _validateEmailChange( - context: context, - emailActionType: arguments.emailActionType, - presentationEmail: arguments.presentationEmail, - mailboxRole: arguments.mailboxRole - ); - - if (isChanged && context.mounted) { - final newEmail = await _generateEmail( - context, - userProfile, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: arguments, - ); - - return SaveToDraftArguments( - session: session, - accountId: accountId, - newEmail: newEmail, - oldEmailId: arguments.emailActionType == EmailActionType.editDraft - ? arguments.presentationEmail?.id - : null); - } else { - return null; - } - } - } - - void saveToDraftAction(BuildContext context) async { + void handleClickSaveAsDraftsButton(BuildContext context) async { if (_saveToDraftButtonState == ButtonState.disabled) { - log('ComposerController::saveToDraftAction: Saving to draft'); + log('ComposerController::handleClickSaveAsDraftsButton: Saving to draft'); return; } _saveToDraftButtonState = ButtonState.disabled; - final userProfile = mailboxDashBoardController.userProfile.value; - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; - - if (draftMailboxId == null || userProfile == null || session == null || accountId == null) { - log('ComposerController::saveToDraftAction: Param is NULL'); + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null || + mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts] == null + ) { + log('ComposerController::handleClickSaveAsDraftsButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _saveToDraftButtonState = ButtonState.enabled; return; } - final newEmail = await _generateEmail( - context, - userProfile, - asDrafts: true, - draftMailboxId: draftMailboxId, - arguments: mailboxDashBoardController.composerArguments); - - mailboxDashBoardController.saveEmailToDraft( - arguments: SaveToDraftArguments( - session: session, - accountId:accountId, - newEmail: newEmail, - oldEmailId: _emailIdEditing - ) + final emailContent = await _getContentInEditor(); + final cancelToken = CancelToken(); + final resultState = await _showSavingMessageToDraftsDialog( + emailContent: emailContent, + draftEmailId: _emailIdEditing, + cancelToken: cancelToken ); - } - - File _covertSharedMediaFileToFile(SharedMediaFile sharedMediaFile) { - return File( - Platform.isIOS - ? sharedMediaFile.type == SharedMediaType.FILE - ? sharedMediaFile.path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') - : sharedMediaFile.path.toString().replaceAll('%20', ' ') - : sharedMediaFile.path, - ); - } - FileInfo _covertFileToFileInfo(File file) { - return FileInfo( - file.path.split('/').last, - file.path, - file.existsSync() ? file.lengthSync() : 0, - ); - } - - List covertListSharedMediaFileToInlineImage(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } + if (resultState is SaveEmailAsDraftsSuccess) { + _saveToDraftButtonState = ButtonState.enabled; + _emailIdEditing = resultState.emailId; + mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + } else if (resultState is UpdateEmailDraftsSuccess) { + _saveToDraftButtonState = ButtonState.enabled; + _emailIdEditing = resultState.emailId; + mailboxDashBoardController.consumeState(Stream.value(Right(resultState))); + } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || + (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { + _saveToDraftButtonState = ButtonState.enabled; + } else if ((resultState is SaveEmailAsDraftsFailure || + resultState is UpdateEmailDraftsFailure || + resultState is GenerateEmailFailure) && + context.mounted + ) { + await _showConfirmDialogWhenSaveMessageToDraftsFailure( + context: context, + failure: resultState, + onConfirmAction: () { + _saveToDraftButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: () { + _saveToDraftButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + } + ); + } else { + _saveToDraftButtonState = ButtonState.enabled; } - - final List listInlineImage = newFiles.map( - (e) => InlineImage( - ImageSource.local, - fileInfo: _covertFileToFileInfo(e), - ) - ).toList(); - return listInlineImage; } - List covertListSharedMediaFileToFileInfo(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } - } + void _addAttachmentFromFileShare(List listSharedMediaFile) { + final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); - final List listFileInfo = newFiles.map( - (e) => _covertFileToFileInfo(e), - ).toList(); - return listFileInfo; - } + final tupleListFileInfo = partition(listFileInfo, (fileInfo) => fileInfo.isInline == true); + final listAttachments = tupleListFileInfo.value2; - void _addAttachmentFromFileShare(List listSharedMediaFile) { - final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); - final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); - if (listImageSharedMediaFile.isNotEmpty) { - final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); - for (var e in listInlineImage) { - _uploadInlineAttachmentsAction(e.fileInfo!, fromFileShared: true); - } - } - if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile); - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo(listFile))) { - _uploadAttachmentsAction(listFile); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } - } + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); } void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { @@ -1410,7 +1308,7 @@ class ComposerController extends BaseController { if (arguments.emailActionType == EmailActionType.editDraft) { return arguments.presentationEmail?.firstEmailAddressInFrom ?? ''; } else { - return mailboxDashBoardController.userProfile.value?.email ?? ''; + return mailboxDashBoardController.sessionCurrent?.username.value ?? ''; } } return ''; @@ -1425,20 +1323,22 @@ class ComposerController extends BaseController { FocusScope.of(context).unfocus(); } - void _closeComposerAction({dynamic result}) { - log('ComposerController::_closeComposerAction:'); - isSendEmailLoading.value = false; - + void _closeComposerAction({dynamic result, bool closeOverlays = false}) { if (PlatformInfo.isWeb) { + if (closeOverlays) { + popBack(); + } mailboxDashBoardController.closeComposerOverlay(result: result); } else { - popBack(result: result); + popBack(result: result, closeOverlays: closeOverlays); } } void displayScreenTypeComposerAction(ScreenDisplayMode displayMode) async { - createFocusNodeInput(); - _updateTextForEditor(); + if (richTextWebController != null && screenDisplayMode.value != ScreenDisplayMode.minimize) { + final textCurrent = await richTextWebController!.editorController.getText(); + richTextWebController!.editorController.setText(textCurrent); + } screenDisplayMode.value = displayMode; await Future.delayed( @@ -1446,22 +1346,6 @@ class ComposerController extends BaseController { _autoFocusFieldWhenLauncher); } - void _updateTextForEditor() async { - final textCurrent = await richTextWebController.editorController.getText(); - richTextWebController.editorController.setText(textCurrent); - } - - void deleteComposer() { - FocusManager.instance.primaryFocus?.unfocus(); - mailboxDashBoardController.closeComposerOverlay(); - } - - void toggleDisplayAttachments() { - final newExpandMode = expandModeAttachments.value == ExpandMode.COLLAPSE - ? ExpandMode.EXPAND : ExpandMode.COLLAPSE; - expandModeAttachments.value = newExpandMode; - } - void addEmailAddressType(PrefixEmailAddress prefixEmailAddress) { switch(prefixEmailAddress) { case PrefixEmailAddress.from: @@ -1591,12 +1475,15 @@ class ComposerController extends BaseController { switch(prefixEmailAddress) { case PrefixEmailAddress.to: toAddressExpandMode.value = ExpandMode.EXPAND; + toAddressFocusNode?.requestFocus(); break; case PrefixEmailAddress.cc: ccAddressExpandMode.value = ExpandMode.EXPAND; + ccAddressFocusNode?.requestFocus(); break; case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.EXPAND; + bccAddressFocusNode?.requestFocus(); break; default: break; @@ -1677,6 +1564,10 @@ class ComposerController extends BaseController { if (newIdentity.signatureAsString.isNotEmpty == true) { await _applySignature(newIdentity.signatureAsString.asSignatureHtml()); } + + if (PlatformInfo.isMobile) { + await htmlEditorApi?.onDocumentChanged(); + } } void _applyBccEmailAddressFromIdentity(Set listEmailAddress) { @@ -1705,16 +1596,16 @@ class ComposerController extends BaseController { Future _applySignature(String signature) async { if (PlatformInfo.isWeb) { - richTextWebController.editorController.insertSignature(signature); + richTextWebController?.editorController.insertSignature(signature); } else { - await htmlEditorApi?.insertSignature(signature); + await htmlEditorApi?.insertSignature(signature, allowCollapsed: false); } } Future _removeSignature() async { log('ComposerController::_removeSignature():'); if (PlatformInfo.isWeb) { - richTextWebController.editorController.removeSignature(); + richTextWebController?.editorController.removeSignature(); } else { await htmlEditorApi?.removeSignature(); } @@ -1726,82 +1617,10 @@ class ComposerController extends BaseController { if (responsiveUtils.isMobile(context)) { maxWithEditor = maxWith - 40; } else { - maxWithEditor = maxWith - 120; + maxWithEditor = maxWith - 70; } - final inlineImage = await _selectFromFile(); - if (inlineImage != null) { - if (PlatformInfo.isWeb) { - _insertImageOnWeb(inlineImage); - } else { - _insertImageOnMobileAndTablet(inlineImage); - } - } else { - if (context.mounted) { - appToast.showToastErrorMessage(context, AppLocalizations.of(context).cannotSelectThisImage); - } - } - } - - Future _selectFromFile() async { - final filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: PlatformInfo.isWeb - ); - if (filePickerResult?.files.isNotEmpty == true) { - PlatformFile platformFile = filePickerResult!.files.first; - final fileSelected = FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null, - ); - return InlineImage(ImageSource.local, fileInfo: fileSelected); - } - - return null; - } - void _insertImageOnWeb(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextWebController.insertImage(inlineImage); - } - } - - void _insertImageOnMobileAndTablet(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextMobileTabletController.insertImage(inlineImage); - } - } - - void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo([pickedFile]))) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.uploadFileAction( - pickedFile, - uploadUri, - isInline: true, - fromFileShared: fromFileShared - ); - } - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {isSendEmailLoading.value = false}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false); - } - } + consumeState(_localImagePickerInteractor.execute()); } void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { @@ -1818,7 +1637,6 @@ class ComposerController extends BaseController { uploadState.attachment.cid!, uploadState.fileInfo, maxWidth: maxWithEditor, - fromFileShared: uploadState.fromFileShared, )); } } @@ -1840,7 +1658,7 @@ class ComposerController extends BaseController { if (coordinates?[1] != null && coordinates?[1] != 0) { final coordinateY = max((coordinates?[1] ?? 0) - defaultPaddingCoordinateYCursorEditor, 0); final realCoordinateY = coordinateY + (headerEditorMobileSize?.height ?? 0); - final outsideHeight = Get.height - ComposerStyle.keyboardMaxHeight - ComposerStyle.keyboardToolBarHeight; + final outsideHeight = Get.height - MediaQuery.viewInsetsOf(context).bottom - ComposerStyle.keyboardToolBarHeight; final webViewEditorClientY = max(outsideHeight, 0) + scrollController.position.pixels; if (scrollController.position.pixels >= realCoordinateY) { _scrollToCursorEditor( @@ -1864,9 +1682,12 @@ class ComposerController extends BaseController { double headerEditorMobileHeight, BuildContext context, ) { - scrollController.jumpTo( - realCoordinateY - (responsiveUtils.isLandscapeMobile(context) ? 0 : headerEditorMobileHeight / 2), - ); + final scrollTarget = realCoordinateY - + (responsiveUtils.isLandscapeMobile(context) + ? 0 + : headerEditorMobileHeight / 2); + final maxScrollExtend = scrollController.position.maxScrollExtent; + scrollController.jumpTo(min(scrollTarget, maxScrollExtend)); } void _onEnterKeyDown() { @@ -1898,7 +1719,7 @@ class ComposerController extends BaseController { } else if (subjectEmailInputController.text.isEmpty) { subjectEmailInputFocusNode?.requestFocus(); } else if (PlatformInfo.isWeb) { - richTextWebController.editorController.setFocus(); + richTextWebController?.editorController.setFocus(); } } @@ -1908,21 +1729,25 @@ class ComposerController extends BaseController { bccAddressFocusNode?.hasFocus == true || subjectEmailInputFocusNode?.hasFocus == true; - void handleInitHtmlEditorWeb(String initContent) { + void handleInitHtmlEditorWeb(String initContent) async { log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; - richTextWebController.editorController.setFullScreen(); + richTextWebController?.editorController.setFullScreen(); + richTextWebController?.editorController.setOnDragDropEvent(); onChangeTextEditorWeb(initContent); - richTextWebController.setEnableCodeView(); + richTextWebController?.setEnableCodeView(); if (identitySelected.value == null) { _getAllIdentities(); + } else { + await _selectIdentity(identitySelected.value); + _autoFocusFieldWhenLauncher(); } } void handleOnFocusHtmlEditorWeb() { FocusManager.instance.primaryFocus?.unfocus(); - richTextWebController.editorController.setFocus(); - richTextWebController.closeAllMenuPopup(); + richTextWebController?.editorController.setFocus(); + richTextWebController?.closeAllMenuPopup(); } void handleOnUnFocusHtmlEditorWeb() { @@ -1935,72 +1760,6 @@ class ComposerController extends BaseController { onEditorFocusChange(true); } - void handleImageUploadSuccess ( - BuildContext context, - web_html_editor.FileUpload fileUpload - ) async { - log('ComposerController::handleImageUploadSuccess:NAME: ${fileUpload.name} | TYPE: ${fileUpload.type} | SIZE: ${fileUpload.size}'); - if (fileUpload.base64 == null) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - return; - } - - if (fileUpload.type?.startsWith(Constant.imageType) == true) { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _uploadInlineAttachmentsAction(fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } else { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _addAttachmentFromDragAndDrop(fileInfo: fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } - } - - void handleImageUploadFailure({ - required BuildContext context, - required web_html_editor.UploadError uploadError, - web_html_editor.FileUpload? fileUpload, - String? base64Str, - }) { - logError('ComposerController::handleImageUploadFailure:fileUpload: $fileUpload | uploadError: $uploadError'); - appToast.showToastErrorMessage( - context, - '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' - ); - } - - void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: uploadController.getTotalSizeFromListFileInfo([fileInfo]))) { - _uploadAttachmentsAction([fileInfo]); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } - } - FocusNode? getNextFocusOfToEmailAddress() { if (ccRecipientState.value == PrefixRecipientState.enabled) { return ccAddressFocusNode; @@ -2025,14 +1784,14 @@ class ComposerController extends BaseController { bool get isNetworkConnectionAvailable => networkConnectionController.isNetworkConnectionAvailable(); - UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; - String? get textEditorWeb => _textEditorWeb; - HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; + HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController?.htmlEditorApi; void onChangeTextEditorWeb(String? text) { - initTextEditor(text); + if (identitySelected.value != null) { + initTextEditor(text); + } _textEditorWeb = text; } @@ -2042,32 +1801,6 @@ class ComposerController extends BaseController { void setSubjectEmail(String subject) => subjectEmail.value = subject; - Future _getEmailBodyText(BuildContext context, {bool asDrafts = false}) async { - var contentHtml = ''; - - if (PlatformInfo.isWeb) { - if (responsiveUtils.isDesktop(context) && - screenDisplayMode.value == ScreenDisplayMode.minimize) { - contentHtml = _textEditorWeb ?? ''; - } else { - if (asDrafts) { - contentHtml = await richTextWebController.editorController.getText(); - } else { - contentHtml = await richTextWebController.editorController.getTextWithSignatureContent(); - } - } - } else { - if (asDrafts) { - contentHtml = (await htmlEditorApi?.getText()) ?? ''; - } else { - contentHtml = (await htmlEditorApi?.getTextWithSignatureContent()) ?? ''; - } - } - - final newContentHtml = contentHtml.removeEditorStartTag(); - return newContentHtml; - } - void removeDraggableEmailAddress(DraggableEmailAddress draggableEmailAddress) { log('ComposerController::removeDraggableEmailAddress: $draggableEmailAddress'); switch(draggableEmailAddress.prefix) { @@ -2092,22 +1825,12 @@ class ComposerController extends BaseController { _updateStatusEmailSendButton(); } - void addAttachmentFromDropZone(Attachment attachment) { - log('ComposerController::addAttachmentFromDropZone: $attachment'); - if (uploadController.hasEnoughMaxAttachmentSize(fileInfoTotalSize: attachment.size?.value)) { - uploadController.initializeUploadAttachments([attachment]); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } - } + void onAttachmentDropZoneListener(Attachment attachment) { + log('ComposerController::onAttachmentDropZoneListener: attachment = $attachment'); + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: attachment.size?.value ?? 0, + onValidationSuccess: () => uploadController.initializeUploadAttachments([attachment]) + ); } void _handleGetAllIdentitiesFailure(GetAllIdentitiesFailure failure) async { @@ -2159,25 +1882,289 @@ class ComposerController extends BaseController { log('ComposerController::handleClickCloseComposer: _closeComposerButtonState = disabled'); return; } - - if (!_isEmailBodyLoaded) { - log('ComposerController::handleClickCloseComposer: _isEmailBodyLoaded = false'); + + _closeComposerButtonState = ButtonState.disabled; + + if (composerArguments.value == null || !_isEmailBodyLoaded) { + log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL or EMAIL NOT LOADED'); + _closeComposerButtonState = ButtonState.enabled; clearFocus(context); _closeComposerAction(); return; } - _closeComposerButtonState = ButtonState.disabled; - clearFocus(context); - final draftArgs = await _generateSaveAsDraftsArguments(context); - _closeComposerAction(result: draftArgs); - _closeComposerButtonState = ButtonState.enabled; + final isChanged = await _validateEmailChange( + context: context, + emailActionType: composerArguments.value!.emailActionType, + presentationEmail: composerArguments.value!.presentationEmail, + mailboxRole: composerArguments.value!.mailboxRole + ); + + if (isChanged && context.mounted) { + clearFocus(context); + await _showConfirmDialogSaveMessage(context); + return; + } + + if (context.mounted) { + _closeComposerButtonState = ButtonState.enabled; + clearFocus(context); + _closeComposerAction(); + } } - + + Future _showConfirmDialogSaveMessage(BuildContext context) async { + await showConfirmDialogAction( + context, + title: AppLocalizations.of(context).saveMessage.capitalizeFirstEach, + AppLocalizations.of(context).warningMessageWhenClickCloseComposer, + AppLocalizations.of(context).save, + cancelTitle: AppLocalizations.of(context).discardChanges, + alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, + titleActionButtonMaxLines: 1, + isArrangeActionButtonsVertical: true, + usePopScope: true, + onConfirmAction: () => _handleSaveMessageToDraft(context), + onCancelAction: () { + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + }, + onCloseButtonAction: () { + _closeComposerButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + onPopInvoked: (didPop) { + log('ComposerController::_showConfirmDialogSaveMessage: didPop = $didPop'); + if (!didPop) { + _closeComposerButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + } + }, + marginIcon: EdgeInsets.zero, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + titleStyle: Theme.of(context).textTheme.labelLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); + } + void _getAlwaysReadReceiptSetting() { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null) { consumeState(_getAlwaysReadReceiptSettingInteractor.execute(accountId)); } } + + void handleOnDragEnterHtmlEditorWeb() { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + } + + void onLocalFileDropZoneListener({ + required BuildContext context, + required DropDoneDetails details, + required double maxWidth + }) async { + if (responsiveUtils.isMobile(context)) { + maxWithEditor = maxWidth - 40; + } else { + maxWithEditor = maxWidth - 70; + } + + final listFileInfo = await onDragDone(context: context, details: details); + + if (listFileInfo.isEmpty && context.mounted) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).can_not_upload_this_file_as_attachments + ); + return; + } + + final listAttachments = listFileInfo + .where((fileInfo) => fileInfo.isInline != true) + .toList(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); + } + + void _handleSaveMessageToDraft(BuildContext context) async { + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null || + mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts] == null + ) { + log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + return; + } + + popBack(); + + final emailContent = await _getContentInEditor(); + final draftEmailId = _getDraftEmailId(); + log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); + final cancelToken = CancelToken(); + final resultState = await _showSavingMessageToDraftsDialog( + emailContent: emailContent, + draftEmailId: draftEmailId, + cancelToken: cancelToken + ); + + if (resultState is SaveEmailAsDraftsSuccess || resultState is UpdateEmailDraftsSuccess) { + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(result: resultState); + } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || + (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { + _closeComposerButtonState = ButtonState.enabled; + } else if ((resultState is SaveEmailAsDraftsFailure || + resultState is UpdateEmailDraftsFailure || + resultState is GenerateEmailFailure) && + context.mounted + ) { + await _showConfirmDialogWhenSaveMessageToDraftsFailure( + context: context, + failure: resultState + ); + } else { + _closeComposerButtonState = ButtonState.enabled; + } + } + + EmailId? _getDraftEmailId() { + if (_emailIdEditing != null && + _emailIdEditing != composerArguments.value!.presentationEmail?.id) { + return _emailIdEditing; + } else if (composerArguments.value!.emailActionType == EmailActionType.editDraft) { + return composerArguments.value!.presentationEmail?.id; + } else { + return null; + } + } + + Future _showSavingMessageToDraftsDialog({ + required String emailContent, + EmailId? draftEmailId, + CancelToken? cancelToken, + }) { + final childWidget = PointerInterceptor( + child: SavingMessageDialogView( + createEmailRequest: CreateEmailRequest( + session: mailboxDashBoardController.sessionCurrent!, + accountId: mailboxDashBoardController.accountId.value!, + emailActionType: composerArguments.value!.emailActionType, + subject: subjectEmail.value ?? '', + emailContent: emailContent, + fromSender: composerArguments.value!.presentationEmail?.from ?? {}, + toRecipients: listToEmailAddress.toSet(), + ccRecipients: listCcEmailAddress.toSet(), + bccRecipients: listBccEmailAddress.toSet(), + isRequestReadReceipt: hasRequestReadReceipt.value, + identity: identitySelected.value, + attachments: uploadController.attachmentsUploaded, + inlineAttachments: uploadController.mapInlineAttachments, + sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent], + draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts], + draftsEmailId: draftEmailId, + answerForwardEmailId: composerArguments.value!.presentationEmail?.id, + unsubscribeEmailId: composerArguments.value!.previousEmailId, + messageId: composerArguments.value!.messageId, + references: composerArguments.value!.references, + emailSendingQueue: composerArguments.value!.sendingEmail + ), + createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, + onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, + cancelToken: cancelToken, + ), + ); + return Get.dialog( + PlatformInfo.isMobile + ? PopScope(canPop: false, child: childWidget) + : childWidget, + barrierDismissible: false, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } + + void _handleCancelSavingMessageToDrafts({CancelToken? cancelToken}) { + cancelToken?.cancel([SavingEmailToDraftsCanceledException()]); + } + + Future _showConfirmDialogWhenSaveMessageToDraftsFailure({ + required BuildContext context, + required FeatureFailure failure, + VoidCallback? onConfirmAction, + VoidCallback? onCancelAction, + }) async { + await showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSaveEmailToDraftsFailure, + AppLocalizations.of(context).edit, + cancelTitle: AppLocalizations.of(context).closeAnyway, + alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, + onConfirmAction: onConfirmAction ?? () { + _closeComposerButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + }, + onCancelAction: onCancelAction ?? () { + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); + } + + void handleEnableRecipientsInputAction(bool isEnabled) { + fromRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index bdb3d63399..18743df6e7 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -36,7 +36,7 @@ class ComposerView extends GetWidget { return ResponsiveWidget( responsiveUtils: controller.responsiveUtils, mobile: MobileContainerView( - keyboardRichTextController: controller.keyboardRichTextController, + keyboardRichTextController: controller.richTextMobileTabletController!.richTextController, onCloseViewAction: () => controller.handleClickCloseComposer(context), onClearFocusAction: () => controller.clearFocus(context), onAttachFileAction: () => controller.isNetworkConnectionAvailable @@ -49,7 +49,7 @@ class ComposerView extends GetWidget { ? controller.insertImage(context, constraints.maxWidth) : null, backgroundColor: MobileAppBarComposerWidgetStyle.backgroundColor, - childBuilder: (context) => SafeArea( + childBuilder: (context, constraints) => SafeArea( left: !controller.responsiveUtils.isLandscapeMobile(context), right: !controller.responsiveUtils.isLandscapeMobile(context), child: Container( @@ -60,7 +60,7 @@ class ComposerView extends GetWidget { Obx(() => LandscapeAppBarComposerWidget( isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -74,7 +74,7 @@ class ComposerView extends GetWidget { Obx(() => AppBarComposerWidget( isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -110,6 +110,8 @@ class ComposerView extends GetWidget { Obx(() => RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -127,12 +129,15 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, )), Obx(() { if (controller.ccRecipientState.value == PrefixRecipientState.enabled) { return RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -157,6 +162,8 @@ class ComposerView extends GetWidget { return RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -184,6 +191,16 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, onTapOutside: controller.onTapOutsideSubject, ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() => Center( child: InsertImageLoadingBarWidget( uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, @@ -200,17 +217,7 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return MobileAttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - ); - } else { - return const SizedBox.shrink(); - } - }), - const SizedBox(height: ComposerStyle.keyboardMaxHeight), + SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64), ], ), ), @@ -221,7 +228,7 @@ class ComposerView extends GetWidget { ), ), tablet: TabletContainerView( - keyboardRichTextController: controller.keyboardRichTextController, + keyboardRichTextController: controller.richTextMobileTabletController!.richTextController, onCloseViewAction: () => controller.handleClickCloseComposer(context), onClearFocusAction: () => controller.clearFocus(context), onAttachFileAction: () => controller.isNetworkConnectionAvailable @@ -263,6 +270,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, @@ -280,11 +289,14 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, @@ -304,6 +316,8 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, @@ -329,6 +343,16 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, onTapOutside: controller.onTapOutsideSubject, ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() => Center( child: InsertImageLoadingBarWidget( uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, @@ -345,24 +369,15 @@ class ComposerView extends GetWidget { onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, ), )), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return MobileAttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - ); - } else { - return const SizedBox.shrink(); - } - }) + SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64), ], ), ) ), TabletBottomBarComposerWidget( deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( context, @@ -453,7 +468,7 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.saveToDraftAction(context); + controller.handleClickSaveAsDraftsButton(context); } ) ), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 409921903b..efe72a1680 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -17,10 +17,11 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/web/drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -42,14 +43,14 @@ class ComposerView extends GetWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() => MobileResponsiveAppBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, isSendButtonEnabled: controller.isEnableEmailSendButton.value, onCloseViewAction: () => controller.handleClickCloseComposer(context), attachFileAction: () => controller.openFilePickerByType(context, FileType.any), insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + sendMessageAction: () => controller.handleClickSendButton(context), openContextMenuAction: (position) { controller.openPopupMenuAction( context, @@ -86,12 +87,15 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.mobileRecipientPadding, @@ -109,9 +113,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -129,9 +136,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -157,72 +167,111 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, ), Expanded( - child: Stack( - children: [ - Padding( - padding: ComposerStyle.mobileEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.mobileEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController!.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController!.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController!, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details, + maxWidth: constraintsEditor.maxWidth, + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow - ), - ); - } else { - return const SizedBox.shrink(); - } - }) ] ), ); @@ -265,12 +314,15 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.desktopRecipientPadding, @@ -288,9 +340,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -308,9 +363,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -336,123 +394,149 @@ class ComposerView extends GetWidget { margin: ComposerStyle.desktopSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( - children: [ - Expanded( - child: Padding( - padding: ComposerStyle.desktopEditorPadding, - child: Obx(() { - return Stack( + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor + ), + child: Column( children: [ - WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, + Expanded( + child: Padding( + padding: ComposerStyle.desktopEditorPadding, + child: Obx(() { + return WebEditorView( + editorController: controller.richTextWebController!.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController?.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController?.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + ); + }), + ), ), - if (controller.mailboxDashBoardController.isDraggableAppActive) - PointerInterceptor( - child: DropZoneWidget( - width: constraints.maxWidth, - height: constraints.maxHeight, - addAttachmentFromDropZone: controller.addAttachmentFromDropZone, - ) - ) + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController!, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) ], - ); - }), - ), - ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ], - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + ), + ), + Obx(() => BottomBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + showCodeViewAction: controller.richTextWebController!.toggleCodeView, + deleteComposerAction: () => controller.handleClickDeleteComposer(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + sendMessageAction: () => controller.handleClickSendButton(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )), + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details, + maxWidth: constraintsEditor.maxWidth, + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, - attachFileAction: () => controller.openFilePickerByType(context, FileType.any), - insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, - deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), - requestReadReceiptAction: (position) { - controller.openPopupMenuAction( - context, - position, - _createReadReceiptPopupItems(context), - radius: ComposerStyle.popupMenuRadius - ); - }, - isSending: controller.isSendEmailLoading.value, - )), ]), ); }, @@ -496,12 +580,15 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.to, listEmailAddress: controller.listToEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, fromState: controller.fromRecipientState.value, ccState: controller.ccRecipientState.value, bccState: controller.bccRecipientState.value, expandMode: controller.toAddressExpandMode.value, controller: controller.toEmailAddressController, focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, keyTagEditor: controller.keyToEmailTagEditor, isInitial: controller.isInitialRecipient.value, padding: ComposerStyle.tabletRecipientPadding, @@ -519,9 +606,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.cc, listEmailAddress: controller.listCcEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.ccAddressExpandMode.value, controller: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, + focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), @@ -539,9 +629,12 @@ class ComposerView extends GetWidget { RecipientComposerWidget( prefix: PrefixEmailAddress.bcc, listEmailAddress: controller.listBccEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: constraints.maxWidth, expandMode: controller.bccAddressExpandMode.value, controller: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, + focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, @@ -567,99 +660,132 @@ class ComposerView extends GetWidget { margin: ComposerStyle.tabletSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor + ), + child: Stack( children: [ - Expanded( - child: Padding( - padding: ComposerStyle.tabletEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.tabletEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController!.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController!.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: controller.deleteAttachmentUploaded, + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController!.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController!, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), ), Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), ); } else { return const SizedBox.shrink(); } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details, + maxWidth: constraintsEditor.maxWidth, + ), + ) ), ); } else { return const SizedBox.shrink(); } - }) + }), ], ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + ); + } ), ), Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, attachFileAction: () => controller.openFilePickerByType(context, FileType.any), insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, + showCodeViewAction: controller.richTextWebController!.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: (position) { controller.openPopupMenuAction( context, @@ -707,10 +833,10 @@ class ComposerView extends GetWidget { colorIcon: ComposerStyle.popupItemIconColor, padding: ComposerStyle.popupItemPadding, selectedIcon: controller.imagePaths.icFilterSelected, - isSelected: controller.richTextWebController.codeViewEnabled, + isSelected: controller.richTextWebController?.codeViewEnabled, onCallbackAction: () { popBack(); - controller.richTextWebController.toggleCodeView(); + controller.richTextWebController?.toggleCodeView(); } ) ), @@ -740,7 +866,7 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.saveToDraftAction(context); + controller.handleClickSaveAsDraftsButton(context); } ) ), diff --git a/lib/features/composer/presentation/controller/base_rich_text_controller.dart b/lib/features/composer/presentation/controller/base_rich_text_controller.dart index 262dbd5441..52d3f8c469 100644 --- a/lib/features/composer/presentation/controller/base_rich_text_controller.dart +++ b/lib/features/composer/presentation/controller/base_rich_text_controller.dart @@ -1,12 +1,7 @@ -import 'package:collection/collection.dart'; import 'package:core/presentation/views/dialog/color_picker_dialog_builder.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:html/parser.dart' show parse; -import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -37,32 +32,4 @@ abstract class BaseRichTextController extends GetxController { } ).show(); } - - Future>> refactorContentHasInlineImage( - String emailContent, - Map mapInlineAttachments - ) async { - final document = parse(emailContent); - final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); - final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { - final idImg = imgTag.attributes['id']; - final cid = idImg!.replaceFirst('cid:', '').trim(); - log('BaseRichTextController::refactorContentHasInlineImage(): $cid'); - imgTag.attributes['src'] = 'cid:$cid'; - imgTag.attributes.remove('id'); - return cid; - })).then((listCid) { - log('BaseRichTextController::refactorContentHasInlineImage(): $listCid'); - final listInlineAttachment = listCid - .whereNotNull() - .map((cid) => mapInlineAttachments[cid]) - .whereNotNull() - .toList(); - return listInlineAttachment; - }); - final newContent = document.body?.innerHtml ?? emailContent; - log('BaseRichTextController::refactorContentHasInlineImage(): $newContent'); - log('BaseRichTextController::refactorContentHasInlineImage(): listInlineAttachment: $listInlineAttachment'); - return Tuple2(newContent, listInlineAttachment); - } } \ No newline at end of file diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart index 84b6d98a41..ec887b5f1c 100644 --- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart @@ -6,27 +6,19 @@ import 'package:file_picker/file_picker.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; class RichTextMobileTabletController extends BaseRichTextController { HtmlEditorApi? htmlEditorApi; - void insertImage( - InlineImage image, - { - double? maxWithEditor, - bool fromFileShare = false + final RichTextController richTextController = RichTextController(); + + void insertImage(InlineImage inlineImage) async { + if (inlineImage.fileInfo.isShared == true) { + await htmlEditorApi?.moveCursorAtLastNode(); } - ) async { - log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor | $fromFileShare'); - if (image.source == ImageSource.network) { - htmlEditorApi?.insertImageLink(image.link!); - } else { - if (fromFileShare) { - await htmlEditorApi?.moveCursorAtLastNode(); - } - await htmlEditorApi?.insertHtml(image.base64Uri ?? ''); + if (inlineImage.base64Uri?.isNotEmpty == true) { + await htmlEditorApi?.insertHtml(inlineImage.base64Uri!); } } @@ -51,4 +43,11 @@ class RichTextMobileTabletController extends BaseRichTextController { logError('RichTextMobileTabletController::insertImageData:Exception: $e'); } } + + @override + void onClose() { + richTextController.dispose(); + htmlEditorApi = null; + super.onClose(); + } } diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index 782aca1e4c..76f5cf473b 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; @@ -194,13 +193,8 @@ class RichTextWebController extends BaseRichTextController { bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) => listTextStyleApply.contains(richTextStyleType); - void insertImage(InlineImage image) async { - log('RichTextWebController::insertImage(): $image'); - if (image.source == ImageSource.network) { - editorController.insertNetworkImage(image.link!); - } else { - editorController.insertHtml("
${image.base64Uri ?? ''}

"); - } + void insertImage(InlineImage inlineImage) { + editorController.insertHtml("
${inlineImage.base64Uri ?? ''}

"); } void applyNewFontStyle(FontNameType? newFont) { @@ -285,11 +279,11 @@ class RichTextWebController extends BaseRichTextController { menuOrderListController.hideMenu(); } - void insertImageAsBase64({required PlatformFile platformFile}) { + void insertImageAsBase64({required PlatformFile platformFile, int? maxWidth}) { if (platformFile.bytes != null) { final base64Data = base64Encode(platformFile.bytes!); editorController.insertHtml( - 'Image in my signature' + 'Image in my signature' ); } else { logError("RichTextWebController::insertImageAsBase64: bytes is null"); diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart new file mode 100644 index 0000000000..f112bc0e60 --- /dev/null +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -0,0 +1,183 @@ +import 'package:core/core.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.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/email/email_action_type.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/username_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/identity_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; + +extension CreateEmailRequestExtension on CreateEmailRequest { + + Set createSenders() { + if (identity?.email?.isNotEmpty == true) { + return { identity!.toEmailAddress() }; + } else { + return { session.username.toEmailAddress() }; + } + } + + String createMdnEmailAddress() { + if (emailActionType == EmailActionType.editDraft && fromSender.isNotEmpty) { + return fromSender.first.emailAddress; + } else { + return session.username.value; + } + } + + Set createReplyToRecipients() { + if (identity?.replyTo?.isNotEmpty == true) { + return identity!.replyTo!.toSet(); + } else { + return { session.username.toEmailAddress() }; + } + } + + Set createAttachments() => attachments?.toEmailBodyPart() ?? {}; + + Map? createKeywords() { + if (draftsMailboxId != null) { + return { + KeyWordIdentifier.emailDraft: true, + KeyWordIdentifier.emailSeen: true, + }; + } else { + return null; + } + } + + Map? createMailboxIds() { + if (draftsMailboxId != null || outboxMailboxId != null) { + return { + if (draftsMailboxId != null) + draftsMailboxId!: true, + if (outboxMailboxId != null) + outboxMailboxId!: true, + }; + } else { + return null; + } + } + + MessageIdsHeaderValue? createInReplyTo() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll + ) { + return messageId; + } + return null; + } + + MessageIdsHeaderValue? createReferences() { + if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyAll || + emailActionType == EmailActionType.forward + ) { + Set ids = {}; + if (messageId?.ids.isNotEmpty == true) { + ids.addAll(messageId!.ids); + } + if (references?.ids.isNotEmpty == true) { + ids.addAll(references!.ids); + } + if (ids.isNotEmpty) { + return MessageIdsHeaderValue(ids); + } + } + return null; + } + + Email generateEmail({ + required String newEmailContent, + required Set newEmailAttachments, + required String userAgent, + required PartId partId, + }) { + return Email( + mailboxIds: createMailboxIds(), + from: createSenders(), + to: toRecipients, + cc: ccRecipients, + bcc: bccRecipients, + replyTo: createReplyToRecipients(), + inReplyTo: createInReplyTo(), + references: createReferences(), + keywords: createKeywords(), + subject: subject, + htmlBody: { + EmailBodyPart( + partId: partId, + type: MediaType.parse(Constant.textHtmlMimeType) + ) + }, + bodyValues: { + partId: EmailBodyValue( + value: newEmailContent, + isEncodingProblem: false, + isTruncated: false, + acceptLanguageHeader: { + IndividualHeaderIdentifier.acceptLanguageHeader: LocalizationService.supportedLocalesToLanguageTags() + }, + contentLanguageHeader: { + IndividualHeaderIdentifier.contentLanguageHeader: LocalizationService.getLocaleFromLanguage().toLanguageTag() + }, + ) + }, + headerUserAgent: { + IndividualHeaderIdentifier.headerUserAgent : userAgent + }, + attachments: newEmailAttachments.isNotEmpty + ? newEmailAttachments + : null, + headerMdn: isRequestReadReceipt + ? { IndividualHeaderIdentifier.headerMdn: createMdnEmailAddress() } + : null, + ); + } + + EmailRequest createEmailRequest({required Email emailObject}) { + if (emailActionType == EmailActionType.editSendingEmail) { + return emailSendingQueue!.toEmailRequest(newEmail: emailObject); + } else { + return EmailRequest( + email: emailObject, + sentMailboxId: sentMailboxId, + identityId: identity?.id, + emailIdDestroyed: draftsEmailId, + emailIdAnsweredOrForwarded: answerForwardEmailId, + emailActionType: emailActionType, + previousEmailId: unsubscribeEmailId, + ); + } + } + + CreateNewMailboxRequest? createMailboxRequest() { + if (outboxMailboxId == null) { + return CreateNewMailboxRequest(MailboxName(PresentationMailbox.outboxRole.inCaps)); + } else { + return null; + } + } + + SendingEmailArguments toSendingEmailArguments({required Email emailObject}) { + return SendingEmailArguments( + session, + accountId, + createEmailRequest(emailObject: emailObject), + createMailboxRequest() + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/file_extension.dart b/lib/features/composer/presentation/extensions/file_extension.dart new file mode 100644 index 0000000000..a5d4470511 --- /dev/null +++ b/lib/features/composer/presentation/extensions/file_extension.dart @@ -0,0 +1,19 @@ + +import 'dart:io'; + +import 'package:model/upload/file_info.dart'; + +extension FileExtension on File { + FileInfo toFileInfo({ + bool? isInline, + bool? isShared + }) { + return FileInfo( + fileName: path.split('/').last, + fileSize: existsSync() ? lengthSync() : 0, + filePath: path, + isInline: isInline, + isShared: isShared + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/file_upload_extension.dart b/lib/features/composer/presentation/extensions/file_upload_extension.dart index 3f6e26c254..ec8707c7c9 100644 --- a/lib/features/composer/presentation/extensions/file_upload_extension.dart +++ b/lib/features/composer/presentation/extensions/file_upload_extension.dart @@ -29,7 +29,8 @@ extension FileUploadExtension on FileUpload { return FileInfo.fromBytes( bytes: bytes, name: name, - size: size + size: size, + type: type, ); } else { return null; diff --git a/lib/features/composer/presentation/extensions/identity_extension.dart b/lib/features/composer/presentation/extensions/identity_extension.dart new file mode 100644 index 0000000000..b5f15ba162 --- /dev/null +++ b/lib/features/composer/presentation/extensions/identity_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; + +extension IdentityExtension on Identity { + EmailAddress toEmailAddress() => EmailAddress(name, email); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart new file mode 100644 index 0000000000..482196e1da --- /dev/null +++ b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart @@ -0,0 +1,8 @@ + +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/shared_media_file_extension.dart'; + +extension ListSharedMediaFileExtension on List { + List toListFileInfo({bool? isShared}) => map((sharedMediaFile) => sharedMediaFile.toFileInfo(isShared: isShared)).toList(); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart new file mode 100644 index 0000000000..93f6caa37f --- /dev/null +++ b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart @@ -0,0 +1,22 @@ + +import 'dart:io'; + +import 'package:core/utils/platform_info.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart'; + +extension SharedMediaFileExtension on SharedMediaFile { + File toFile() { + if (PlatformInfo.isIOS) { + final pathFile = type == SharedMediaType.FILE + ? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') + : path.toString().replaceAll('%20', ' '); + return File(pathFile); + } else { + return File(path); + } + } + + FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart new file mode 100644 index 0000000000..1d27758307 --- /dev/null +++ b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart @@ -0,0 +1,52 @@ +import 'dart:async' as async; +import 'package:async/async.dart'; +import 'package:core/data/constants/constant.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +mixin DragDropFileMixin { + async.Future> showFutureLoadingDialogFullScreen({ + required BuildContext context, + required async.Future Function() future, + }) async { + return await showFutureLoadingDialog( + context: context, + title: AppLocalizations.of(context).loadingPleaseWait, + backLabel: AppLocalizations.of(context).close, + future: future, + ); + } + + async.Future> onDragDone({ + required BuildContext context, + required DropDoneDetails details + }) async { + final bytesList = await showFutureLoadingDialogFullScreen( + context: context, + future: () => async.Future.wait( + details.files.map( + (xFile) => xFile.readAsBytes(), + ), + ), + ); + + if (bytesList.error != null) return []; + + final listFileInfo = []; + for (var i = 0; i < bytesList.result!.length; i++) { + listFileInfo.add( + FileInfo( + bytes: bytesList.result![i], + fileName: details.files[i].name, + type: details.files[i].mimeType, + fileSize: bytesList.result![i].length, + isInline: details.files[i].mimeType?.startsWith(Constant.imageType) == true + ), + ); + } + return listFileInfo; + } +} diff --git a/lib/features/composer/presentation/model/create_email_request.dart b/lib/features/composer/presentation/model/create_email_request.dart new file mode 100644 index 0000000000..1cc956a167 --- /dev/null +++ b/lib/features/composer/presentation/model/create_email_request.dart @@ -0,0 +1,88 @@ + +import 'package:equatable/equatable.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/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.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/email/attachment.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class CreateEmailRequest with EquatableMixin { + + final Session session; + final AccountId accountId; + final EmailActionType emailActionType; + final String subject; + final String emailContent; + final bool isRequestReadReceipt; + final Set fromSender; + final Set toRecipients; + final Set ccRecipients; + final Set bccRecipients; + final Identity? identity; + final List? attachments; + final Map? inlineAttachments; + final MailboxId? outboxMailboxId; + final MailboxId? sentMailboxId; + final MailboxId? draftsMailboxId; + final EmailId? draftsEmailId; + final EmailId? answerForwardEmailId; + final EmailId? unsubscribeEmailId; + final MessageIdsHeaderValue? messageId; + final MessageIdsHeaderValue? references; + final SendingEmail? emailSendingQueue; + + CreateEmailRequest({ + required this.session, + required this.accountId, + required this.emailActionType, + required this.subject, + required this.emailContent, + required this.fromSender, + required this.toRecipients, + required this.ccRecipients, + required this.bccRecipients, + this.isRequestReadReceipt = true, + this.identity, + this.attachments, + this.inlineAttachments, + this.outboxMailboxId, + this.sentMailboxId, + this.draftsMailboxId, + this.draftsEmailId, + this.answerForwardEmailId, + this.unsubscribeEmailId, + this.messageId, + this.references, + this.emailSendingQueue + }); + + @override + List get props => [ + session, + accountId, + emailActionType, + subject, + emailContent, + fromSender, + toRecipients, + ccRecipients, + bccRecipients, + identity, + isRequestReadReceipt, + attachments, + inlineAttachments, + outboxMailboxId, + sentMailboxId, + draftsMailboxId, + draftsEmailId, + answerForwardEmailId, + unsubscribeEmailId, + references, + references, + emailSendingQueue + ]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/image_source.dart b/lib/features/composer/presentation/model/image_source.dart deleted file mode 100644 index 6c2cbf3d2c..0000000000 --- a/lib/features/composer/presentation/model/image_source.dart +++ /dev/null @@ -1,5 +0,0 @@ - -enum ImageSource { - local, - network -} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/inline_image.dart b/lib/features/composer/presentation/model/inline_image.dart index 2dc8e33f02..db16bd6acc 100644 --- a/lib/features/composer/presentation/model/inline_image.dart +++ b/lib/features/composer/presentation/model/inline_image.dart @@ -1,25 +1,19 @@ import 'package:equatable/equatable.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; +import 'package:model/upload/file_info.dart'; class InlineImage with EquatableMixin { - final ImageSource source; - final String? link; - final FileInfo? fileInfo; - final String? cid; + final FileInfo fileInfo; final String? base64Uri; - InlineImage( - this.source, - { - this.link, - this.fileInfo, - this.cid, - this.base64Uri, - } - ); + InlineImage({ + required this.fileInfo, + this.base64Uri, + }); @override - List get props => [source, link, fileInfo, cid, base64Uri]; + List get props => [ + fileInfo, + base64Uri + ]; } \ No newline at end of file diff --git a/lib/features/composer/presentation/model/save_to_draft_arguments.dart b/lib/features/composer/presentation/model/save_to_draft_arguments.dart deleted file mode 100644 index b29a3855c1..0000000000 --- a/lib/features/composer/presentation/model/save_to_draft_arguments.dart +++ /dev/null @@ -1,26 +0,0 @@ -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:tmail_ui_user/main/routes/router_arguments.dart'; - -class SaveToDraftArguments extends RouterArguments { - final Session session; - final AccountId accountId; - final Email newEmail; - final EmailId? oldEmailId; - - SaveToDraftArguments({ - required this.session, - required this.accountId, - required this.newEmail, - required this.oldEmailId - }); - - @override - List get props => [ - session, - accountId, - newEmail, - oldEmailId, - ]; -} diff --git a/lib/features/composer/presentation/model/save_to_draft_view_event.dart b/lib/features/composer/presentation/model/save_to_draft_view_event.dart index 14db275c05..08c84a326f 100644 --- a/lib/features/composer/presentation/model/save_to_draft_view_event.dart +++ b/lib/features/composer/presentation/model/save_to_draft_view_event.dart @@ -4,14 +4,12 @@ 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/mailbox/mailbox.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; class SaveToDraftViewEvent extends ViewEvent { final BuildContext context; final Session session; final AccountId accountId; - final UserProfile userProfile; final MailboxId draftMailboxId; final EmailId? emailIdEditing; final ComposerArguments? arguments; @@ -20,7 +18,6 @@ class SaveToDraftViewEvent extends ViewEvent { required this.context, required this.session, required this.accountId, - required this.userProfile, required this.draftMailboxId, this.emailIdEditing, this.arguments, @@ -31,7 +28,6 @@ class SaveToDraftViewEvent extends ViewEvent { context, session, accountId, - userProfile, draftMailboxId, emailIdEditing, arguments, diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart index d88eca6c80..ca246c3c0e 100644 --- a/lib/features/composer/presentation/styles/composer_style.dart +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; class ComposerStyle { static const double radius = 28; - static const double keyboardMaxHeight = 500; static const double keyboardToolBarHeight = 200; static const double popupMenuRadius = 8; diff --git a/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart index 4c401ff6eb..30eb22df90 100644 --- a/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart @@ -6,5 +6,6 @@ class MobileAttachmentComposerWidgetStyle { static const double listItemHeight = 50; static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 16); + static const EdgeInsetsGeometry tabletPadding = EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 8); static const EdgeInsetsGeometry itemMargin = EdgeInsetsDirectional.only(top: 8); } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart index 7012513aee..39bc5164a6 100644 --- a/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart +++ b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart @@ -8,6 +8,4 @@ class MobileContainerViewStyle { static final Color keyboardToolbarBackgroundColor = PlatformInfo.isIOS ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid; - - static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart index 19c480ab8b..a6fac6b200 100644 --- a/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart +++ b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart @@ -14,8 +14,6 @@ class TabletContainerViewStyle { ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid; - static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); - static EdgeInsetsGeometry getMargin( BuildContext context, ResponsiveUtils responsiveUtils diff --git a/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart index f7543db209..359c864ce8 100644 --- a/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart @@ -25,6 +25,7 @@ class RecipientComposerWidgetStyle { static const EdgeInsetsGeometry prefixButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 3, horizontal: 5); static const EdgeInsetsGeometry labelMargin = EdgeInsetsDirectional.only(top: 16); static const EdgeInsetsGeometry recipientMargin = EdgeInsetsDirectional.only(top: 12); + static const EdgeInsetsGeometry enableRecipientButtonMargin = EdgeInsetsDirectional.only(top: 10); static const TextStyle prefixButtonTextStyle = TextStyle( fontSize: 15, diff --git a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart index 3389db408a..ef82023e20 100644 --- a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart @@ -10,6 +10,7 @@ class BottomBarComposerWidgetStyle { static const double richTextIconSize = 24; static const double sendButtonRadius = 8; static const double sendButtonIconSpace = 5; + static const double height = 60; static const Color backgroundColor = Colors.white; static const Color iconColor = AppColor.colorRichButtonComposer; @@ -17,7 +18,7 @@ class BottomBarComposerWidgetStyle { static const Color selectedBackgroundColor = AppColor.colorSelected; static const Color selectedIconColor = AppColor.primaryColor; - static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32); static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); diff --git a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart index 033c224f68..35fe785430 100644 --- a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart'; class DropZoneWidgetStyle { static const double space = 20; @@ -8,11 +9,16 @@ class DropZoneWidgetStyle { static const List dashSize = [6, 3]; - static const Color backgroundColor = AppColor.colorDropZoneBackground; + static Color backgroundColor = AppColor.colorDropZoneBackground.withOpacity(0.7); static const Color borderColor = AppColor.colorDropZoneBorder; static const EdgeInsetsGeometry padding = EdgeInsets.all(20); - static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.symmetric(vertical: 8); + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only( + bottom: BottomBarComposerWidgetStyle.height, + start: 8, + end: 8, + top: 8 + ); static const TextStyle labelTextStyle = TextStyle( color: Colors.black, diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart index 6854dbeecb..de1ffcd81c 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; import 'package:rich_text_composer/rich_text_composer.dart' as rich_composer; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; @@ -12,7 +11,7 @@ typedef OnInsertImageAction = Function(BoxConstraints constraints); class MobileContainerView extends StatelessWidget { - final Widget Function(BuildContext context) childBuilder; + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; final rich_composer.RichTextController keyboardRichTextController; final VoidCallback onCloseViewAction; final VoidCallback? onAttachFileAction; @@ -49,7 +48,6 @@ class MobileContainerView extends StatelessWidget { backgroundColor: backgroundColor ?? MobileContainerViewStyle.outSideBackgroundColor, resizeToAvoidBottomInset: false, body: LayoutBuilder(builder: (context, constraints) { - return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { return rich_composer.KeyboardRichText( richTextController: keyboardRichTextController, keyBroadToolbar: RichTextKeyboardToolBar( @@ -66,12 +64,9 @@ class MobileContainerView extends StatelessWidget { titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, titleBack: AppLocalizations.of(context).format, ), - paddingChild: isKeyboardVisible - ? MobileContainerViewStyle.keyboardToolbarPadding - : EdgeInsets.zero, - child: childBuilder(context), + paddingChild: EdgeInsets.zero, + child: childBuilder(context, constraints), ); - }); }) ), ) diff --git a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart index 2274ab08e1..9af4547939 100644 --- a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart +++ b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; @@ -45,7 +44,6 @@ class TabletContainerView extends StatelessWidget { child: Scaffold( backgroundColor: TabletContainerViewStyle.outSideBackgroundColor, body: LayoutBuilder(builder: (context, constraints) { - return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { return KeyboardRichText( richTextController: keyboardRichTextController, keyBroadToolbar: RichTextKeyboardToolBar( @@ -62,9 +60,7 @@ class TabletContainerView extends StatelessWidget { titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, titleBack: AppLocalizations.of(context).format, ), - paddingChild: isKeyboardVisible - ? TabletContainerViewStyle.keyboardToolbarPadding - : EdgeInsets.zero, + paddingChild: EdgeInsets.zero, child: Center( child: Card( elevation: TabletContainerViewStyle.elevation, @@ -81,7 +77,6 @@ class TabletContainerView extends StatelessWidget { ), ), ); - }); }) ), ) diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index e9d2826cc1..5da136ef90 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -26,11 +26,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorView({ super.key, @@ -44,11 +43,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -71,11 +69,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -97,11 +94,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ), (success) { if (success is GetEmailContentLoading) { @@ -123,11 +119,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -156,11 +151,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); }, (success) { @@ -185,11 +179,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -205,11 +198,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } diff --git a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart index ffc1070c66..174621b47e 100644 --- a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:filesize/filesize.dart'; @@ -7,6 +8,7 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_h import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; typedef OnToggleExpandAttachmentViewAction = Function(bool isCollapsed); @@ -66,13 +68,26 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { ), padding: AttachmentHeaderComposerWidgetStyle.sizeLabelPadding, child: Text( - filesize(listFileUploaded.totalSize, 0), + filesize(listFileUploaded.totalSize), style: AttachmentHeaderComposerWidgetStyle.sizeLabelTextSize, ), - ) + ), + if (_isExceedMaximumSizeFileAttachedInComposer) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icQuotasWarning, + iconSize: 20, + margin: const EdgeInsetsDirectional.only(start: 4), + padding: const EdgeInsets.all(3), + iconColor: AppColor.colorBackgroundQuotasWarning, + tooltipMessage: AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, + backgroundColor: Colors.transparent, + ) ], ), ), ); } + + bool get _isExceedMaximumSizeFileAttachedInComposer => + listFileUploaded.totalSize > AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; } diff --git a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart index c77b6e7a4c..18f4802548 100644 --- a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart @@ -9,9 +9,10 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_item_composer_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; -typedef OnDeleteAttachmentAction = void Function(UploadFileState fileState); +typedef OnDeleteAttachmentAction = void Function(UploadTaskId uploadTaskId); class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { @@ -20,6 +21,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { final UploadFileState fileState; final double? maxWidth; final EdgeInsetsGeometry? itemMargin; + final EdgeInsetsGeometry? itemPadding; final OnDeleteAttachmentAction? onDeleteAttachmentAction; final Widget? buttonAction; @@ -28,6 +30,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { required this.fileState, this.maxWidth, this.itemMargin, + this.itemPadding, this.buttonAction, this.onDeleteAttachmentAction, }); @@ -41,7 +44,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { color: AttachmentItemComposerWidgetStyle.backgroundColor ), width: AttachmentItemComposerWidgetStyle.width, - padding: AttachmentItemComposerWidgetStyle.padding, + padding: itemPadding ?? AttachmentItemComposerWidgetStyle.padding, margin: itemMargin, child: Row( children: [ @@ -100,7 +103,7 @@ class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { borderRadius: AttachmentItemComposerWidgetStyle.deleteIconRadius, padding: AttachmentItemComposerWidgetStyle.deleteIconPadding, iconColor: AttachmentItemComposerWidgetStyle.deleteIconColor, - onTapActionCallback: () => onDeleteAttachmentAction?.call(fileState), + onTapActionCallback: () => onDeleteAttachmentAction?.call(fileState.uploadTaskId), ) ], ), diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart index e219c6f3f5..6d0e4817c7 100644 --- a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart @@ -1,4 +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/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -6,70 +9,203 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_i import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_item_composer_widget.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class MobileAttachmentComposerWidget extends StatelessWidget { +class MobileAttachmentComposerWidget extends StatefulWidget { final List listFileUploaded; final OnDeleteAttachmentAction onDeleteAttachmentAction; - final _responsiveUtils = Get.find(); - - MobileAttachmentComposerWidget({ + const MobileAttachmentComposerWidget({ super.key, required this.listFileUploaded, required this.onDeleteAttachmentAction, }); + @override + State createState() => _MobileAttachmentComposerWidgetState(); +} + +class _MobileAttachmentComposerWidgetState extends State { + static const int _maxCountDisplayedAttachments = 2; + + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + + List _listFileDisplayed = []; + bool _isCollapsed = false; + + @override + void initState() { + super.initState(); + _updateListFileDisplayed(); + } + + @override + void didUpdateWidget(covariant MobileAttachmentComposerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.listFileUploaded != widget.listFileUploaded) { + _updateListFileDisplayed(); + } + } + @override Widget build(BuildContext context) { return Container( - padding: MobileAttachmentComposerWidgetStyle.padding, + padding: _responsiveUtils.isPortraitMobile(context) + ? MobileAttachmentComposerWidgetStyle.padding + : MobileAttachmentComposerWidgetStyle.tabletPadding, width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isLandscapeMobile(context)) - SizedBox( - width: _responsiveUtils.getSizeScreenWidth(context) * 0.7, - child: GridView.builder( - reverse: true, - primary: false, - shrinkWrap: true, - itemCount: listFileUploaded.length, - gridDelegate: const SliverGridDelegateFixedHeight( - height: MobileAttachmentComposerWidgetStyle.listItemHeight, - crossAxisCount: MobileAttachmentComposerWidgetStyle.maxItemRow, - crossAxisSpacing: MobileAttachmentComposerWidgetStyle.listItemSpace, - ), - itemBuilder: (context, index) { - return AttachmentItemComposerWidget( - fileState: listFileUploaded[index], - itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, - onDeleteAttachmentAction: onDeleteAttachmentAction - ); - } + child: _responsiveUtils.isPortraitMobile(context) + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(child: SizedBox( + width: AttachmentItemComposerWidgetStyle.width, + child: ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: _listFileDisplayed.length, + itemBuilder: (context, index) { + return AttachmentItemComposerWidget( + fileState: _listFileDisplayed[index], + itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, + onDeleteAttachmentAction: widget.onDeleteAttachmentAction + ); + } + ), + )), + if (!_isCollapsed && _isExceededDisplayedAttachments) + TMailButtonWidget( + text: AppLocalizations.of(context).showLess, + icon: _imagePaths.icChevronUp, + iconAlignment: TextDirection.rtl, + iconSpace: 2, + iconSize: 24, + iconColor: AppColor.primaryColor, + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 8), + onTapActionCallback: _toggleListAttachments + ) + ], ), - ) - else - SizedBox( - width: AttachmentItemComposerWidgetStyle.width, - child: ListView.builder( - reverse: true, - shrinkWrap: true, - primary: false, - itemCount: listFileUploaded.length, - itemBuilder: (context, index) { - return AttachmentItemComposerWidget( - fileState: listFileUploaded[index], - itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, - onDeleteAttachmentAction: onDeleteAttachmentAction - ); - } + if (_isCollapsed && _isExceededDisplayedAttachments) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).showMoreAttachment(_countRemainingAttachments), + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + margin: const EdgeInsetsDirectional.only(top: 5), + onTapActionCallback: _toggleListAttachments + ) + ] + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + flex: 7, + child: GridView.builder( + primary: false, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _listFileDisplayed.length, + gridDelegate: const SliverGridDelegateFixedHeight( + height: MobileAttachmentComposerWidgetStyle.listItemHeight, + crossAxisCount: MobileAttachmentComposerWidgetStyle.maxItemRow, + crossAxisSpacing: MobileAttachmentComposerWidgetStyle.listItemSpace, + ), + itemBuilder: (context, index) { + return AttachmentItemComposerWidget( + fileState: _listFileDisplayed[index], + itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, + itemPadding: const EdgeInsets.symmetric(horizontal: 8), + onDeleteAttachmentAction: widget.onDeleteAttachmentAction + ); + } + ) ), - ) - ] - ), + Flexible( + flex: 3, + child: _isExceededDisplayedAttachments + ? Row( + children: [ + if (!_isCollapsed) + TMailButtonWidget( + text: AppLocalizations.of(context).showLess, + icon: _imagePaths.icChevronUp, + iconAlignment: TextDirection.rtl, + iconSpace: 2, + iconSize: 24, + iconColor: AppColor.primaryColor, + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 8, top: 10), + onTapActionCallback: _toggleListAttachments + ) + else + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).showMoreAttachment(_countRemainingAttachments), + backgroundColor: Colors.transparent, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: 15, + color: AppColor.primaryColor + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 8, top: 10), + onTapActionCallback: _toggleListAttachments + ), + const Spacer(), + ], + ) + : const SizedBox.shrink() + ) + ], + ) ); } + + bool get _listFileUploadedSuccess => + widget.listFileUploaded.every((uploadFile) => uploadFile.uploadStatus.completed); + + bool get _isExceededDisplayedAttachments => + _listFileUploadedSuccess && + widget.listFileUploaded.length > _maxCountDisplayedAttachments; + + int get _countRemainingAttachments => + widget.listFileUploaded.length - _maxCountDisplayedAttachments; + + void _updateListFileDisplayed() { + final reversedList = widget.listFileUploaded.reversed.toList(); + if (_isCollapsed && reversedList.length > _maxCountDisplayedAttachments) { + _listFileDisplayed = reversedList.sublist(0, _maxCountDisplayedAttachments); + } else { + _listFileDisplayed = reversedList; + } + } + + void _toggleListAttachments() { + setState(() { + _isCollapsed = !_isCollapsed; + _updateListFileDisplayed(); + }); + } } diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index db4a347b51..45a5ba332a 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:math'; import 'package:collection/collection.dart'; +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/views/button/tmail_button_widget.dart'; @@ -10,7 +11,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -34,17 +34,21 @@ typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress pref typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress prefix, bool isFocus); typedef OnRemoveDraggableEmailAddressAction = void Function(DraggableEmailAddress draggableEmailAddress); typedef OnDeleteTagAction = void Function(EmailAddress emailAddress); +typedef OnEnableAllRecipientsInputAction = void Function(bool isEnabled); class RecipientComposerWidget extends StatefulWidget { final PrefixEmailAddress prefix; final List listEmailAddress; + final ImagePaths imagePaths; + final double maxWidth; final ExpandMode expandMode; final PrefixRecipientState fromState; final PrefixRecipientState ccState; final PrefixRecipientState bccState; final bool? isInitial; final FocusNode? focusNode; + final FocusNode? focusNodeKeyboard; final GlobalKey? keyTagEditor; final FocusNode? nextFocusNode; final TextEditingController? controller; @@ -58,11 +62,16 @@ class RecipientComposerWidget extends StatefulWidget { final VoidCallback? onFocusNextAddressAction; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; + final OnEnableAllRecipientsInputAction? onEnableAllRecipientsInputAction; + final bool isTestingForWeb; const RecipientComposerWidget({ super.key, required this.prefix, required this.listEmailAddress, + required this.imagePaths, + required this.maxWidth, + @visibleForTesting this.isTestingForWeb = false, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, @@ -82,6 +91,8 @@ class RecipientComposerWidget extends StatefulWidget { this.onFocusEmailAddressChangeAction, this.onFocusNextAddressAction, this.onRemoveDraggableEmailAddressAction, + this.onEnableAllRecipientsInputAction, + this.focusNodeKeyboard, }); @override @@ -95,8 +106,6 @@ class _RecipientComposerWidgetState extends State { bool _isDragging = false; late List _currentListEmailAddress; - final _imagePaths = Get.find(); - @override void initState() { super.initState(); @@ -113,237 +122,281 @@ class _RecipientComposerWidgetState extends State { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: RecipientComposerWidgetStyle.borderColor, - width: 1 - ) + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1 ) - ), - padding: widget.padding, - margin: widget.margin, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: RecipientComposerWidgetStyle.labelMargin, - child: Text( - '${widget.prefix.asName(context)}:', - style: RecipientComposerWidgetStyle.labelTextStyle - ), + ) + ), + padding: widget.padding, + margin: widget.margin, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: RecipientComposerWidgetStyle.labelMargin, + child: Text( + '${widget.prefix.asName(context)}:', + key: Key('prefix_${widget.prefix.name}_recipient_composer_widget'), + style: RecipientComposerWidgetStyle.labelTextStyle ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - Expanded( - child: FocusScope( - child: Focus( - onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), - onKey: (focusNode, event) { - if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - widget.nextFocusNode?.requestFocus(); - widget.onFocusNextAddressAction?.call(); - return KeyEventResult.handled; + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + Expanded( + child: FocusScope( + child: Focus( + onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), + onKey: PlatformInfo.isWeb ? _recipientInputOnKeyListener : null, + child: StatefulBuilder( + builder: (context, stateSetter) { + if (PlatformInfo.isWeb || widget.isTestingForWeb) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + focusNodeKeyboard: widget.focusNodeKeyboard, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + autoHideTextInputField: true, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + }, + onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), + onLeave: (draggableEmailAddress) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + stateSetter(() => _isDragging = true); + } + }, + ); + } else { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + focusNodeKeyboard: widget.focusNodeKeyboard, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + autoHideTextInputField: true, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); } - return KeyEventResult.ignored; }, - child: StatefulBuilder( - builder: (context, stateSetter) { - if (PlatformInfo.isWeb) { - return DragTarget( - builder: (context, candidateData, rejectedData) { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - enableBorder: _isDragging, - borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, - enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - }, - onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), - onLeave: (draggableEmailAddress) { - if (_isDragging) { - stateSetter(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - stateSetter(() => _isDragging = true); - } - }, - ); - } else { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: constraints.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - } - }, - ) ) ) - ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to && widget.fromState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).from_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), - ), - if (widget.prefix == PrefixEmailAddress.to && widget.ccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).cc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), - ), - if (widget.prefix == PrefixEmailAddress.to && widget.bccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).bcc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), - ), - if (widget.prefix != PrefixEmailAddress.to) + ) + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + if (widget.prefix == PrefixEmailAddress.to) + if (PlatformInfo.isWeb || widget.isTestingForWeb) + ...[ + if (widget.fromState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_from_button'), + text: AppLocalizations.of(context).from_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), + ), + if (widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_cc_button'), + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'), + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + ] + else if (PlatformInfo.isMobile) TMailButtonWidget.fromIcon( - icon: _imagePaths.icClose, + key: Key('prefix_${widget.prefix.name}_recipient_expand_button'), + icon: _isAllRecipientInputEnabled + ? widget.imagePaths.icChevronUp + : widget.imagePaths.icChevronDownOutline, backgroundColor: Colors.transparent, - iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, - iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, - padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), + iconSize: 24, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), ) - ] - ), - ); - }); + else if (PlatformInfo.isWeb || widget.isTestingForWeb) + TMailButtonWidget.fromIcon( + icon: widget.imagePaths.icClose, + backgroundColor: Colors.transparent, + iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, + iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, + padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), + ) + ] + ), + ); + } + + KeyEventResult _recipientInputOnKeyListener(FocusNode node, RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + widget.nextFocusNode?.requestFocus(); + widget.onFocusNextAddressAction?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; + bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled + && widget.ccState == PrefixRecipientState.enabled + && widget.bccState == PrefixRecipientState.enabled; + List get _collapsedListEmailAddress => _isCollapse ? _currentListEmailAddress.sublist(0, 1) : _currentListEmailAddress; diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart index 87b83b29c4..29dc018b0d 100644 --- a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -3,7 +3,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.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/extensions/email_address_extension.dart'; import 'package:super_tag_editor/widgets/rich_text_widget.dart'; @@ -17,16 +16,16 @@ class RecipientSuggestionItemWidget extends StatelessWidget { final SuggestionEmailState suggestionState; final EmailAddress emailAddress; + final ImagePaths imagePaths; final String? suggestionValid; final bool highlight; final OnSelectedRecipientSuggestionAction? onSelectedAction; - final _imagePaths = Get.find(); - - RecipientSuggestionItemWidget({ + const RecipientSuggestionItemWidget({ super.key, required this.suggestionState, required this.emailAddress, + required this.imagePaths, this.suggestionValid, this.highlight = false, this.onSelectedAction, @@ -61,7 +60,7 @@ class RecipientSuggestionItemWidget extends StatelessWidget { ) : null, trailing: SvgPicture.asset( - _imagePaths.icFilterSelected, + imagePaths.icFilterSelected, width: RecipientSuggestionItemWidgetStyle.selectedIconSize, height: RecipientSuggestionItemWidgetStyle.selectedIconSize, fit: BoxFit.fill diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index 2fea5a509c..e3ba3dc22c 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -1,14 +1,12 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.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/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -16,28 +14,33 @@ import 'package:tmail_ui_user/features/composer/presentation/model/draggable_ema import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; class RecipientTagItemWidget extends StatelessWidget { final bool isCollapsed; final bool isLatestTagFocused; final bool isLatestEmail; + final ImagePaths imagePaths; final double? maxWidth; + final int index; final PrefixEmailAddress prefix; final EmailAddress currentEmailAddress; final List currentListEmailAddress; final List collapsedListEmailAddress; final OnShowFullListEmailAddressAction? onShowFullAction; final OnDeleteTagAction? onDeleteTagAction; + final bool isTestingForWeb; - final _imagePaths = Get.find(); - - RecipientTagItemWidget({ + const RecipientTagItemWidget({ super.key, + required this.index, required this.prefix, required this.currentEmailAddress, required this.currentListEmailAddress, required this.collapsedListEmailAddress, + required this.imagePaths, + @visibleForTesting this.isTestingForWeb = false, this.isCollapsed = false, this.isLatestTagFocused = false, this.isLatestEmail = false, @@ -48,106 +51,77 @@ class RecipientTagItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { + Widget tagWidget = Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: 4, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + padding: EdgeInsets.zero, + label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), + currentEmailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), + labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + side: _getTagBorderSide(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), + ), + avatar: currentEmailAddress.displayName.isNotEmpty + ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), + colors: currentEmailAddress.avatarColors, + label: currentEmailAddress.displayName.firstLetterToUpperCase, + labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ); + + if (PlatformInfo.isWeb || isTestingForWeb) { + tagWidget = Draggable( + data: DraggableEmailAddress(emailAddress: currentEmailAddress, prefix: prefix), + feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: tagWidget, + ), + ); + } + + if ((PlatformInfo.isWeb || isTestingForWeb) && PlatformInfo.isCanvasKit) { + tagWidget = Padding( + padding: const EdgeInsetsDirectional.only(top: 8), + child: tagWidget, + ); + } + return Container( + key: Key('recipient_tag_item_${prefix.name}_$index'), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (PlatformInfo.isWeb) - Flexible( - child: Padding( - padding: EdgeInsetsDirectional.only( - top: !PlatformInfo.isCanvasKit ? 0 : 8 - ), - child: InkWell( - onTap: () => isCollapsed - ? onShowFullAction?.call(prefix) - : null, - child: Draggable( - data: DraggableEmailAddress(emailAddress: currentEmailAddress, prefix: prefix), - feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 - ), - padding: EdgeInsets.zero, - label: Text( - currentEmailAddress.asString(), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - ), - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( - colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, - iconSize: RecipientTagItemWidgetStyle.avatarIconSize, - ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), - ), - ), - ) - ), - ), - ) - else - Flexible( - child: InkWell( - onTap: () => isCollapsed - ? onShowFullAction?.call(prefix) - : null, - child: Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 - ), - label: Text( - currentEmailAddress.asString(), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - ), - padding: EdgeInsets.zero, - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( - colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, - iconSize: RecipientTagItemWidgetStyle.avatarIconSize, - ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), - ) - ), - ), + Flexible(child: tagWidget), if (isCollapsed) TMailButtonWidget.fromText( + key: Key('counter_recipient_tag_item_${prefix.name}_$index'), margin: _counterMargin, text: '+$countRecipients', onTapActionCallback: () => onShowFullAction?.call(prefix), borderRadius: RecipientTagItemWidgetStyle.radius, textStyle: RecipientTagItemWidgetStyle.labelTextStyle, - padding: PlatformInfo.isWeb + padding: PlatformInfo.isWeb || isTestingForWeb ? RecipientTagItemWidgetStyle.counterPadding : RecipientTagItemWidgetStyle.mobileCounterPadding, backgroundColor: AppColor.colorEmailAddressTag, @@ -158,7 +132,7 @@ class RecipientTagItemWidget extends StatelessWidget { } EdgeInsetsGeometry? get _counterMargin { - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb || isTestingForWeb) { return PlatformInfo.isCanvasKit ? RecipientTagItemWidgetStyle.webCounterMargin : RecipientTagItemWidgetStyle.webMobileCounterMargin; @@ -172,7 +146,7 @@ class RecipientTagItemWidget extends StatelessWidget { Color _getTagBackgroundColor() { if (isLatestTagFocused && isLatestEmail) { return AppColor.colorItemRecipientSelected; - } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { return AppColor.colorEmailAddressTag; } else { return Colors.white; @@ -182,7 +156,7 @@ class RecipientTagItemWidget extends StatelessWidget { BorderSide _getTagBorderSide() { if (isLatestTagFocused && isLatestEmail) { return const BorderSide(width: 1, color: AppColor.primaryColor); - } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag); } else { return const BorderSide( diff --git a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart new file mode 100644 index 0000000000..04a94b804c --- /dev/null +++ b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart @@ -0,0 +1,243 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +typedef OnCancelSavingEmailToDraftsAction = Function({CancelToken? cancelToken}); + +class SavingMessageDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSaveEmailToDraftsInteractor createNewAndSaveEmailToDraftsInteractor; + final OnCancelSavingEmailToDraftsAction? onCancelSavingEmailToDraftsAction; + final CancelToken? cancelToken; + + const SavingMessageDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSaveEmailToDraftsInteractor, + this.onCancelSavingEmailToDraftsAction, + this.cancelToken, + }); + + @override + State createState() => _SavingMessageDialogViewState(); +} + +class _SavingMessageDialogViewState extends State { + + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _streamSubscription = widget.createNewAndSaveEmailToDraftsInteractor + .execute( + createEmailRequest: widget.createEmailRequest, + cancelToken: widget.cancelToken + ) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _viewStateNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SaveEmailAsDraftsFailure || + failure is UpdateEmailDraftsFailure || + failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SaveEmailAsDraftsSuccess || success is UpdateEmailDraftsSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SavingMessageDialogViewState::_handleErrorStream: Exception = $error'); + if (error is UnknownError && error.message is List) { + popBack(result: SaveEmailAsDraftsFailure(SavingEmailToDraftsCanceledException())); + } else { + popBack(result: SaveEmailAsDraftsFailure(error)); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).savingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } + ); + }, + child: Text( + '...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ), + if (widget.onCancelSavingEmailToDraftsAction != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black87, + fontSize: 15 + ), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16), + onTapActionCallback: () { + _viewStateNotifier.value = dartz.Right(CancelSavingEmailToDrafts()); + widget.onCancelSavingEmailToDraftsAction!(cancelToken: widget.cancelToken); + }, + ), + ) + ], + ) + ], + ), + ), + ); + } + + String _getStatusMessage(Success success) { + if (success is GenerateEmailLoading) { + return AppLocalizations.of(context).creatingMessage; + } else if (success is CancelSavingEmailToDrafts) { + return AppLocalizations.of(context).canceling; + } else { + return AppLocalizations.of(context).savingMessageToDraftFolder; + } + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart new file mode 100644 index 0000000000..774b7600c6 --- /dev/null +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -0,0 +1,240 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +typedef OnCancelSendingEmailAction = Function({CancelToken? cancelToken}); + +class SendingMessageDialogView extends StatefulWidget { + + final CreateEmailRequest createEmailRequest; + final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; + final OnCancelSendingEmailAction? onCancelSendingEmailAction; + final CancelToken? cancelToken; + + const SendingMessageDialogView({ + super.key, + required this.createEmailRequest, + required this.createNewAndSendEmailInteractor, + this.onCancelSendingEmailAction, + this.cancelToken, + }); + + @override + State createState() => _SendingMessageDialogViewState(); +} + +class _SendingMessageDialogViewState extends State { + + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _streamSubscription = widget.createNewAndSendEmailInteractor + .execute( + createEmailRequest: widget.createEmailRequest, + cancelToken: widget.cancelToken + ) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _viewStateNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is SendEmailFailure || failure is GenerateEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is SendEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); + if (error is UnknownError && error.message is List) { + popBack(result: SendEmailFailure(exception: SendingEmailCanceledException())); + } else { + popBack(result: SendEmailFailure(exception: error)); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).sendingMessage.capitalizeFirstEach, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } + ); + }, + child: Text( + '...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ), + if (widget.onCancelSendingEmailAction != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black87, + fontSize: 15 + ), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16), + onTapActionCallback: () { + _viewStateNotifier.value = dartz.Right(CancelSendingEmail()); + widget.onCancelSendingEmailAction!(cancelToken: widget.cancelToken); + }, + ), + ) + ], + ) + ], + ), + ), + ); + } + + String _getStatusMessage(Success success) { + if (success is GenerateEmailLoading) { + return AppLocalizations.of(context).creatingMessage; + } else if (success is CancelSendingEmail) { + return AppLocalizations.of(context).canceling; + } else { + return AppLocalizations.of(context).sendingMessage; + } + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart similarity index 59% rename from lib/features/composer/presentation/widgets/web/drop_zone_widget.dart rename to lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart index 6391feb457..2faae585a8 100644 --- a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart +++ b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart @@ -3,42 +3,35 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -typedef OnAddAttachmentFromDropZone = Function(Attachment attachment); +typedef OnAttachmentDropZoneListener = Function(Attachment attachment); -class DropZoneWidget extends StatefulWidget { +class AttachmentDropZoneWidget extends StatelessWidget { + final ImagePaths imagePaths; final double? width; final double? height; - final OnAddAttachmentFromDropZone? addAttachmentFromDropZone; + final OnAttachmentDropZoneListener? onAttachmentDropZoneListener; - const DropZoneWidget({ + const AttachmentDropZoneWidget({ super.key, + required this.imagePaths, this.width, this.height, - this.addAttachmentFromDropZone + this.onAttachmentDropZoneListener }); - @override - State createState() => _DropZoneWidgetState(); -} - -class _DropZoneWidgetState extends State { - - final _imagePaths = Get.find(); - - bool _isDragging = false; - @override Widget build(BuildContext context) { return DragTarget( - builder: (context, candidateData, rejectedData) { - if (_isDragging) { - return Padding( + builder: (context, _, __) { + return SizedBox( + width: width, + height: height, + child: Padding( padding: DropZoneWidgetStyle.margin, child: DottedBorder( borderType: BorderType.RRect, @@ -48,20 +41,18 @@ class _DropZoneWidgetState extends State { dashPattern: DropZoneWidgetStyle.dashSize, child: Container( clipBehavior: Clip.antiAlias, - decoration: const ShapeDecoration( + decoration: ShapeDecoration( color: DropZoneWidgetStyle.backgroundColor, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), ), ), - width: widget.width, - height: widget.height, padding: DropZoneWidgetStyle.padding, alignment: AlignmentDirectional.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset(_imagePaths.icDropZoneIcon), + SvgPicture.asset(imagePaths.icDropZoneIcon), const SizedBox(height: DropZoneWidgetStyle.space), Text( AppLocalizations.of(context).dropFileHereToAttachThem, @@ -71,22 +62,10 @@ class _DropZoneWidgetState extends State { ), ), ), - ); - } else { - return SizedBox(width: widget.width, height: widget.height); - } - }, - onAccept: widget.addAttachmentFromDropZone, - onLeave: (attachment) { - if (_isDragging) { - setState(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - setState(() => _isDragging = true); - } + ), + ); }, + onAccept: onAttachmentDropZoneListener ); } } diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 90655a40e2..691f7136d3 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -19,7 +19,6 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback saveToDraftAction; final VoidCallback sendMessageAction; final OnRequestReadReceiptAction? requestReadReceiptAction; - final bool isSending; final _imagePaths = Get.find(); @@ -35,13 +34,13 @@ class BottomBarComposerWidget extends StatelessWidget { required this.saveToDraftAction, required this.sendMessageAction, this.requestReadReceiptAction, - this.isSending = false, }); @override Widget build(BuildContext context) { return Container( padding: BottomBarComposerWidgetStyle.padding, + height: BottomBarComposerWidgetStyle.height, color: BottomBarComposerWidgetStyle.backgroundColor, child: Row( children: [ @@ -128,7 +127,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const SizedBox(width: BottomBarComposerWidgetStyle.sendButtonSpace), TMailButtonWidget( - text: isSending ? AppLocalizations.of(context).sending : AppLocalizations.of(context).send, + text: AppLocalizations.of(context).send, icon: _imagePaths.icSend, iconAlignment: TextDirection.rtl, padding: BottomBarComposerWidgetStyle.sendButtonPadding, @@ -138,7 +137,6 @@ class BottomBarComposerWidget extends StatelessWidget { backgroundColor: BottomBarComposerWidgetStyle.sendButtonBackgroundColor, borderRadius: BottomBarComposerWidgetStyle.sendButtonRadius, onTapActionCallback: sendMessageAction, - isLoading: isSending, ) ] ), diff --git a/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart new file mode 100644 index 0000000000..058e4a7a94 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart @@ -0,0 +1,69 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:desktop_drop/desktop_drop.dart'; + +typedef OnLocalFileDropZoneListener = Function(DropDoneDetails details); + +class LocalFileDropZoneWidget extends StatelessWidget { + + final ImagePaths imagePaths; + final double? width; + final double? height; + final OnLocalFileDropZoneListener? onLocalFileDropZoneListener; + + const LocalFileDropZoneWidget({ + super.key, + required this.imagePaths, + this.width, + this.height, + this.onLocalFileDropZoneListener + }); + + @override + Widget build(BuildContext context) { + return DropTarget( + onDragDone: onLocalFileDropZoneListener, + child: SizedBox( + width: width, + height: height, + child: Padding( + padding: DropZoneWidgetStyle.margin, + child: DottedBorder( + borderType: BorderType.RRect, + radius: const Radius.circular(DropZoneWidgetStyle.radius), + color: DropZoneWidgetStyle.borderColor, + strokeWidth: DropZoneWidgetStyle.borderWidth, + dashPattern: DropZoneWidgetStyle.dashSize, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: DropZoneWidgetStyle.backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), + ), + ), + padding: DropZoneWidgetStyle.padding, + alignment: AlignmentDirectional.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(imagePaths.icDropZoneIcon), + const SizedBox(height: DropZoneWidgetStyle.space), + Text( + AppLocalizations.of(context).dropFileHereToAttachThem, + style: DropZoneWidgetStyle.labelTextStyle, + ) + ] + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index c3078af3cf..a2ac28b523 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -11,8 +11,6 @@ typedef OnChangeContentEditorAction = Function(String? text); typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); -typedef OnImageUploadSuccessAction = Function(FileUpload fileUpload); -typedef OnImageUploadFailureAction = Function(FileUpload? fileUpload, String? base64Str, UploadError error); typedef OnEditorTextSizeChanged = Function(int? size); class WebEditorWidget extends StatefulWidget { @@ -26,11 +24,10 @@ class WebEditorWidget extends StatefulWidget { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorWidget({ super.key, @@ -43,11 +40,10 @@ class WebEditorWidget extends StatefulWidget { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -133,6 +129,7 @@ class _WebEditorState extends State { initialText: widget.content, customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), spellCheck: true, + disableDragAndDrop: true, webInitialScripts: UnmodifiableListView([ WebScript( name: HtmlUtils.lineHeight100Percent.name, @@ -145,7 +142,7 @@ class _WebEditorState extends State { WebScript( name: HtmlUtils.unregisterDropListener.name, script: HtmlUtils.unregisterDropListener.script, - ), + ) ]) ), htmlToolbarOptions: const HtmlToolbarOptions( @@ -154,8 +151,8 @@ class _WebEditorState extends State { ), otherOptions: OtherOptions( height: height, - dropZoneWidth: dropZoneWidth, - dropZoneHeight: dropZoneHeight, + // dropZoneWidth: dropZoneWidth, + // dropZoneHeight: dropZoneHeight, ), callbacks: Callbacks( onBeforeCommand: widget.onChangeContent, @@ -173,12 +170,12 @@ class _WebEditorState extends State { onMouseDown: () => widget.onMouseDown?.call(context), onChangeSelection: widget.onEditorSettings, onChangeCodeview: widget.onChangeContent, - onImageUpload: widget.onImageUploadSuccessAction, - onImageUploadError: widget.onImageUploadFailureAction, onTextFontSizeChanged: widget.onEditorTextSizeChanged, onPaste: () => _editorController.evaluateJavascriptWeb( HtmlUtils.lineHeight100Percent.name ), + onDragEnter: widget.onDragEnter, + onDragLeave: () {}, ), ); } diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index c6cca3804b..2d243e1f52 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -2,21 +2,20 @@ import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.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_address.dart'; import 'package:model/autocomplete/auto_complete_pattern.dart'; -import 'package:model/user/user_profile.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; @@ -30,9 +29,8 @@ class ContactController extends BaseController { ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final searchQuery = SearchQuery.initial().obs; + final session = Rxn(); final listContactSearched = RxList(); - final scrollListViewController = ScrollController(); - final userProfile = Rxn(); GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -40,7 +38,6 @@ class ContactController extends BaseController { final Debouncer _deBouncerTime = Debouncer(const Duration(milliseconds: 300), initialValue: ''); AccountId? _accountId; - Session? _session; ContactArguments? arguments; EmailAddress? contactSelected; @@ -65,17 +62,14 @@ class ContactController extends BaseController { textInputSearchFocus.requestFocus(); if (arguments != null) { _accountId = arguments!.accountId; - _session = arguments!.session; - if (_session != null) { - userProfile.value = UserProfile(_session!.username.value); - } + session.value = arguments!.session; final listContactSelected = arguments!.listContactSelected; log('ContactController::onReady(): arguments: $arguments'); log('ContactController::onReady(): listContactSelected: $listContactSelected'); if (listContactSelected.isNotEmpty) { contactSelected = EmailAddress(listContactSelected.first, listContactSelected.first); } - injectAutoCompleteBindings(_session, _accountId); + injectAutoCompleteBindings(session.value, _accountId); } if (PlatformInfo.isMobile) { Future.delayed( @@ -90,7 +84,6 @@ class ContactController extends BaseController { textInputSearchFocus.dispose(); textInputSearchController.dispose(); _deBouncerTime.cancel(); - scrollListViewController.dispose(); super.onClose(); } diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 632917dc5d..fe2e23dcee 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -6,7 +6,6 @@ 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:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; import 'package:tmail_ui_user/features/contact/presentation/contact_controller.dart'; import 'package:tmail_ui_user/features/contact/presentation/utils/contact_utils.dart'; @@ -62,8 +61,6 @@ class ContactView extends GetWidget { searchInputController: controller.textInputSearchController, hasBackButton: false, hasSearchButton: true, - padding: EdgeInsets.zero, - heightSearchBar: 44, margin: ContactUtils.getPaddingSearchInputForm(context, controller.responsiveUtils), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), @@ -80,9 +77,11 @@ class ContactView extends GetWidget { ), if (PlatformInfo.isWeb) Obx(() { - final userEmail = controller.userProfile.value?.email; - if (userEmail != null && userEmail.isNotEmpty) { - final userEmailAddress = EmailAddress(AppLocalizations.of(context).me, controller.userProfile.value?.email); + final username = controller.session.value?.username.value ?? ''; + if (username.isNotEmpty) { + final userEmailAddress = EmailAddress( + AppLocalizations.of(context).me, + username); final fromMeSuggestionEmailAddress = SuggestionEmailAddress(userEmailAddress, state: SuggestionEmailState.valid); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), @@ -137,30 +136,26 @@ class ContactView extends GetWidget { return Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.scrollListViewController, - child: ListView.separated( - itemCount: controller.listContactSearched.length, - controller: controller.scrollListViewController, - separatorBuilder: (context, index) { - return Padding( - padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), - child: const Divider(height: 1, color: AppColor.colorDivider), - ); - }, - itemBuilder: (context, index) { - final emailAddress = controller.listContactSearched[index]; - final suggestionEmailAddress = _toSuggestionEmailAddress( - emailAddress, - controller.contactSelected != null ? [controller.contactSelected!] : [] - ); - return ContactSuggestionBoxItem( - suggestionEmailAddress, - padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), - ); - } - ), + child: ListView.separated( + itemCount: controller.listContactSearched.length, + separatorBuilder: (context, index) { + return Padding( + padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), + child: const Divider(height: 1, color: AppColor.colorDivider), + ); + }, + itemBuilder: (context, index) { + final emailAddress = controller.listContactSearched[index]; + final suggestionEmailAddress = _toSuggestionEmailAddress( + emailAddress, + controller.contactSelected != null ? [controller.contactSelected!] : [] + ); + return ContactSuggestionBoxItem( + suggestionEmailAddress, + padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), + selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), + ); + } ) ); } diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index 4f02e76096..9437b23eaa 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -387,7 +386,6 @@ class DestinationPickerController extends BaseMailboxController { final nameMailbox = newNameMailbox.value; if (nameMailbox != null && nameMailbox.isNotEmpty) { - final generateCreateId = Id(uuid.v1()); final parentId = mailboxDestination.value == PresentationMailbox.unifiedMailbox ? null : mailboxDestination.value?.id; @@ -396,7 +394,6 @@ class DestinationPickerController extends BaseMailboxController { _session!, accountId!, CreateNewMailboxRequest( - generateCreateId, MailboxName(nameMailbox), parentId: parentId)); } diff --git a/lib/features/destination_picker/presentation/destination_picker_view.dart b/lib/features/destination_picker/presentation/destination_picker_view.dart index c0781617f5..57cce40785 100644 --- a/lib/features/destination_picker/presentation/destination_picker_view.dart +++ b/lib/features/destination_picker/presentation/destination_picker_view.dart @@ -154,6 +154,7 @@ class DestinationPickerView extends GetWidget PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), scrollController: controller.destinationListScrollController, child: RefreshIndicator( @@ -179,6 +180,7 @@ class DestinationPickerView extends GetWidget PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), scrollController: controller.destinationListScrollController, child: RefreshIndicator( diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 05c236aff5..612468edf6 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -28,11 +28,14 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. abstract class EmailDataSource { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ); Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); @@ -71,15 +74,36 @@ abstract class EmailDataSource { MarkStarAction markStarAction ); - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ); - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); diff --git a/lib/features/email/data/datasource/html_datasource.dart b/lib/features/email/data/datasource/html_datasource.dart index 36f7d8975b..3545774623 100644 --- a/lib/features/email/data/datasource/html_datasource.dart +++ b/lib/features/email/data/datasource/html_datasource.dart @@ -1,6 +1,8 @@ - import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; -import 'package:model/model.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_content.dart'; abstract class HtmlDataSource { Future transformEmailContent( @@ -13,4 +15,11 @@ abstract class HtmlDataSource { String htmlContent, TransformConfiguration configuration ); + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }); + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}); } \ No newline at end of file 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 3983a16dfe..407c1c1a07 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -42,14 +42,23 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken, + } ) async { try { - return await emailAPI.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); + return await emailAPI.sendEmail( + session, + accountId, + emailRequest, + mailboxRequest: mailboxRequest, + cancelToken: cancelToken + ); } catch (error, stackTrace) { return await _sendEmailExceptionThrower.throwException(error, stackTrace); } @@ -107,23 +116,55 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.saveEmailAsDrafts(session, accountId, email); + return await emailAPI.saveEmailAsDrafts( + session, + accountId, + email, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.removeEmailDrafts(session, accountId, emailId); + return await emailAPI.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.updateEmailDrafts(session, accountId, newEmail, oldEmailId); + return await emailAPI.updateEmailDrafts( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } @@ -157,9 +198,19 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { return Future.sync(() async { - return await emailAPI.deleteEmailPermanently(session, accountId, emailId); + return await emailAPI.deleteEmailPermanently( + session, + accountId, + emailId, + cancelToken: cancelToken + ); }).catchError(_exceptionThrower.throwException); } 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 ae2c3d25f7..fe9a6eade6 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 @@ -71,7 +71,12 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { ); @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @@ -124,17 +129,35 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } @override - Future sendEmail(Session session, AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}) { + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } + ) { throw UnimplementedError(); } @@ -165,7 +188,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/datasource_impl/html_datasource_impl.dart b/lib/features/email/data/datasource_impl/html_datasource_impl.dart index 7ce83177e6..5d695d739d 100644 --- a/lib/features/email/data/datasource_impl/html_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/html_datasource_impl.dart @@ -1,4 +1,7 @@ import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; @@ -35,4 +38,26 @@ class HtmlDataSourceImpl extends HtmlDataSource { ); }).catchError(_exceptionThrower.throwException); } + + @override + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) { + return Future.sync(() async { + return await _htmlAnalyzer.replaceImageBase64ToImageCID( + emailContent: emailContent, + inlineAttachments: inlineAttachments + ); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) { + return Future.sync(() async { + return await _htmlAnalyzer.removeCollapsedExpandedSignatureEffect( + emailContent: emailContent + ); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index ca7550dc0e..904c320a05 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,10 +1,15 @@ import 'package:collection/collection.dart'; +import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; import 'package:html/parser.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; import 'package:model/email/email_content.dart'; import 'package:model/email/email_content_type.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; class HtmlAnalyzer { @@ -99,4 +104,57 @@ class HtmlAnalyzer { ); return htmlContentTransformed; } + + Future>> replaceImageBase64ToImageCID({ + required String emailContent, + required Map inlineAttachments + }) async { + final document = parse(emailContent); + final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); + + final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { + final idImg = imgTag.attributes['id']; + final cid = idImg!.replaceFirst('cid:', '').trim(); + imgTag.attributes['src'] = 'cid:$cid'; + imgTag.attributes.remove('id'); + return cid; + })).then((listCid) { + final listInlineAttachment = listCid + .map((cid) { + if (inlineAttachments.containsKey(cid)) { + return inlineAttachments[cid]!.toEmailBodyPart(charset: Constant.base64Charset); + } else { + return null; + } + }) + .whereNotNull() + .toSet(); + + return listInlineAttachment; + }); + + final newContent = document.body?.innerHtml ?? emailContent; + + return Tuple2(newContent, listInlineAttachment); + } + + Future removeCollapsedExpandedSignatureEffect({required String emailContent}) async { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: BEFORE = $emailContent'); + final document = parse(emailContent); + final signatureElements = document.querySelectorAll('div.tmail-signature'); + await Future.wait(signatureElements.map((signatureTag) async { + final signatureChildren = signatureTag.children; + for (var child in signatureChildren) { + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: CHILD = ${child.outerHtml}'); + if (child.attributes['class']?.contains('tmail-signature-button') == true) { + child.remove(); + } else if (child.attributes['class']?.contains('tmail-signature-content') == true) { + signatureTag.innerHtml = child.innerHtml; + } + } + })); + final newContent = document.body?.innerHtml ?? emailContent; + log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); + return newContent; + } } \ 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 94c71e57e2..4a148a663e 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -113,11 +113,14 @@ class EmailAPI with HandleSetErrorMixin { } } - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken, + } ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -125,9 +128,10 @@ class EmailAPI with HandleSetErrorMixin { MailboxId? outboxMailboxId; if (mailboxRequest != null) { + final generateCreateId = Id(_uuid.v1()); final setMailboxMethod = SetMailboxMethod(accountId) ..addCreate( - mailboxRequest.creationId, + generateCreateId, Mailbox( name: mailboxRequest.newName, parentId: mailboxRequest.parentId, @@ -139,7 +143,7 @@ class EmailAPI with HandleSetErrorMixin { outboxMailboxId = MailboxId(ReferenceId( ReferencePrefix.defaultPrefix, - mailboxRequest.creationId)); + generateCreateId)); emailNeedsToBeCreated = emailRequest.email.updatedEmail(newMailboxIds: {outboxMailboxId: true}); } else { outboxMailboxId = emailRequest.email.mailboxIds?.keys.first; @@ -198,7 +202,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -222,9 +226,7 @@ class EmailAPI with HandleSetErrorMixin { markAsAnsweredOrForwardedSetResponse ]); - if (emailCreated != null && mapErrors.isEmpty) { - return true; - } else { + if (emailCreated == null || mapErrors.isNotEmpty) { throw SetMethodException(mapErrors); } } @@ -477,7 +479,12 @@ class EmailAPI with HandleSetErrorMixin { }); } - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) async { + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) async { final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) ..addCreate(idCreateMethod, email); @@ -492,7 +499,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -509,7 +516,12 @@ class EmailAPI with HandleSetErrorMixin { } } - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) async { + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) async { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -523,56 +535,48 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, SetEmailResponse.deserialize); - return Future.sync(() async { - return setEmailResponse?.destroyed?.contains(emailId.id) == true; - }).catchError((error) { - throw error; - }); + final isEmailDestroyed = setEmailResponse?.destroyed?.contains(emailId.id) ?? false; + final mapErrors = handleSetResponse([setEmailResponse]); + + if (isEmailDestroyed && mapErrors.isEmpty) { + return isEmailDestroyed; + } else { + throw SetMethodException(mapErrors); + } } Future updateEmailDrafts( Session session, AccountId accountId, Email newEmail, - EmailId oldEmailId + EmailId oldEmailId, + {CancelToken? cancelToken} ) async { - final idCreateMethod = Id(_uuid.v1()); - final setEmailMethod = SetEmailMethod(accountId) - ..addCreate(idCreateMethod, newEmail) - ..addDestroy({oldEmailId.id}); - - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); - - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize + final emailCreated = await saveEmailAsDrafts( + session, + accountId, + newEmail, + cancelToken: cancelToken ); - final emailUpdated = setEmailResponse?.created?[idCreateMethod]; - final isEmailDeleted = setEmailResponse?.destroyed?.contains(oldEmailId.id); - final mapErrors = handleSetResponse([setEmailResponse]); - - if (emailUpdated != null && isEmailDeleted == true && mapErrors.isEmpty) { - return emailUpdated; - } else { - throw SetMethodException(mapErrors); + try { + await removeEmailDrafts( + session, + accountId, + oldEmailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailAPI::updateEmailDrafts: Exception = $e'); } + + return emailCreated; } Future> deleteMultipleEmailsPermanently( @@ -607,7 +611,12 @@ class EmailAPI with HandleSetErrorMixin { return List.empty(); } - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) async { + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -620,7 +629,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(); + .execute(cancelToken: cancelToken); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 91f9f1e750..2d699dc11e 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -54,13 +54,22 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ) { - return emailDataSource[DataSourceType.network]!.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); + return emailDataSource[DataSourceType.network]!.sendEmail( + session, + accountId, + emailRequest, + mailboxRequest: mailboxRequest, + cancelToken: cancelToken, + ); } @override @@ -132,18 +141,50 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { - return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts(session, accountId, email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( + session, + accountId, + email, + cancelToken: cancelToken + ); } @override - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { - return emailDataSource[DataSourceType.network]!.removeEmailDrafts(session, accountId, emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); } @override - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { - return emailDataSource[DataSourceType.network]!.updateEmailDrafts(session, accountId, newEmail, oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.updateEmailDrafts( + session, + accountId, + newEmail, + oldEmailId, + cancelToken: cancelToken + ); } @override @@ -172,8 +213,18 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { - return emailDataSource[DataSourceType.network]!.deleteEmailPermanently(session, accountId, emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ) { + return emailDataSource[DataSourceType.network]!.deleteEmailPermanently( + session, + accountId, + emailId, + cancelToken: cancelToken + ); } @override diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index 8057441639..8199c94c2f 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -6,4 +6,6 @@ class EmptyEmailContentException implements Exception {} class NotFoundEmailRecoveryActionException implements Exception {} -class NotFoundEmailBlobIdException implements Exception {} \ No newline at end of file +class NotFoundEmailBlobIdException implements Exception {} + +class CannotCreateEmailObjectException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_attachments_extension.dart b/lib/features/email/domain/extensions/list_attachments_extension.dart index 4bfa32c976..dfa9fb1048 100644 --- a/lib/features/email/domain/extensions/list_attachments_extension.dart +++ b/lib/features/email/domain/extensions/list_attachments_extension.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; +import 'package:model/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; @@ -19,4 +21,6 @@ extension ListAttachmentsExtension on List { .map((attachment) => attachment.blobId) .whereNotNull() .toSet(); + + Set toEmailBodyPart({String? charset}) => map((attachment) => attachment.toEmailBodyPart(charset: charset)).toSet(); } \ No newline at end of file diff --git a/lib/features/email/domain/model/event_action.dart b/lib/features/email/domain/model/event_action.dart index 144985f9d9..a14a5ebdb3 100644 --- a/lib/features/email/domain/model/event_action.dart +++ b/lib/features/email/domain/model/event_action.dart @@ -8,7 +8,8 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum EventActionType { yes, maybe, - no; + no, + mailToAttendees; String getLabelButton(BuildContext context) { switch(this) { @@ -18,6 +19,8 @@ enum EventActionType { return AppLocalizations.of(context).maybe; case EventActionType.no: return AppLocalizations.of(context).no; + case EventActionType.mailToAttendees: + return AppLocalizations.of(context).mailToAttendees; } } @@ -41,6 +44,8 @@ enum EventActionType { KeyWordIdentifierExtension.tentativelyAcceptedEventAttendance.generatePath(): null, KeyWordIdentifierExtension.rejectedEventAttendance.generatePath(): true, }); + case EventActionType.mailToAttendees: + return PatchObject({}); } } @@ -52,6 +57,8 @@ enum EventActionType { return AppLocalizations.of(context).youMayAttendThisMeeting; case EventActionType.no: return AppLocalizations.of(context).youWillNotAttendThisMeeting; + case EventActionType.mailToAttendees: + return ''; } } } @@ -62,6 +69,10 @@ class EventAction with EquatableMixin { EventAction(this.actionType, this.link); + factory EventAction.mailToAttendees() { + return EventAction(EventActionType.mailToAttendees, ''); + } + @override List get props => [actionType, link]; } \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index acedd10061..38488b0581 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -30,11 +30,14 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r abstract class EmailRepository { Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail( + Future sendEmail( Session session, AccountId accountId, EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + { + CreateNewMailboxRequest? mailboxRequest, + CancelToken? cancelToken + } ); Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); @@ -79,15 +82,36 @@ abstract class EmailRepository { TransformConfiguration transformConfiguration ); - Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + Future saveEmailAsDrafts( + Session session, + AccountId accountId, + Email email, + {CancelToken? cancelToken} + ); - Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + Future removeEmailDrafts( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); - Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId, + {CancelToken? cancelToken} + ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); + Future deleteEmailPermanently( + Session session, + AccountId accountId, + EmailId emailId, + {CancelToken? cancelToken} + ); Future getEmailState(Session session, AccountId accountId); diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index e7a5445902..3a276779ba 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -40,4 +40,8 @@ class PrintEmailAction extends EmailUIAction { @override List get props => [context, userEmail, email]; -} \ No newline at end of file +} + +class HideEmailContentViewAction extends EmailUIAction {} + +class ShowEmailContentViewAction extends EmailUIAction {} \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 849339bd01..a589ded969 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -96,7 +96,7 @@ class EmailBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/email/presentation/controller/email_supervisor_controller.dart b/lib/features/email/presentation/controller/email_supervisor_controller.dart index cda09da80a..bee43932af 100644 --- a/lib/features/email/presentation/controller/email_supervisor_controller.dart +++ b/lib/features/email/presentation/controller/email_supervisor_controller.dart @@ -1,6 +1,6 @@ import 'dart:collection'; + import 'package:collection/collection.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -114,7 +114,7 @@ class EmailSupervisorController extends GetxController { void _jumpToPage(int page) { if (PlatformInfo.isWeb) { - pageController?.jumpToPage(page); + onPageChanged(page); } else { pageController?.animateToPage( page, @@ -124,11 +124,12 @@ class EmailSupervisorController extends GetxController { } void updateScrollPhysicPageView(bool isScrollPageViewActivated) { - log('EmailSupervisorController::updateScrollPhysicPageView:isScrollPageViewActivated: $isScrollPageViewActivated'); - if (PlatformInfo.isWeb || !isScrollPageViewActivated) { - scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); - } else { - scrollPhysicsPageView.value = null; + if (PlatformInfo.isMobile) { + if (!isScrollPageViewActivated) { + scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); + } else { + scrollPhysicsPageView.value = null; + } } } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 5c65219bee..68fdcf9e20 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -16,6 +16,8 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; @@ -72,8 +74,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; @@ -115,7 +117,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final _downloadManager = Get.find(); final _printUtils = Get.find(); final _attachmentListScrollController = ScrollController(); - final emailContentScrollController = ScrollController(); final GetEmailContentInteractor _getEmailContentInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; @@ -143,6 +144,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final emailLoadedViewState = Rx>(Right(UIState.idle)); final emailUnsubscribe = Rxn(); final attachmentsViewState = RxMap>(); + final isEmailContentHidden = RxBool(false); EmailId? _currentEmailId; Identity? _identitySelected; @@ -190,7 +192,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void onClose() { _downloadProgressStateController.close(); _attachmentListScrollController.dispose(); - emailContentScrollController.dispose(); super.onClose(); } @@ -289,6 +290,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (action is CloseEmailDetailedViewAction) { closeEmailView(context: currentContext); mailboxDashBoardController.clearEmailUIAction(); + } else if (action is HideEmailContentViewAction) { + isEmailContentHidden.value = true; + mailboxDashBoardController.clearEmailUIAction(); + } else if (action is ShowEmailContentViewAction) { + isEmailContentHidden.value = false; + mailboxDashBoardController.clearEmailUIAction(); } }); @@ -327,8 +334,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { markAsEmailRead(selectedEmail, ReadActions.markAsRead, MarkReadAction.tap); } - if (_identitySelected == null) { + if (mailboxDashBoardController.listIdentities.isEmpty) { _getAllIdentities(); + } else { + _initializeSelectedIdentity(mailboxDashBoardController.listIdentities); } } @@ -418,17 +427,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _getAllIdentitiesSuccess(GetAllIdentitiesSuccess success) { if (success.identities?.isNotEmpty == true) { - if (currentEmail != null) { - final currentMailbox = getMailboxContain(currentEmail!); - log('SingleEmailController::_getAllIdentitiesSuccess():currentMailbox: $currentMailbox'); - if (_isBelongToTeamMailboxes(currentMailbox)) { - _setUpDefaultIdentityForTeamMailbox(success.identities!, currentMailbox!); - } else { - _setUpDefaultIdentity(success.identities!); - } + _initializeSelectedIdentity(success.identities!); + } + } + + void _initializeSelectedIdentity(List identities) { + if (currentEmail != null) { + final currentMailbox = getMailboxContain(currentEmail!); + if (_isBelongToTeamMailboxes(currentMailbox)) { + _setUpDefaultIdentityForTeamMailbox(identities, currentMailbox!); } else { - _setUpDefaultIdentity(success.identities!); + _setUpDefaultIdentity(identities); } + } else { + _setUpDefaultIdentity(identities); } } @@ -621,6 +633,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { blobCalendarEvent.value = null; emailUnsubscribe.value = null; _printEmailAction = null; + _identitySelected = null; if (isEmailClosing) { emailLoadedViewState.value = Right(UIState.idle); viewState.value = Right(UIState.idle); @@ -1193,8 +1206,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _handleSendReceiptToSenderAction(BuildContext context) { final accountId = mailboxDashBoardController.accountId.value; - final userProfile = mailboxDashBoardController.userProfile.value; - if (accountId == null || userProfile == null) { + final session = mailboxDashBoardController.sessionCurrent; + if (accountId == null || session == null) { return; } @@ -1219,7 +1232,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { return; } - final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? userProfile.email; + final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? session.username.value; log('SingleEmailController::_handleSendReceiptToSenderAction():receiverEmailAddress: $receiverEmailAddress'); final mdnToSender = _generateMDN(context, currentEmail!, receiverEmailAddress); final sendReceiptRequest = SendReceiptToSenderRequest( @@ -1661,7 +1674,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { break; } } - + void _acceptCalendarEventAction(EmailId emailId) { if (_acceptCalendarEventInteractor == null || _displayingEventBlobId == null @@ -1787,7 +1800,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void handleViewAttachmentAction(BuildContext context, Attachment attachment) { if (PlatformInfo.isWeb) { - if (PlatformInfo.isCanvasKit && attachment.isDisplayedPDFIcon) { + if (PlatformInfo.isCanvasKit && attachment.validatePDFIcon()) { previewPDFFileAction(context, attachment); } else { downloadAttachmentForWeb(attachment); @@ -1826,4 +1839,24 @@ class SingleEmailController extends BaseController with AppLoaderMixin { }, ); } + + void handleMailToAttendees(CalendarOrganizer? organizer, List? attendees) { + final listEmailAddressAttendees = attendees + ?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value)) + .toList() ?? []; + + if (organizer != null) { + listEmailAddressAttendees.add(EmailAddress(organizer.name, organizer.mailto?.value)); + } + + final listEmailAddressMailTo = listEmailAddressAttendees + .where((emailAddress) => emailAddress.emailAddress.isNotEmpty && emailAddress.emailAddress != mailboxDashBoardController.sessionCurrent?.username.value) + .toSet() + .toList(); + + log('SingleEmailController::handleMailToAttendees: listEmailAddressMailTo = $listEmailAddressMailTo'); + mailboxDashBoardController.goToComposer( + ComposerArguments.fromMailtoUri(listEmailAddress: listEmailAddressMailTo) + ); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 3848123135..a64fa314aa 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart'; import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; @@ -17,7 +16,6 @@ import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; @@ -46,123 +44,122 @@ class EmailView extends GetWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: controller.responsiveUtils.isWebDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white, - appBar: PlatformInfo.isIOS - ? PreferredSize( - preferredSize: const Size(double.infinity, 100), + return SelectionArea( + child: Scaffold( + backgroundColor: controller.responsiveUtils.isWebDesktop(context) + ? AppColor.colorBgDesktop + : Colors.white, + appBar: PlatformInfo.isIOS + ? PreferredSize( + preferredSize: const Size(double.infinity, 100), + child: Obx(() { + if (controller.currentEmail != null) { + return SafeArea( + top: false, + bottom: false, + child: EmailViewAppBarWidget( + key: const Key('email_view_app_bar_widget'), + presentationEmail: controller.currentEmail!, + mailboxContain: _getMailboxContain(controller.currentEmail!), + isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, + onBackAction: () => controller.closeEmailView(context: context), + onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), + onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position) + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ) + : null, + body: SafeArea( + right: controller.responsiveUtils.isLandscapeMobile(context), + left: controller.responsiveUtils.isLandscapeMobile(context), + bottom: !PlatformInfo.isIOS, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: controller.responsiveUtils.isWebDesktop(context) + ? BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), + color: Colors.white) + : const BoxDecoration(color: Colors.white), + margin: _getMarginEmailView(context), child: Obx(() { - if (controller.currentEmail != null) { - return SafeArea( - top: false, - bottom: false, - child: EmailViewAppBarWidget( - key: const Key('email_view_app_bar_widget'), - presentationEmail: controller.currentEmail!, - mailboxContain: _getMailboxContain(controller.currentEmail!), - isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, - onBackAction: () => controller.closeEmailView(context: context), - onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), - onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position) - ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ) - : null, - body: SafeArea( - right: controller.responsiveUtils.isLandscapeMobile(context), - left: controller.responsiveUtils.isLandscapeMobile(context), - bottom: !PlatformInfo.isIOS, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: controller.responsiveUtils.isWebDesktop(context) - ? BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: Colors.white) - : const BoxDecoration(color: Colors.white), - margin: _getMarginEmailView(context), - child: Obx(() { - final currentEmail = controller.currentEmail; - if (currentEmail != null) { - return Column(children: [ - if (!PlatformInfo.isIOS) - Obx(() => EmailViewAppBarWidget( - key: const Key('email_view_app_bar_widget'), - presentationEmail: currentEmail, - mailboxContain: _getMailboxContain(currentEmail), - isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, - onBackAction: () => controller.closeEmailView(context: context), - onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), - onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position), - optionsWidget: PlatformInfo.isWeb && controller.emailSupervisorController.supportedPageView.isTrue - ? _buildNavigatorPageViewWidgets(context) - : null, - )), - Obx(() { - final vacation = controller.mailboxDashBoardController.vacationResponse.value; - if (vacation?.vacationResponderIsValid == true && - ( - controller.responsiveUtils.isMobile(context) || - controller.responsiveUtils.isTablet(context) || - controller.responsiveUtils.isLandscapeMobile(context) - ) - ) { - return VacationNotificationMessageWidget( - margin: const EdgeInsets.only(left: 12, right: 12, bottom: 5), - vacationResponse: vacation!, - actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, - actionEndNow: controller.mailboxDashBoardController.disableVacationResponder - ); - } else { - return const SizedBox.shrink(); - } - }), - Expanded( - child: LayoutBuilder(builder: (context, constraints) { - log('EmailView::build: EMAIL_BODY_MAX_HEIGHT = ${constraints.maxHeight}'); - return Obx(() { - if (controller.emailSupervisorController.supportedPageView.isTrue) { + final currentEmail = controller.currentEmail; + if (currentEmail != null) { + return Column(children: [ + if (!PlatformInfo.isIOS) + Obx(() => EmailViewAppBarWidget( + key: const Key('email_view_app_bar_widget'), + presentationEmail: currentEmail, + mailboxContain: _getMailboxContain(currentEmail), + isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, + onBackAction: () => controller.closeEmailView(context: context), + onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), + onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(context: context, presentationEmail: presentationEmail, position: position), + optionsWidget: PlatformInfo.isWeb && controller.emailSupervisorController.supportedPageView.isTrue + ? _buildNavigatorPageViewWidgets(context) + : null, + )), + Obx(() { + final vacation = controller.mailboxDashBoardController.vacationResponse.value; + if (vacation?.vacationResponderIsValid == true && + ( + controller.responsiveUtils.isMobile(context) || + controller.responsiveUtils.isTablet(context) || + controller.responsiveUtils.isLandscapeMobile(context) + ) + ) { + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 5), + vacationResponse: vacation!, + actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, + actionEndNow: controller.mailboxDashBoardController.disableVacationResponder + ); + } else { + return const SizedBox.shrink(); + } + }), + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + return Obx(() { + bool supportedPageView = controller.emailSupervisorController.supportedPageView.isTrue && PlatformInfo.isMobile; final currentListEmail = controller.emailSupervisorController.currentListEmail; - return PageView.builder( - physics: controller.emailSupervisorController.scrollPhysicsPageView.value, - itemCount: currentListEmail.length, - allowImplicitScrolling: true, - controller: controller.emailSupervisorController.pageController, - onPageChanged: controller.emailSupervisorController.onPageChanged, - itemBuilder: (context, index) { - final currentEmail = currentListEmail[index]; - if (PlatformInfo.isMobile) { - return SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: Obx(() => _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent, - maxBodyHeight: constraints.maxHeight - )) - ) - ); - } else { - return Obx(() { - final calendarEvent = controller.calendarEvent; - if (currentEmail.hasCalendarEvent && calendarEvent != null) { - return Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, + + if (supportedPageView) { + return PageView.builder( + physics: controller.emailSupervisorController.scrollPhysicsPageView.value, + itemCount: currentListEmail.length, + allowImplicitScrolling: true, + controller: controller.emailSupervisorController.pageController, + onPageChanged: controller.emailSupervisorController.onPageChanged, + itemBuilder: (context, index) { + final currentEmail = currentListEmail[index]; + if (PlatformInfo.isMobile) { + return SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: Obx(() => _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: controller.calendarEvent, + maxBodyHeight: constraints.maxHeight + )) + ) + ); + } else { + return Obx(() { + final calendarEvent = controller.calendarEvent; + if (currentEmail.hasCalendarEvent && calendarEvent != null) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), child: SingleChildScrollView( physics : const ClampingScrollPhysics(), - controller: controller.emailContentScrollController, child: Container( width: double.infinity, alignment: Alignment.center, @@ -175,46 +172,42 @@ class EmailView extends GetWidget { ) ) ), - ), - ); - } else { - return _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - maxBodyHeight: constraints.maxHeight - ); - } - }); + ); + } else { + return _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + maxBodyHeight: constraints.maxHeight + ); + } + }); + } } - } - ); - } else { - if (PlatformInfo.isMobile) { - return SingleChildScrollView( - physics : const ClampingScrollPhysics(), - child: Container( - width: double.infinity, - alignment: Alignment.center, - color: Colors.white, - child: Obx(() => _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - calendarEvent: controller.calendarEvent, - maxBodyHeight: constraints.maxHeight - )) - ) ); } else { - return Obx(() { - final calendarEvent = controller.calendarEvent; - if (currentEmail.hasCalendarEvent && calendarEvent != null) { - return Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.emailContentScrollController, + if (PlatformInfo.isMobile) { + return SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: Obx(() => _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + calendarEvent: controller.calendarEvent, + maxBodyHeight: constraints.maxHeight + )) + ) + ); + } else { + return Obx(() { + final calendarEvent = controller.calendarEvent; + if (currentEmail.hasCalendarEvent && calendarEvent != null) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), child: SingleChildScrollView( physics : const ClampingScrollPhysics(), - controller: controller.emailContentScrollController, child: Container( width: double.infinity, alignment: Alignment.center, @@ -228,33 +221,33 @@ class EmailView extends GetWidget { ) ) ), - ), - ); - } else { - return _buildEmailMessage( - context: context, - presentationEmail: currentEmail, - maxBodyHeight: constraints.maxHeight - ); - } - }); + ); + } else { + return _buildEmailMessage( + context: context, + presentationEmail: currentEmail, + maxBodyHeight: constraints.maxHeight + ); + } + }); + } } - } - }); - }), - ), - EmailViewBottomBarWidget( - key: const Key('email_view_button_bar'), - presentationEmail: currentEmail, - emailActionCallback: controller.pressEmailAction - ), - ]); - } else { - return const EmailViewEmptyWidget(); - } - }) + }); + }), + ), + EmailViewBottomBarWidget( + key: const Key('email_view_button_bar'), + presentationEmail: currentEmail, + emailActionCallback: controller.pressEmailAction + ), + ]); + } else { + return const EmailViewEmptyWidget(); + } + }) + ) ) - ) + ), ); } @@ -339,9 +332,9 @@ class EmailView extends GetWidget { responsiveUtils: controller.responsiveUtils, attachments: controller.attachments, imagePaths: controller.imagePaths, - onDragStarted: controller.mailboxDashBoardController.enableDraggableApp, + onDragStarted: controller.mailboxDashBoardController.enableAttachmentDraggableApp, onDragEnd: (details) { - controller.mailboxDashBoardController.disableDraggableApp(); + controller.mailboxDashBoardController.disableAttachmentDraggableApp(); }, downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment), viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment), @@ -367,6 +360,7 @@ class EmailView extends GetWidget { controller.onCalendarEventReplyAction(eventActionType, presentationEmail.id!), calendarEventReplying: controller.calendarEventProcessing, presentationEmail: controller.currentEmail, + onMailtoAttendeesAction: controller.handleMailToAttendees, )), if (calendarEvent.getTitleEventAction(context, emailAddressSender ?? []).isNotEmpty) CalendarEventActionBannerWidget( @@ -376,7 +370,7 @@ class EmailView extends GetWidget { Obx(() => CalendarEventDetailWidget( calendarEvent: calendarEvent, emailContent: controller.currentEmailLoaded?.htmlContent ?? '', - isDraggableAppActive: controller.mailboxDashBoardController.isDraggableAppActive, + isDraggableAppActive: controller.mailboxDashBoardController.isAttachmentDraggableAppActive, onOpenComposerAction: controller.openNewComposerAction, onOpenNewTabAction: controller.openNewTabAction, onMailtoDelegateAction: controller.openMailToLink, @@ -384,6 +378,7 @@ class EmailView extends GetWidget { controller.onCalendarEventReplyAction(eventActionType, presentationEmail.id!), calendarEventReplying: controller.calendarEventProcessing, presentationEmail: controller.currentEmail, + onMailtoAttendeesAction: controller.handleMailToAttendees, )), ], ) @@ -407,7 +402,14 @@ class EmailView extends GetWidget { mailtoDelegate: controller.openMailToLink, direction: AppUtils.getCurrentDirection(context), ), - if (controller.mailboxDashBoardController.isDraggableAppActive) + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) + PointerInterceptor( + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + ) + ), + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) PointerInterceptor( child: SizedBox( width: constraints.maxWidth, @@ -420,6 +422,30 @@ class EmailView extends GetWidget { }), ), ); + } else if (PlatformInfo.isIOS + && !controller.responsiveUtils.isScreenWithShortestSide(context)) { + return Obx(() { + if (controller.isEmailContentHidden.isTrue) { + return const SizedBox.shrink(); + } else { + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: EmailViewStyles.mobileContentVerticalMargin, + horizontal: EmailViewStyles.mobileContentHorizontalMargin + ), + child: LayoutBuilder(builder: (context, constraints) { + return HtmlContentViewer( + contentHtml: allEmailContents, + initialWidth: constraints.maxWidth, + direction: AppUtils.getCurrentDirection(context), + onMailtoDelegateAction: controller.openMailToLink, + onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, + onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView, + ); + }) + ); + } + }); } else { return Padding( padding: const EdgeInsetsDirectional.symmetric( diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index c1d1db8ae0..64fc09d289 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,38 +1,9 @@ -import 'package:core/data/constants/constant.dart'; -import 'package:core/domain/extensions/media_type_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; extension AttachmentExtension on Attachment { + String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; - String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) { - final mediaType = type ?? fileMediaType; - - if (isDisplayedPDFIcon) { - return imagePaths.icFilePdf; - } - - if (mediaType == null) { - return imagePaths.icFileEPup; - } - if (mediaType.isDocFile()) { - return imagePaths.icFileDocx; - } else if (mediaType.isExcelFile()) { - return imagePaths.icFileXlsx; - } else if (mediaType.isPowerPointFile()) { - return imagePaths.icFilePptx; - } else if (mediaType.isPdfFile()) { - return imagePaths.icFilePdf; - } else if (mediaType.isZipFile()) { - return imagePaths.icFileZip; - } else if (mediaType.isImageFile()) { - return imagePaths.icFilePng; - } - return imagePaths.icFileEPup; - } - - bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType - || (type?.mimeType == Constant.octetStreamMimeType - && name?.endsWith(Constant.pdfExtension) == true); + bool validatePDFIcon() => type?.validatePDFIcon(fileName: name) ?? false; } \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/composer_arguments_extension.dart b/lib/features/email/presentation/extensions/composer_arguments_extension.dart new file mode 100644 index 0000000000..625576a76e --- /dev/null +++ b/lib/features/email/presentation/extensions/composer_arguments_extension.dart @@ -0,0 +1,24 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +extension ComposerArgumentsExtension on ComposerArguments { + + ComposerArguments withIdentity({List? identities}) { + return ComposerArguments( + emailActionType: emailActionType, + presentationEmail: presentationEmail, + emailContents: emailContents, + attachments: attachments, + mailboxRole: mailboxRole, + listEmailAddress: listEmailAddress, + listSharedMediaFile: listSharedMediaFile, + sendingEmail: sendingEmail, + subject: subject, + body: body, + messageId: messageId, + references: references, + previousEmailId: previousEmailId, + identities: identities, + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 527ad67af7..0b6291fd13 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -1,3 +1,4 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -22,6 +23,7 @@ class ComposerArguments extends RouterArguments { final MessageIdsHeaderValue? messageId; final MessageIdsHeaderValue? references; final EmailId? previousEmailId; + final List? identities; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -37,6 +39,7 @@ class ComposerArguments extends RouterArguments { this.messageId, this.references, this.previousEmailId, + this.identities, }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -170,5 +173,6 @@ class ComposerArguments extends RouterArguments { body, messageId, references, + identities, ]; } \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_subject_styles.dart b/lib/features/email/presentation/styles/email_subject_styles.dart index 177944a342..c8d7a8481c 100644 --- a/lib/features/email/presentation/styles/email_subject_styles.dart +++ b/lib/features/email/presentation/styles/email_subject_styles.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; class EmailSubjectStyles { static const double textSize = 20; static const int? maxLines = PlatformInfo.isWeb ? 2 : null; - static const int? minLines = PlatformInfo.isWeb ? 1 : null; static const Color textColor = AppColor.colorNameEmail; static const Color cursorColor = AppColor.colorTextButton; diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 6b8bca98ca..d4eb188455 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -1,6 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/mail/mail_address.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart'; -import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; @@ -68,7 +71,16 @@ class EmailUtils { required String internalDomain }) { log('EmailUtils::isSameDomain: emailAddress = $emailAddress | internalDomain = $internalDomain'); - return GetUtils.isEmail(emailAddress) && + return EmailUtils.isEmailAddressValid(emailAddress) && emailAddress.split('@').last.toLowerCase() == internalDomain.toLowerCase(); } + + static bool isEmailAddressValid(String address) { + try { + return GetUtils.isEmail(address) && MailAddress.validateAddress(address).asString().isNotEmpty; + } catch(e) { + logError('EmailUtils::isEmailAddressValid: Exception = $e'); + return false; + } + } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart index 607e2a1353..8834905b8a 100644 --- a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart @@ -16,8 +16,8 @@ class AttendeeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( style: const TextStyle( fontSize: AttendeeWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart index 36c6507467..7fe70559fe 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart @@ -48,8 +48,8 @@ class CalendarEventActionBannerWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: TextStyle( fontSize: CalendarEventActionBannerStyles.titleTextSize, fontWeight: FontWeight.w400, diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart index ffe7bbe5d8..73dd1e37c4 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart @@ -14,6 +14,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final VoidCallback? onMailToAttendeesAction; final _responsiveUtils = Get.find(); @@ -23,6 +24,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { required this.calendarEventReplying, this.margin, this.presentationEmail, + this.onMailToAttendeesAction, }); @override @@ -57,7 +59,7 @@ class CalendarEventActionButtonWidget extends StatelessWidget { color: _getButtonBorderColor(action) ), onTapActionCallback: _getCallbackFunction(action), - ), + ) )) .toList(), ), @@ -95,6 +97,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.loadingBackgroundColor; } + return CalendarEventActionButtonWidgetStyles.backgroundColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.backgroundColor; } } @@ -118,6 +122,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.selectedTextColor; } + return CalendarEventActionButtonWidgetStyles.textColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.textColor; } } @@ -141,6 +147,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return CalendarEventActionButtonWidgetStyles.selectedBackgroundColor; } + return CalendarEventActionButtonWidgetStyles.textColor; + case EventActionType.mailToAttendees: return CalendarEventActionButtonWidgetStyles.textColor; } } @@ -162,6 +170,8 @@ class CalendarEventActionButtonWidget extends StatelessWidget { return null; } return () => onCalendarEventReplyActionClick(eventActionType); + case EventActionType.mailToAttendees: + return onMailToAttendeesAction; } } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart index c571072b55..748bb307c3 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart @@ -1,6 +1,8 @@ import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_detail_widget_styles.dart'; @@ -13,6 +15,8 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +typedef OnMailtoAttendeesAction = Function(CalendarOrganizer? organizer, List? participants); + class CalendarEventDetailWidget extends StatelessWidget { final CalendarEvent calendarEvent; @@ -24,6 +28,7 @@ class CalendarEventDetailWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final OnMailtoAttendeesAction? onMailtoAttendeesAction; const CalendarEventDetailWidget({ super.key, @@ -36,6 +41,7 @@ class CalendarEventDetailWidget extends StatelessWidget { this.onOpenComposerAction, this.onMailtoDelegateAction, this.presentationEmail, + this.onMailtoAttendeesAction, }); @override @@ -99,6 +105,10 @@ class CalendarEventDetailWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index 852d6566de..fb65d3f25f 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_ev import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_information_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart'; @@ -24,6 +25,7 @@ class CalendarEventInformationWidget extends StatelessWidget { final OnCalendarEventReplyActionClick onCalendarEventReplyActionClick; final bool calendarEventReplying; final PresentationEmail? presentationEmail; + final OnMailtoAttendeesAction? onMailtoAttendeesAction; final _responsiveUtils = Get.find(); @@ -35,6 +37,7 @@ class CalendarEventInformationWidget extends StatelessWidget { this.onOpenNewTabAction, this.onOpenComposerAction, this.presentationEmail, + this.onMailtoAttendeesAction, }); @override @@ -76,8 +79,8 @@ class CalendarEventInformationWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: const TextStyle( fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, fontWeight: FontWeight.w500, @@ -122,6 +125,10 @@ class CalendarEventInformationWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), @@ -147,8 +154,8 @@ class CalendarEventInformationWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( style: const TextStyle( fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, fontWeight: FontWeight.w500, @@ -193,6 +200,10 @@ class CalendarEventInformationWidget extends StatelessWidget { onCalendarEventReplyActionClick: onCalendarEventReplyActionClick, calendarEventReplying: calendarEventReplying, presentationEmail: presentationEmail, + onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( + calendarEvent.organizer, + calendarEvent.participants, + ), ), ], ), diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart index f38edc797a..9ab413796f 100644 --- a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart @@ -36,8 +36,8 @@ class EventAttendeeInformationWidget extends StatelessWidget { ), ), ), - Expanded(child: RichText( - text: TextSpan( + Expanded(child: Text.rich( + TextSpan( style: const TextStyle( fontSize: EventAttendeeInformationWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart index 4ac0dce035..6ed9f4ae5a 100644 --- a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart @@ -15,8 +15,8 @@ class OrganizerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( style: const TextStyle( fontSize: OrganizerWidgetStyles.textSize, fontWeight: FontWeight.w500, diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index ba896651c9..06e6566ae0 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -64,7 +64,7 @@ class EmailAttachmentsWidget extends StatelessWidget { child: Text( AppLocalizations.of(context).titleHeaderAttachment( attachments.length, - filesize(attachments.totalSize(), 1) + filesize(attachments.totalSize, 1) ), style: const TextStyle( fontSize: EmailAttachmentsStyles.headerTextSize, diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index acee9f0884..4c385aa602 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -1,10 +1,9 @@ +import 'package:collection/collection.dart'; 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/style_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -14,10 +13,9 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; -import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/prefix_recipient_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -47,233 +45,254 @@ class _EmailReceiverWidgetState extends State { final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); - final _scrollController = ScrollController(); bool _isDisplayAll = false; @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Padding( - padding: EdgeInsets.only(top: _isDisplayAll - ? DirectionUtils.isDirectionRTLByLanguage(context) ? 3 : 5.5 - : 0), - child: PlatformInfo.isWeb - ? Container( - constraints: BoxConstraints( - maxHeight: _isDisplayAll && widget.maxHeight != null - ? widget.maxHeight! / 2 - _offsetTop - : double.infinity - ), - child: ScrollbarListView( - scrollController: _scrollController, - child: SingleChildScrollView( - controller: _scrollController, - child: _buildEmailAddressOfReceiver( - context, - widget.emailSelected, - _isDisplayAll, - widget.maxWidth - ), - ), + if (PlatformInfo.isWeb) { + if (_isDisplayAll) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + constraints: BoxConstraints(maxHeight: _maxHeight), + child: ListView( + primary: false, + shrinkWrap: true, + padding: EdgeInsets.zero, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ], ), ) - : _buildEmailAddressOfReceiver( - context, - widget.emailSelected, - _isDisplayAll, - widget.maxWidth - ), - )), - if (_isDisplayAll) - Padding( - padding: EdgeInsets.symmetric( - vertical: DirectionUtils.isDirectionRTLByLanguage(context) ? 0 : 6), - child: MaterialTextButton( - padding: DirectionUtils.isDirectionRTLByLanguage(context) - ? const EdgeInsets.symmetric(horizontal: 8, vertical: 4) - : null, - onTap: () => setState(() => _isDisplayAll = false), - label: AppLocalizations.of(context).hide, - ) - ) - ] - ); - } - - Widget _buildEmailAddressOfReceiver( - BuildContext context, - PresentationEmail presentationEmail, - bool isDisplayFull, - double maxWidth - ) { - if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - color: Colors.white, - constraints: BoxConstraints(maxWidth: _getMaxWidthEmailAddressDisplayed(context, maxWidth)), - child: ListView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - children: [ - if (presentationEmail.to.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.to, - isDisplayFull - ), - if (presentationEmail.cc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.cc, - isDisplayFull - ), - if (presentationEmail.bcc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.bcc, - isDisplayFull - ), - ] ), - ), - Transform( - transform: Matrix4.translationValues(0.0, -5.0, 0.0), - child: TMailButtonWidget.fromIcon( - icon: _imagePaths.icChevronDown, + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).hide, + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + color: AppColor.primaryColor, + fontSize: 15 + ), backgroundColor: Colors.transparent, - onTapActionCallback: () => setState(() => _isDisplayAll = true), + onTapActionCallback: () => setState(() => _isDisplayAll = false), + ) + ] + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: _getMaxWidth(context), + maxHeight: 34, + ), + child: ListView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ] + ), ), - ) - ] - ); + if (widget.emailSelected.numberOfAllEmailAddress() > 1) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icChevronDown, + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = true), + ) + ] + ); + } } else { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (presentationEmail.to.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.to, - isDisplayFull - ), - if (presentationEmail.cc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.cc, - isDisplayFull - ), - if (presentationEmail.bcc.numberEmailAddress() > 0) - _buildEmailAddressByPrefix( - context, - presentationEmail, - PrefixEmailAddress.bcc, - isDisplayFull + if (_isDisplayAll) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + constraints: BoxConstraints(maxHeight: _maxHeight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + _buildRecipientsWidgetToDisplayFull( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ], + ), + ) + ), + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).hide, + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + color: AppColor.primaryColor, + fontSize: 15 + ), + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = false), + ) + ] + ); + } else { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + constraints: BoxConstraints(maxWidth: _getMaxWidth(context)), + child: ListView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + if (widget.emailSelected.to.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.to, + listEmailAddress: PrefixEmailAddress.to.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.cc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.cc, + listEmailAddress: PrefixEmailAddress.cc.listEmailAddress(widget.emailSelected) + ), + if (widget.emailSelected.bcc.numberEmailAddress() > 0) + ..._buildRecipientsWidget( + context: context, + prefixEmailAddress: PrefixEmailAddress.bcc, + listEmailAddress: PrefixEmailAddress.bcc.listEmailAddress(widget.emailSelected) + ), + ] + ), ), - ], - ); + if (widget.emailSelected.numberOfAllEmailAddress() > 1) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icChevronDown, + backgroundColor: Colors.transparent, + onTapActionCallback: () => setState(() => _isDisplayAll = true), + ) + ] + ); + } } } - Widget _buildEmailAddressByPrefix( - BuildContext context, - PresentationEmail presentationEmail, - PrefixEmailAddress prefixEmailAddress, - bool isDisplayFull - ) { + List _buildRecipientsTag({required List listEmailAddress}) { + return listEmailAddress + .mapIndexed((index, emailAddress) => TMailButtonWidget.fromText( + text: index == listEmailAddress.length - 1 + ? emailAddress.asString() + : '${emailAddress.asString()},', + textStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.black, + fontSize: 16, + ), + padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), + backgroundColor: Colors.transparent, + onTapActionCallback: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), + onLongPressActionCallback: () => AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress), + )) + .toList(); + } + + + Widget _buildRecipientsWidgetToDisplayFull({ + required BuildContext context, + required PrefixEmailAddress prefixEmailAddress, + required List listEmailAddress, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(top: 5), - child: Text( - '${prefixEmailAddress.asName(context)}:', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColor.colorEmailAddressFull - ) - ), - ), - if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) - _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull + PrefixRecipientWidget(prefixEmailAddress: prefixEmailAddress), + Expanded( + child: Wrap( + children: _buildRecipientsTag(listEmailAddress: listEmailAddress) ) - else - Expanded(child: _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull - )) - ] + ) + ], ); } - Widget _buildListEmailAddressWidget( - BuildContext context, - List listEmailAddress, - bool isDisplayFull - ) { - final lastEmailAddress = listEmailAddress.last; - final emailAddressWidgets = listEmailAddress.map((emailAddress) { - return MaterialTextButton( - label: lastEmailAddress == emailAddress - ? emailAddress.asString() - : '${emailAddress.asString()},', - onTap: () => widget.openEmailAddressDetailAction?.call(context, emailAddress), - onLongPress: () { - AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); - }, - borderRadius: 8, - labelColor: Colors.black, - labelSize: 16, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - ); - }).toList(); - - if (isDisplayFull) { - return Wrap(children: emailAddressWidgets); - } else { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - child: Row( - crossAxisAlignment:CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: emailAddressWidgets - ), - ); - } + List _buildRecipientsWidget({ + required BuildContext context, + required PrefixEmailAddress prefixEmailAddress, + required List listEmailAddress, + }) { + return [ + PrefixRecipientWidget(prefixEmailAddress: prefixEmailAddress), + ..._buildRecipientsTag(listEmailAddress: listEmailAddress) + ]; } - double _getMaxWidthEmailAddressDisplayed(BuildContext context, double maxWidth) { + double _getMaxWidth(BuildContext context) { if (_responsiveUtils.isPortraitMobile(context)) { - return maxWidth - _maxSizeFullDisplayEmailAddressArrowDownButton; + return widget.maxWidth - _maxSizeFullDisplayEmailAddressArrowDownButton; } else if (_responsiveUtils.isWebDesktop(context)) { - return maxWidth / 2; + return widget.maxWidth / 2; } else { - return maxWidth * 3/4; + return widget.maxWidth * 3/4; } } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); + double get _maxHeight { + return _isDisplayAll && widget.maxHeight != null + ? widget.maxHeight! / 2 - _offsetTop + : double.infinity; } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_subject_widget.dart b/lib/features/email/presentation/widgets/email_subject_widget.dart index e7611f9634..6132c433c6 100644 --- a/lib/features/email/presentation/widgets/email_subject_widget.dart +++ b/lib/features/email/presentation/widgets/email_subject_widget.dart @@ -12,11 +12,9 @@ class EmailSubjectWidget extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: EmailSubjectStyles.padding, - child: SelectableText( + child: Text( presentationEmail.getEmailTitle(), maxLines: EmailSubjectStyles.maxLines, - minLines: EmailSubjectStyles.minLines, - cursorColor: EmailSubjectStyles.cursorColor, style: const TextStyle( fontSize: EmailSubjectStyles.textSize, color: EmailSubjectStyles.textColor, 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 55bb5541b1..d28f0d1db2 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 @@ -37,8 +37,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - color: Colors.white, + return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 16), child: Row( crossAxisAlignment: emailSelected.numberOfAllEmailAddress() > 0 @@ -55,32 +54,26 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { children: [ if (emailSelected.from?.isNotEmpty == true) Row(children: [ - Expanded( - child: Row( - children: [ - Flexible(child: Transform( - transform: Matrix4.translationValues(-5.0, 0.0, 0.0), - child: EmailSenderBuilder( - emailAddress: emailSelected.from!.first, - openEmailAddressDetailAction: openEmailAddressDetailAction, - ) - )), - if (!emailSelected.isSubscribed && emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context)) - TMailButtonWidget.fromText( - text: AppLocalizations.of(context).unsubscribe, - textStyle: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 14, - color: AppColor.colorTextBody, - decoration: TextDecoration.underline, - ), - padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), - backgroundColor: Colors.transparent, - onTapActionCallback: () => onEmailActionClick?.call(emailSelected, EmailActionType.unsubscribe), - ), - ], - ) - ), + Flexible(child: Transform( + transform: Matrix4.translationValues(-5.0, 0.0, 0.0), + child: EmailSenderBuilder( + emailAddress: emailSelected.from!.first, + openEmailAddressDetailAction: openEmailAddressDetailAction, + ) + )), + if (!emailSelected.isSubscribed && emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context)) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).unsubscribe, + textStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + color: AppColor.colorTextBody, + decoration: TextDecoration.underline, + ), + padding: const EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8), + backgroundColor: Colors.transparent, + onTapActionCallback: () => onEmailActionClick?.call(emailSelected, EmailActionType.unsubscribe), + ), ReceivedTimeBuilder(emailSelected: emailSelected), ]), if (emailSelected.numberOfAllEmailAddress() > 0) diff --git a/lib/features/email/presentation/widgets/prefix_recipient_widget.dart b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart new file mode 100644 index 0000000000..03fe772c2a --- /dev/null +++ b/lib/features/email/presentation/widgets/prefix_recipient_widget.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; + +class PrefixRecipientWidget extends StatelessWidget { + final PrefixEmailAddress prefixEmailAddress; + + const PrefixRecipientWidget({super.key, required this.prefixEmailAddress}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + '${prefixEmailAddress.asName(context)}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColor.colorEmailAddressFull + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index 302ac7837d..4266539e20 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -23,7 +23,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/identity_extension.dart'; import 'package:model/extensions/session_extension.dart'; -import 'package:model/user/user_profile.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; @@ -68,20 +67,19 @@ class IdentityCreatorController extends BaseController { final isMobileEditorFocus = RxBool(false); final isCompressingInlineImage = RxBool(false); - final RichTextController keyboardRichTextController = RichTextController(); - final RichTextMobileTabletController richTextMobileTabletController = RichTextMobileTabletController(); - final RichTextWebController richTextWebController = RichTextWebController(); final TextEditingController inputNameIdentityController = TextEditingController(); final TextEditingController inputBccIdentityController = TextEditingController(); final FocusNode inputNameIdentityFocusNode = FocusNode(); final FocusNode inputBccIdentityFocusNode = FocusNode(); final ScrollController scrollController = ScrollController(); + RichTextMobileTabletController? richTextMobileTabletController; + RichTextWebController? richTextWebController; + String? _nameIdentity; String? _contentHtmlEditor; AccountId? accountId; Session? session; - UserProfile? userProfile; Identity? identity; IdentityCreatorArguments? arguments; @@ -112,6 +110,11 @@ class IdentityCreatorController extends BaseController { @override void onInit() { super.onInit(); + if (PlatformInfo.isWeb) { + richTextWebController = RichTextWebController(); + } else { + richTextMobileTabletController = RichTextMobileTabletController(); + } log('IdentityCreatorController::onInit():arguments: ${Get.arguments}'); arguments = Get.arguments; } @@ -123,7 +126,6 @@ class IdentityCreatorController extends BaseController { if (arguments != null) { accountId = arguments!.accountId; session = arguments!.session; - userProfile = arguments!.userProfile; identity = arguments!.identity; actionType.value = arguments!.actionType; _checkDefaultIdentityIsSupported(); @@ -135,13 +137,18 @@ class IdentityCreatorController extends BaseController { @override void onClose() { log('IdentityCreatorController::onClose():'); - keyboardRichTextController.dispose(); inputNameIdentityFocusNode.dispose(); inputBccIdentityFocusNode.dispose(); inputNameIdentityController.dispose(); inputBccIdentityController.dispose(); scrollController.dispose(); - richTextWebController.onClose(); + if (PlatformInfo.isWeb) { + richTextWebController?.onClose(); + richTextWebController = null; + } else { + richTextMobileTabletController?.onClose(); + richTextMobileTabletController = null; + } super.onClose(); } @@ -174,7 +181,7 @@ class IdentityCreatorController extends BaseController { if (identity?.signatureAsString.isNotEmpty == true) { updateContentHtmlEditor(arguments?.identity?.signatureAsString ?? ''); if (PlatformInfo.isWeb) { - richTextWebController.editorController.setText(arguments?.identity?.signatureAsString ?? ''); + richTextWebController?.editorController.setText(arguments?.identity?.signatureAsString ?? ''); } } } @@ -214,8 +221,8 @@ class IdentityCreatorController extends BaseController { void _setDefaultEmailAddressList() { listEmailAddressOfReplyTo.add(noneEmailAddress); - if (userProfile != null && userProfile?.email.isNotEmpty == true) { - final userEmailAddress = EmailAddress(null, userProfile!.email); + if (session?.username.value.isNotEmpty == true) { + final userEmailAddress = EmailAddress(null, session?.username.value); listEmailAddressDefault.add(userEmailAddress); listEmailAddressOfReplyTo.addAll(listEmailAddressDefault); } @@ -302,9 +309,9 @@ class IdentityCreatorController extends BaseController { Future _getSignatureHtmlText() async { if (PlatformInfo.isWeb) { - return richTextWebController.editorController.getText(); + return richTextWebController?.editorController.getText(); } else { - return keyboardRichTextController.htmlEditorApi?.getText(); + return richTextMobileTabletController?.richTextController.htmlEditorApi?.getText(); } } @@ -424,7 +431,7 @@ class IdentityCreatorController extends BaseController { void clearFocusEditor(BuildContext context) { if (PlatformInfo.isMobile) { - keyboardRichTextController.htmlEditorApi?.unfocus(); + richTextMobileTabletController?.htmlEditorApi?.unfocus(); KeyboardUtils.hideSystemKeyboardMobile(); } KeyboardUtils.hideKeyboard(context); @@ -437,15 +444,15 @@ class IdentityCreatorController extends BaseController { } void initRichTextForMobile(BuildContext context, HtmlEditorApi editorApi) { - richTextMobileTabletController.htmlEditorApi = editorApi; - keyboardRichTextController.onCreateHTMLEditor( + richTextMobileTabletController?.htmlEditorApi = editorApi; + richTextMobileTabletController?.richTextController.onCreateHTMLEditor( editorApi, onEnterKeyDown: _onEnterKeyDownOnMobile, onFocus: _onFocusHTMLEditorOnMobile, context: context ); - keyboardRichTextController.htmlEditorApi?.onFocusOut = () { - keyboardRichTextController.hideRichTextView(); + richTextMobileTabletController?.htmlEditorApi?.onFocusOut = () { + richTextMobileTabletController?.richTextController.hideRichTextView(); isMobileEditorFocus.value = false; }; } @@ -549,9 +556,9 @@ class IdentityCreatorController extends BaseController { } } else { if (PlatformInfo.isWeb) { - richTextWebController.insertImageAsBase64(platformFile: file); + richTextWebController?.insertImageAsBase64(platformFile: file, maxWidth: maxWidth); } else if (PlatformInfo.isMobile) { - richTextMobileTabletController.insertImageData(platformFile: file, maxWidth: maxWidth); + richTextMobileTabletController?.insertImageData(platformFile: file, maxWidth: maxWidth); if (file.path != null) { _deleteCompressedFileOnMobile(file.path!); } diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index e4668d184a..042d754e4f 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -147,14 +147,14 @@ class IdentityCreatorView extends GetWidget ? AppColor.colorBackgroundKeyboard : AppColor.colorBackgroundKeyboardAndroid, isLandScapeMode: controller.responsiveUtils.isLandscapeMobile(context), - richTextController: controller.keyboardRichTextController, + richTextController: controller.richTextMobileTabletController!.richTextController, titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, insertImage: () => controller.pickImage(context), ), - richTextController: controller.keyboardRichTextController, + richTextController: controller.richTextMobileTabletController!.richTextController, paddingChild: EdgeInsets.zero, child: responsiveWidget ); @@ -311,7 +311,7 @@ class IdentityCreatorView extends GetWidget children: [ if (PlatformInfo.isWeb) ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, + richTextWebController: controller.richTextWebController!, padding: const EdgeInsets.only(bottom: 12), extendedOption: [ Padding( @@ -344,7 +344,7 @@ class IdentityCreatorView extends GetWidget Widget _buildHtmlEditorWeb(BuildContext context, String initContent) { return html_editor_browser.HtmlEditor( key: const Key('identity_create_editor_web'), - controller: controller.richTextWebController.editorController, + controller: controller.richTextWebController!.editorController, htmlEditorOptions: html_editor_browser.HtmlEditorOptions( shouldEnsureVisible: true, hint: '', @@ -361,16 +361,16 @@ class IdentityCreatorView extends GetWidget onBeforeCommand: controller.updateContentHtmlEditor, onChangeContent: controller.updateContentHtmlEditor, onInit: () { - controller.richTextWebController.editorController.setFullScreen(); + controller.richTextWebController?.editorController.setFullScreen(); controller.updateContentHtmlEditor(initContent); }, onFocus: () { FocusManager.instance.primaryFocus?.unfocus(); Future.delayed(const Duration(milliseconds: 500), () { - controller.richTextWebController.editorController.setFocus(); + controller.richTextWebController?.editorController.setFocus(); }); - controller.richTextWebController.closeAllMenuPopup(); + controller.richTextWebController?.closeAllMenuPopup(); }, - onChangeSelection: controller.richTextWebController.onEditorSettingsChange, + onChangeSelection: controller.richTextWebController?.onEditorSettingsChange, onChangeCodeview: controller.updateContentHtmlEditor ), ); diff --git a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart index 0259f2b443..16245588f2 100644 --- a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart +++ b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart @@ -3,20 +3,17 @@ import 'package:equatable/equatable.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/identities/identity.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; class IdentityCreatorArguments with EquatableMixin { final AccountId accountId; final Session session; - final UserProfile userProfile; final IdentityActionType actionType; final Identity? identity; IdentityCreatorArguments( this.accountId, this.session, - this.userProfile, { this.identity, this.actionType = IdentityActionType.create @@ -27,7 +24,6 @@ class IdentityCreatorArguments with EquatableMixin { List get props => [ accountId, session, - userProfile, - identity, + identity, actionType]; } \ No newline at end of file diff --git a/lib/features/login/data/datasource/authentication_datasource.dart b/lib/features/login/data/datasource/authentication_datasource.dart index a7f775ee71..b574c2764d 100644 --- a/lib/features/login/data/datasource/authentication_datasource.dart +++ b/lib/features/login/data/datasource/authentication_datasource.dart @@ -1,7 +1,6 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; abstract class AuthenticationDataSource { - Future authenticationUser(Uri baseUrl, UserName userName, Password password); + Future authenticationUser(Uri baseUrl, UserName userName, Password password); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart index 781563a414..7691cc0916 100644 --- a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart @@ -1,16 +1,18 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class AuthenticationDataSourceImpl extends AuthenticationDataSource { - AuthenticationDataSourceImpl(); + final ExceptionThrower _exceptionThrower; + + AuthenticationDataSourceImpl(this._exceptionThrower); @override - Future authenticationUser(Uri baseUrl, UserName userName, Password password) { - return Future.sync(() { - return UserProfile(userName.value); - }); + Future authenticationUser(Uri baseUrl, UserName userName, Password password) async { + return Future.sync(() async { + return userName; + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 1e75a46e33..34d21b5db6 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dio/dio.dart'; -import 'package:get/get_connect/http/src/request/request.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/password.dart'; @@ -122,8 +121,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey]; requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); - requestOptions.headers[HttpHeaders.contentTypeHeader] = uploadExtra[FileUploader.typeExtraKey]; - requestOptions.headers[HttpHeaders.contentLengthHeader] = uploadExtra[FileUploader.sizeExtraKey]; final newOptions = Options( method: requestOptions.method, @@ -155,11 +152,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Stream>? _getDataUploadRequest(dynamic mapUploadExtra) { - final currentPlatform = mapUploadExtra[FileUploader.platformExtraKey]; - if (currentPlatform == 'web') { - return BodyBytesStream.fromBytes(mapUploadExtra[FileUploader.bytesExtraKey]); - } else { - return File(mapUploadExtra[FileUploader.filePathExtraKey]).openRead(); + try { + String? filePath = mapUploadExtra[FileUploader.filePathExtraKey]; + if (filePath?.isNotEmpty == true) { + return File(filePath!).openRead(); + } else { + return mapUploadExtra[FileUploader.streamDataExtraKey]; + } + } catch(e) { + log('AuthorizationInterceptors::_getDataUploadRequest: Exception = $e'); + return null; } } diff --git a/lib/features/login/data/repository/authentication_repository_impl.dart b/lib/features/login/data/repository/authentication_repository_impl.dart index ea2e77e4c4..410a8d7a42 100644 --- a/lib/features/login/data/repository/authentication_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_repository_impl.dart @@ -1,6 +1,5 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; @@ -10,7 +9,7 @@ class AuthenticationRepositoryImpl extends AuthenticationRepository { AuthenticationRepositoryImpl(this.loginDataSource); @override - Future authenticationUser(Uri baseUrl, UserName userName, Password password) { + Future authenticationUser(Uri baseUrl, UserName userName, Password password) { return loginDataSource.authenticationUser(baseUrl, userName, password); } } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_repository.dart b/lib/features/login/domain/repository/authentication_repository.dart index 03aa122dbe..189f29c3e3 100644 --- a/lib/features/login/domain/repository/authentication_repository.dart +++ b/lib/features/login/domain/repository/authentication_repository.dart @@ -1,8 +1,7 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/user/user_profile.dart'; abstract class AuthenticationRepository { - Future authenticationUser(Uri baseUrl, UserName userName, Password password); + Future authenticationUser(Uri baseUrl, UserName userName, Password password); } \ No newline at end of file diff --git a/lib/features/login/domain/state/authentication_user_state.dart b/lib/features/login/domain/state/authentication_user_state.dart index bf9fc83653..611e806a6c 100644 --- a/lib/features/login/domain/state/authentication_user_state.dart +++ b/lib/features/login/domain/state/authentication_user_state.dart @@ -1,16 +1,16 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:model/user/user_profile.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; class AuthenticationUserLoading extends LoadingState {} class AuthenticationUserSuccess extends UIState { - final UserProfile userProfile; + final UserName userName; - AuthenticationUserSuccess(this.userProfile); + AuthenticationUserSuccess(this.userName); @override - List get props => [userProfile]; + List get props => [userName]; } class AuthenticationUserFailure extends FeatureFailure { diff --git a/lib/features/login/domain/usecases/authentication_user_interactor.dart b/lib/features/login/domain/usecases/authentication_user_interactor.dart index ba1f4f8272..c60c802521 100644 --- a/lib/features/login/domain/usecases/authentication_user_interactor.dart +++ b/lib/features/login/domain/usecases/authentication_user_interactor.dart @@ -29,24 +29,24 @@ class AuthenticationInteractor { }) async* { try { yield Right(AuthenticationUserLoading()); - final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); + final username = await authenticationRepository.authenticationUser(baseUrl, userName, password); await Future.wait([ credentialRepository.saveBaseUrl(baseUrl), credentialRepository.storeAuthenticationInfo( AuthenticationInfoCache( - userName.value, + username.value, password.value ) ), ]); await _accountRepository.setCurrentAccount( PersonalAccount( - userName.value, + username.value, AuthenticationType.basic, isSelected: true ) ); - yield Right(AuthenticationUserSuccess(user)); + yield Right(AuthenticationUserSuccess(username)); } catch (e) { yield Left(AuthenticationUserFailure(e)); } diff --git a/lib/features/mailbox/data/network/mailbox_api.dart b/lib/features/mailbox/data/network/mailbox_api.dart index 6d7df8bfe9..901a0c4739 100644 --- a/lib/features/mailbox/data/network/mailbox_api.dart +++ b/lib/features/mailbox/data/network/mailbox_api.dart @@ -148,9 +148,11 @@ class MailboxAPI with HandleSetErrorMixin { } Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest request) async { + final generateCreateId = Id(_uuid.v1()); + final setMailboxMethod = SetMailboxMethod(accountId) ..addCreate( - request.creationId, + generateCreateId, Mailbox( name: request.newName, isSubscribed: IsSubscribed(request.isSubscribed), @@ -176,14 +178,14 @@ class MailboxAPI with HandleSetErrorMixin { final mapMailboxCreated = setMailboxResponse?.created; if (mapMailboxCreated != null && - mapMailboxCreated.containsKey(request.creationId)) { - final mailboxCreated = mapMailboxCreated[request.creationId]!; + mapMailboxCreated.containsKey(generateCreateId)) { + final mailboxCreated = mapMailboxCreated[generateCreateId]!; final newMailboxCreated = mailboxCreated.toMailbox( request.newName, parentId: request.parentId); return newMailboxCreated; } else { - throw _parseErrorForSetMailboxResponse(setMailboxResponse, request.creationId); + throw _parseErrorForSetMailboxResponse(setMailboxResponse, generateCreateId); } } diff --git a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart index 3c50807a5f..b9273b5cb6 100644 --- a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart +++ b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart @@ -1,17 +1,14 @@ import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class CreateNewMailboxRequest with EquatableMixin { final MailboxName newName; - final Id creationId; final MailboxId? parentId; final bool isSubscribed; CreateNewMailboxRequest( - this.creationId, this.newName, { this.parentId, @@ -21,7 +18,6 @@ class CreateNewMailboxRequest with EquatableMixin { @override List get props => [ - creationId, newName, parentId, isSubscribed diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 8dd3ff48ec..25247f7891 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -8,7 +8,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -591,9 +590,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - final generateCreateId = Id(uuid.v1()); _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - generateCreateId, result.newName, parentId: result.mailboxLocation?.id)); } diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 069ebf4107..ed13f9f8da 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; @@ -85,13 +85,25 @@ class MailboxView extends BaseMailboxView { : const SizedBox.shrink(), ), Obx(() { - final appInformation = controller.mailboxDashBoardController.appInformation.value; - if (appInformation != null - && !controller.isSelectionEnabled()) { - if (controller.responsiveUtils.isLandscapeMobile(context)) { - return const SizedBox.shrink(); - } - return _buildVersionInformation(context, appInformation); + if (!controller.isSelectionEnabled() && controller.responsiveUtils.isPortraitMobile(context)) { + return Container( + color: AppColor.colorBgMailbox, + width: double.infinity, + padding: const EdgeInsets.all(16), + child: SafeArea( + top: false, + child: ApplicationVersionWidget( + applicationManager: controller.applicationManager, + padding: EdgeInsets.zero, + title: '${AppLocalizations.of(context).version} ', + textStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 16, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w500 + ), + ), + ), + ); } else { return const SizedBox.shrink(); } @@ -167,7 +179,9 @@ class MailboxView extends BaseMailboxView { return const SizedBox.shrink(); } return UserInformationWidget( - userProfile: controller.mailboxDashBoardController.userProfile.value, + userName: controller.mailboxDashBoardController.accountId.value != null + ? controller.mailboxDashBoardController.sessionCurrent?.username + : null, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( @@ -365,20 +379,4 @@ class MailboxView extends BaseMailboxView { } }).toList() ?? []; } - - Widget _buildVersionInformation(BuildContext context, PackageInfo packageInfo) { - return Container( - color: AppColor.colorBgMailbox, - width: double.infinity, - padding: const EdgeInsets.all(16), - child: SafeArea( - top: false, - child: Text( - '${AppLocalizations.of(context).version} ${packageInfo.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 19b3f23ed0..14847e24fe 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -3,7 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; @@ -68,13 +68,10 @@ class MailboxView extends BaseMailboxView { textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), logoSVG: controller.imagePaths.icTMailLogo ), - Obx(() { - if (controller.mailboxDashBoardController.appInformation.value != null) { - return _buildVersionInformation(context, controller.mailboxDashBoardController.appInformation.value!); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.mailboxDashBoardController.applicationManager, + padding: const EdgeInsets.only(top: 4), + ) ]) ); } @@ -91,6 +88,7 @@ class MailboxView extends BaseMailboxView { PointerDeviceKind.mouse, PointerDeviceKind.trackpad }, + scrollbars: false ), child: RefreshIndicator( color: AppColor.primaryColor, @@ -103,7 +101,9 @@ class MailboxView extends BaseMailboxView { child: Column(children: [ if (!controller.responsiveUtils.isDesktop(context)) Obx(() => UserInformationWidget( - userProfile: controller.mailboxDashBoardController.userProfile.value, + userName: controller.mailboxDashBoardController.accountId.value != null + ? controller.mailboxDashBoardController.sessionCurrent?.username + : null, subtitle: AppLocalizations.of(context).manage_account, onSubtitleClick: controller.mailboxDashBoardController.goToSettings, border: const Border( @@ -132,7 +132,11 @@ class MailboxView extends BaseMailboxView { const SizedBox(height: 8), const Divider(color: AppColor.colorDividerMailbox, height: 1), Padding( - padding: const EdgeInsetsDirectional.symmetric(vertical: 4), + padding: EdgeInsetsDirectional.only( + top: 4, + bottom: 4, + start: controller.responsiveUtils.isDesktop(context) ? 0 : 16 + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -250,7 +254,10 @@ class MailboxView extends BaseMailboxView { controller.imagePaths, categories, controller, - toggleMailboxCategories: controller.toggleMailboxCategories + toggleMailboxCategories: controller.toggleMailboxCategories, + padding: controller.responsiveUtils.isDesktop(context) + ? null + : const EdgeInsetsDirectional.only(start: 16) ), AnimatedContainer( duration: const Duration(milliseconds: 400), @@ -331,15 +338,4 @@ class MailboxView extends BaseMailboxView { controller.mailboxDashBoardController.dragSelectedMultipleEmailToMailboxAction(listEmails, presentationMailbox); } } - - Widget _buildVersionInformation(BuildContext context, PackageInfo packageInfo) { - return Container( - padding: const EdgeInsets.only(top: 4), - child: Text( - 'v.${packageInfo.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget.dart b/lib/features/mailbox/presentation/widgets/user_information_widget.dart index a4a6140cdd..6f378e66cf 100644 --- a/lib/features/mailbox/presentation/widgets/user_information_widget.dart +++ b/lib/features/mailbox/presentation/widgets/user_information_widget.dart @@ -4,14 +4,15 @@ import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; -import 'package:model/user/user_profile.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/username_extension.dart'; import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnSubtitleClick = void Function(); class UserInformationWidget extends StatelessWidget { - final UserProfile? userProfile; + final UserName? userName; final String? subtitle; final EdgeInsetsGeometry? titlePadding; final OnSubtitleClick? onSubtitleClick; @@ -20,7 +21,7 @@ class UserInformationWidget extends StatelessWidget { const UserInformationWidget({ Key? key, - this.userProfile, + this.userName, this.subtitle, this.titlePadding, this.onSubtitleClick, @@ -35,7 +36,7 @@ class UserInformationWidget extends StatelessWidget { decoration: BoxDecoration(border: border), child: Row(children: [ (AvatarBuilder() - ..text(userProfile != null ? userProfile!.getAvatarText() : '') + ..text(userName?.firstCharacter ?? '') ..backgroundColor(Colors.white) ..textColor(Colors.black) ..addBoxShadows([const BoxShadow( @@ -48,7 +49,7 @@ class UserInformationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ TextOverflowBuilder( - userProfile != null ? '${userProfile?.email}' : '', + userName?.value ?? '', style: const TextStyle( fontSize: 17, color: AppColor.colorNameEmail, diff --git a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart b/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart deleted file mode 100644 index 0598dd21c1..0000000000 --- a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:model/user/user_profile.dart'; - -class GetUserProfileSuccess extends UIState { - final UserProfile userProfile; - - GetUserProfileSuccess(this.userProfile); - - @override - List get props => []; -} - -class GetUserProfileFailure extends FeatureFailure { - - GetUserProfileFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart index 6674a50292..5857b4eb46 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart @@ -8,7 +8,7 @@ class RemoveComposerCacheOnWebInteractor { RemoveComposerCacheOnWebInteractor(this.composerCacheRepository); - Either execute() { + Future> execute() async { try { composerCacheRepository.removeComposerCacheOnWeb(); return Right(RemoveComposerCacheSuccess()); diff --git a/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart deleted file mode 100644 index e08c19eef5..0000000000 --- a/lib/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart'; - -class SaveComposerCacheOnWebInteractor { - final ComposerCacheRepository composerCacheRepository; - - SaveComposerCacheOnWebInteractor(this.composerCacheRepository); - - Either execute(Email email) { - try { - composerCacheRepository.saveComposerCacheOnWeb(email); - return Right(SaveComposerCacheSuccess()); - } catch (exception) { - return Left(SaveComposerCacheFailure(exception)); - } - } -} diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index c2004adb93..375143f3ec 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -8,9 +8,7 @@ import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart'; @@ -71,7 +69,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unr import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_spam_report_state_interactor.dart'; @@ -81,6 +78,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; @@ -170,12 +171,12 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -204,7 +205,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => SearchDataSourceImpl( Get.find(), Get.find())); @@ -280,7 +281,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find()) ); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => MarkAsEmailReadInteractor( Get.find(), @@ -328,14 +328,6 @@ class MailboxDashBoardBindings extends BaseBindings { )); SendingQueueInteractorBindings().dependencies(); Get.lazyPut(() => StoreSessionInteractor(Get.find())); - Get.lazyPut(() => SaveEmailAsDraftsInteractor( - Get.find(), - Get.find() - )); - Get.lazyPut(() => UpdateEmailDraftsInteractor( - Get.find(), - Get.find() - )); Get.lazyPut(() => UnsubscribeEmailInteractor(Get.find())); Get.lazyPut(() => RestoredDeletedMessageInteractor( Get.find(), @@ -345,6 +337,12 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find() )); + + IdentityInteractorsBindings().dependencies(); + Get.lazyPut(() => GetAllIdentitiesInteractor( + Get.find(), + Get.find() + )); } @override diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 7b259b794d..6cbc94eb79 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -18,12 +18,12 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; @@ -36,13 +36,12 @@ import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_draft import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; @@ -67,6 +66,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int import 'package:tmail_ui_user/features/email/domain/usecases/restore_deleted_message_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/unsubscribe_email_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/composer_arguments_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_recovery_arguments.dart'; @@ -82,6 +82,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_app_da import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; @@ -99,8 +100,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; @@ -180,12 +183,12 @@ class MailboxDashBoardController extends ReloadableController { final GetAllSendingEmailInteractor _getAllSendingEmailInteractor; final StoreSessionInteractor _storeSessionInteractor; final EmptySpamFolderInteractor _emptySpamFolderInteractor; - final SaveEmailAsDraftsInteractor _saveEmailAsDraftsInteractor; - final UpdateEmailDraftsInteractor _updateEmailDraftsInteractor; final DeleteSendingEmailInteractor _deleteSendingEmailInteractor; final UnsubscribeEmailInteractor _unsubscribeEmailInteractor; final RestoredDeletedMessageInteractor _restoreDeletedMessageInteractor; final GetRestoredDeletedMessageInterator _getRestoredDeletedMessageInteractor; + final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; + final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -199,12 +202,10 @@ class MailboxDashBoardController extends ReloadableController { final selectedMailbox = Rxn(); final selectedEmail = Rxn(); final accountId = Rxn(); - final userProfile = Rxn(); final dashBoardAction = Rxn(); final mailboxUIAction = Rxn(); final emailUIAction = Rxn(); final dashboardRoute = DashboardRoutes.waiting.obs; - final appInformation = Rxn(); final currentSelectMode = SelectMode.INACTIVE.obs; final filterMessageOption = FilterMessageOption.all.obs; final listEmailSelected = [].obs; @@ -216,8 +217,9 @@ class MailboxDashBoardController extends ReloadableController { final searchMailboxActivated = RxBool(false); final listSendingEmails = RxList(); final refreshingMailboxState = Rx>(Right(UIState.idle)); - final draggableAppState = Rxn(); + final attachmentDraggableAppState = Rxn(); final isRecoveringDeletedMessage = RxBool(false); + final localFileDraggableAppState = Rxn(); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -226,6 +228,7 @@ class MailboxDashBoardController extends ReloadableController { final listResultSearch = RxList(); PresentationMailbox? outboxMailbox; ComposerArguments? composerArguments; + List? _identities; late StreamSubscription _emailAddressStreamSubscription; late StreamSubscription _emailContentStreamSubscription; @@ -260,18 +263,21 @@ class MailboxDashBoardController extends ReloadableController { this._getAllSendingEmailInteractor, this._storeSessionInteractor, this._emptySpamFolderInteractor, - this._saveEmailAsDraftsInteractor, - this._updateEmailDraftsInteractor, this._deleteSendingEmailInteractor, this._unsubscribeEmailInteractor, this._restoreDeletedMessageInteractor, this._getRestoredDeletedMessageInteractor, + this._removeComposerCacheOnWebInteractor, + this._getAllIdentitiesInteractor, ); @override - void onInit() { + void onInit() async { _registerStreamListener(); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await applicationManager.initUserAgent(); + }); super.onInit(); } @@ -281,7 +287,6 @@ class MailboxDashBoardController extends ReloadableController { _registerPendingEmailContents(); _registerPendingFileInfo(); _handleArguments(); - _getAppVersion(); super.onReady(); } @@ -289,8 +294,8 @@ class MailboxDashBoardController extends ReloadableController { _getEmailCacheOnWebInteractor.execute().fold( (failure) {}, (success) { - if(success is GetComposerCacheSuccess){ - openComposerOverlay(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); + if (success is GetComposerCacheSuccess) { + goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } }, ); @@ -366,6 +371,8 @@ class MailboxDashBoardController extends ReloadableController { _handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!); } else if (success is GetRestoredDeletedMessageSuccess) { _handleGetRestoredDeletedMessageSuccess(success); + } else if (success is GetAllIdentitiesSuccess) { + _handleGetAllIdentitiesSuccess(success); } } @@ -396,7 +403,10 @@ class MailboxDashBoardController extends ReloadableController { @override void handleExceptionAction({Failure? failure, Exception? exception}) { super.handleExceptionAction(failure: failure, exception: exception); - if (failure is SendEmailFailure && exception is NoNetworkError) { + if (failure is SendEmailFailure && + exception is NoNetworkError && + PlatformInfo.isMobile + ) { log('MailboxDashBoardController::handleExceptionAction(): $failure'); _storeSendingEmailInCaseOfSendingFailureInMobile(failure); } @@ -513,7 +523,6 @@ class MailboxDashBoardController extends ReloadableController { final currentAccountId = session.personalAccount.accountId; sessionCurrent = session; accountId.value = currentAccountId; - userProfile.value = UserProfile(session.username.value); injectAutoCompleteBindings(session, currentAccountId); injectRuleFilterBindings(session, currentAccountId); @@ -522,6 +531,7 @@ class MailboxDashBoardController extends ReloadableController { _getVacationResponse(); spamReportController.getSpamReportStateAction(); + _getAllIdentities(); if (PlatformInfo.isMobile) { getAllSendingEmails(); @@ -542,12 +552,6 @@ class MailboxDashBoardController extends ReloadableController { _handleNotificationMessageFromEmailId(arguments.emailId); } - Future _getAppVersion() async { - final info = await PackageInfo.fromPlatform(); - log('MailboxDashBoardController::_getAppVersion(): ${info.version}'); - appInformation.value = info; - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); @@ -666,7 +670,7 @@ class MailboxDashBoardController extends ReloadableController { currentOverlayContext!, AppLocalizations.of(currentContext!).drafts_saved, actionName: AppLocalizations.of(currentContext!).discard, - onActionClick: () => _discardEmail(success.emailAsDrafts), + onActionClick: () => _discardEmail(success.emailId), leadingSVGIcon: imagePaths.icMailboxDrafts, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -709,11 +713,11 @@ class MailboxDashBoardController extends ReloadableController { } } - void _discardEmail(Email email) { + void _discardEmail(EmailId emailId) { final currentAccountId = accountId.value; final session = sessionCurrent; - if (currentAccountId != null && session != null && email.id != null) { - consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, email.id!)); + if (currentAccountId != null && session != null) { + consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, emailId)); } } @@ -1262,16 +1266,19 @@ class MailboxDashBoardController extends ReloadableController { composerOverlayState.value = ComposerOverlayState.active; } - void closeComposerOverlay({dynamic result}) { + void closeComposerOverlay({dynamic result}) async { composerArguments = null; ComposerBindings().dispose(); composerOverlayState.value = ComposerOverlayState.inActive; - if (result is SendingEmailArguments) { handleSendEmailAction(result); - } else if (result is SaveToDraftArguments) { - saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess || + result is SaveEmailAsDraftsSuccess || + result is UpdateEmailDraftsSuccess) { + consumeState(Stream.value(Right(result))); } + + await _removeComposerCacheOnWeb(); } void dispatchRoute(DashboardRoutes route) { @@ -1394,16 +1401,51 @@ class MailboxDashBoardController extends ReloadableController { } void goToComposer(ComposerArguments arguments) async { + final argumentsWithIdentity = arguments.withIdentity(identities: List.from(_identities ?? [])); + if (PlatformInfo.isWeb) { if (composerOverlayState.value == ComposerOverlayState.inActive) { - openComposerOverlay(arguments); + openComposerOverlay(argumentsWithIdentity); } } else { - final result = await push(AppRoutes.composer, arguments: arguments); + BackButtonInterceptor.removeByName(AppRoutes.dashboard); + + bool isTabletPlatform = currentContext != null + && !responsiveUtils.isScreenWithShortestSide(currentContext!); + dynamic result; + + if (isTabletPlatform) { + if (PlatformInfo.isIOS) { + dispatchEmailUIAction(HideEmailContentViewAction()); + } + + result = await Get.to( + () => const ComposerView(), + binding: ComposerBindings(), + opaque: false, + arguments: argumentsWithIdentity); + + if (PlatformInfo.isIOS) { + await Future.delayed( + const Duration(milliseconds: 200), + () => dispatchEmailUIAction(ShowEmailContentViewAction())); + } + } else { + result = await push( + AppRoutes.composer, + arguments: argumentsWithIdentity); + } + + BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); + if (result is SendingEmailArguments) { handleSendEmailAction(result); - } else if (result is SaveToDraftArguments) { - saveEmailToDraft(arguments: result); + } else if (result is SendEmailSuccess || + result is SaveEmailAsDraftsSuccess || + result is UpdateEmailDraftsSuccess) { + consumeState(Stream.value(Right(result))); + } else if (validateSendingEmailFailedWhenNetworkIsLostOnMobile(result)) { + _storeSendingEmailInCaseOfSendingFailureInMobile(result); } } } @@ -1443,16 +1485,17 @@ class MailboxDashBoardController extends ReloadableController { () => _replaceBrowserHistory(uri: result.value2) ); } + + _getAllIdentities(); } void selectQuickSearchFilter(QuickSearchFilter filter) { - return searchController.selectQuickSearchFilter(filter, userProfile.value!); + return searchController.selectQuickSearchFilter(filter); } void selectQuickSearchFilterFrom(EmailAddress fromEmailFilter) { return searchController.selectQuickSearchFilter( QuickSearchFilter.fromMe, - userProfile.value!, fromEmailFilter: fromEmailFilter ); } @@ -1462,11 +1505,10 @@ class MailboxDashBoardController extends ReloadableController { } Future> quickSearchEmails(String query) async { - if (sessionCurrent != null && accountId.value != null && userProfile.value != null) { + if (sessionCurrent != null && accountId.value != null) { return searchController.quickSearchEmails( session: sessionCurrent!, accountId: accountId.value!, - userProfile: userProfile.value!, query: query ); } else { @@ -1523,6 +1565,8 @@ class MailboxDashBoardController extends ReloadableController { () => _replaceBrowserHistory(uri: result.value2) ); } + + _getAllIdentities(); } void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { @@ -1814,7 +1858,9 @@ class MailboxDashBoardController extends ReloadableController { void _handleSendEmailFailure(SendEmailFailure failure) { logError('MailboxDashBoardController::_handleSendEmailFailure():failure: $failure'); - _storeSendingEmailInCaseOfSendingFailureInMobile(failure); + if (PlatformInfo.isMobile) { + _storeSendingEmailInCaseOfSendingFailureInMobile(failure); + } if (currentContext == null) { clearState(); return; @@ -1944,12 +1990,15 @@ class MailboxDashBoardController extends ReloadableController { } void _storeSendingEmailInCaseOfSendingFailureInMobile(SendEmailFailure failure) { - if (PlatformInfo.isMobile) { + if (failure.session != null && + failure.accountId != null && + failure.emailRequest != null + ) { _tryToStoreSendingEmail( - failure.session, - failure.accountId, - failure.emailRequest, - failure.mailboxRequest + failure.session!, + failure.accountId!, + failure.emailRequest!, + failure.mailboxRequest ); } } @@ -2148,35 +2197,16 @@ class MailboxDashBoardController extends ReloadableController { openMailboxAction(inboxPresentation); } - bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active; + bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active; - void enableDraggableApp() { - draggableAppState.value = DraggableAppState.active; - } + bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active; - void disableDraggableApp() { - draggableAppState.value = DraggableAppState.inActive; + void enableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.active; } - void saveEmailToDraft({required SaveToDraftArguments arguments}) { - if (arguments.oldEmailId != null) { - consumeState( - _updateEmailDraftsInteractor.execute( - arguments.session, - arguments.accountId, - arguments.newEmail, - arguments.oldEmailId! - ) - ); - } else { - consumeState( - _saveEmailAsDraftsInteractor.execute( - arguments.session, - arguments.accountId, - arguments.newEmail, - ) - ); - } + void disableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.inActive; } void _handleSendEmailSuccess(SendEmailSuccess success) { @@ -2485,7 +2515,36 @@ class MailboxDashBoardController extends ReloadableController { isRecoveringDeletedMessage.value = true; } - String get userEmail => userProfile.value?.email ?? ''; + String get userEmail => sessionCurrent?.username.value ?? ''; + + Future _removeComposerCacheOnWeb() async { + await _removeComposerCacheOnWebInteractor.execute(); + } + + bool validateSendingEmailFailedWhenNetworkIsLostOnMobile(dynamic failure) { + return failure is SendEmailFailure && + failure.exception is NoNetworkError && + PlatformInfo.isMobile; + } + + void _getAllIdentities() { + if (accountId.value != null && sessionCurrent != null) { + consumeState(_getAllIdentitiesInteractor.execute( + sessionCurrent!, + accountId.value! + )); + } + } + + void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) async { + final listIdentitiesMayDeleted = success.identities?.toListMayDeleted() ?? []; + if (listIdentitiesMayDeleted.isNotEmpty) { + _identities = listIdentitiesMayDeleted; + } + log('MailboxDashBoardController::_handleGetAllIdentitiesSuccess: IDENTITIES_SIZE = ${_identities?.length}'); + } + + List get listIdentities => _identities ?? []; @override void onClose() { @@ -2497,7 +2556,14 @@ class MailboxDashBoardController extends ReloadableController { _refreshActionEventController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); + applicationManager.releaseUserAgent(); BackButtonInterceptor.removeByName(AppRoutes.dashboard); + _identities = null; + composerArguments = null; + outboxMailbox = null; + sessionCurrent = null; + mapMailboxById = {}; + mapDefaultMailboxIdByRole = {}; super.onClose(); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index ea55dd620c..c725cda47d 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; @@ -17,7 +18,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_filter_condition_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; @@ -73,10 +73,9 @@ class SearchController extends BaseController with DateRangePickerMixin { void selectQuickSearchFilter( QuickSearchFilter quickSearchFilter, - UserProfile userProfile, {EmailAddress? fromEmailFilter} ) { - final isFilterSelected = quickSearchFilter.isSelected(searchEmailFilter.value, userProfile); + final isFilterSelected = quickSearchFilter.isSelected(searchEmailFilter.value); switch (quickSearchFilter) { case QuickSearchFilter.hasAttachment: @@ -110,7 +109,6 @@ class SearchController extends BaseController with DateRangePickerMixin { required Session session, required AccountId accountId, required String query, - required UserProfile userProfile, }) async { return await _quickSearchEmailInteractor.execute( session, @@ -119,7 +117,7 @@ class SearchController extends BaseController with DateRangePickerMixin { sort: {}..add( EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), - filter: _mappingToFilterOnSuggestionForm(userProfile: userProfile, query: query), + filter: _mappingToFilterOnSuggestionForm(userName: session.username, query: query), properties: ThreadConstants.propertiesQuickSearch ).then((result) => result.fold( (failure) => [], @@ -129,7 +127,7 @@ class SearchController extends BaseController with DateRangePickerMixin { )); } - Filter? _mappingToFilterOnSuggestionForm({required String query, required UserProfile userProfile}) { + Filter? _mappingToFilterOnSuggestionForm({required String query, required UserName userName}) { log('SearchController::_mappingToFilterOnSuggestionForm():query: $query'); final filterCondition = EmailFilterCondition( text: query.isNotEmpty == true ? query : null, @@ -143,7 +141,7 @@ class SearchController extends BaseController with DateRangePickerMixin { ? true : null, from: listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe) - ? userProfile.email + ? userName.value : null ); @@ -152,7 +150,7 @@ class SearchController extends BaseController with DateRangePickerMixin { : null; } - void applyFilterSuggestionToSearchFilter(UserProfile? userProfile) { + void applyFilterSuggestionToSearchFilter(UserName? userName) { final receiveTime = listFilterOnSuggestionForm.contains(QuickSearchFilter.last7Days) ? EmailReceiveTimeType.last7Days : EmailReceiveTimeType.allTime; @@ -160,11 +158,11 @@ class SearchController extends BaseController with DateRangePickerMixin { final hasAttachment = listFilterOnSuggestionForm.contains(QuickSearchFilter.hasAttachment) ? true : false; var listFromAddress = searchEmailFilter.value.from; - if (userProfile != null) { + if (userName != null) { if (listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe)) { - listFromAddress.add(userProfile.email); + listFromAddress.add(userName.value); } else { - listFromAddress.remove(userProfile.email); + listFromAddress.remove(userName.value); } } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 31c5a86a0c..622fa1d07c 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:model/extensions/username_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; @@ -75,21 +77,9 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { logoSVG: controller.imagePaths.icTMailLogo, onTapCallback: controller.redirectToInboxAction, ), - Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.w500), - )); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.applicationManager + ) ]) ), Expanded(child: Container( @@ -339,13 +329,16 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { : const SizedBox.shrink(), const SizedBox(width: 24), Obx(() => (AvatarBuilder() - ..text(controller.userProfile.value?.getAvatarText() ?? '') + ..text(controller.accountId.value != null + ? controller.sessionCurrent?.username.firstCharacter ?? '' + : '' + ) ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) ..addOnTapAvatarActionWithPositionClick((position) => controller.openPopupMenuAction(context, position, popupMenuUserSettingActionTile(context, - controller.userProfile.value, + controller.sessionCurrent?.username, onLogoutAction: () { popBack(); controller.logout(controller.sessionCurrent, controller.accountId.value); @@ -598,7 +591,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Obx(() { final isFilterSelected = filter.isSelected( controller.searchController.searchEmailFilter.value, - controller.userProfile.value ); return Padding( @@ -776,7 +768,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { String _getQuickSearchFilterFromTitle(BuildContext context) { final searchEmailFilterFromFiled = controller.searchController.searchEmailFilter.value.from; if (searchEmailFilterFromFiled.length == 1) { - if (searchEmailFilterFromFiled.first == controller.userProfile.value?.email) { + if (searchEmailFilterFromFiled.first == controller.sessionCurrent?.username.value && + controller.sessionCurrent?.username.value.isNotEmpty == true) { return QuickSearchFilter.fromMe.getTitle(context); } else { return '${AppLocalizations.of(context).from_email_address_prefix} ${searchEmailFilterFromFiled.first}'; diff --git a/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart b/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart index f0f25871c2..363859e194 100644 --- a/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart +++ b/lib/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart @@ -1,8 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/popup_menu/popup_menu_item_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; mixin UserSettingPopupMenuMixin { @@ -10,18 +12,33 @@ mixin UserSettingPopupMenuMixin { List popupMenuUserSettingActionTile( BuildContext context, - UserProfile? userProfile, + UserName? userName, { Function? onLogoutAction, Function? onSettingAction } ) { return [ - PopupMenuItem( - enabled: false, - padding: EdgeInsets.zero, - child: _userInformation(context, userProfile) - ), + if (userName != null) + PopupMenuItem( + enabled: false, + padding: EdgeInsets.zero, + child: SizedBox( + width: 300, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Text( + userName.value, + maxLines: 1, + style: const TextStyle( + fontSize: 15, + color: AppColor.colorHintSearchBar, + fontWeight: FontWeight.normal + ) + ) + ), + ) + ), if (onSettingAction != null) ...[ const PopupMenuDivider(height: 0.5), @@ -42,21 +59,6 @@ mixin UserSettingPopupMenuMixin { ]; } - Widget _userInformation(BuildContext context, UserProfile? userProfile) { - if (userProfile != null) { - return SizedBox( - width: 300, - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - title: Text(userProfile.email, maxLines: 1, style: const TextStyle( - fontSize: 15, - color: AppColor.colorHintSearchBar, - fontWeight: FontWeight.normal))), - ); - } - return const SizedBox.shrink(); - } - Widget _settingAction(BuildContext context, Function? onCallBack) { return PopupMenuItemWidget( _imagePaths.icSetting, diff --git a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart index 3b1e078cad..2dc8a9e9f9 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart @@ -2,7 +2,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/cupertino.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; @@ -89,7 +88,7 @@ enum QuickSearchFilter { bool isApplied(List listFilter) => listFilter.contains(this); - bool isSelected(SearchEmailFilter filter, UserProfile? userProfile) { + bool isSelected(SearchEmailFilter filter) { switch (this) { case QuickSearchFilter.hasAttachment: return filter.hasAttachment == true; diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart index 2df963c8a9..d8463069d6 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart @@ -125,7 +125,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { } if (query.isNotEmpty || _searchController.listFilterOnSuggestionForm.isNotEmpty) { - _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.sessionCurrent?.username); _dashBoardController.searchEmail(context, queryString: query); } else { _dashBoardController.clearSearchEmail(); @@ -146,7 +146,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { _searchController.searchFocus.unfocus(); _searchController.enableSearch(); - _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.sessionCurrent?.username); _dashBoardController.searchEmail(context, queryString: recent.value); } diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart index e7a49f215c..482d2b82e1 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart @@ -1,12 +1,10 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/views/list/tree_view.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; @@ -43,12 +41,7 @@ class MailboxVisibilityView extends GetWidget _buildLoadingView(), Expanded(child: Padding( padding: MailboxVisibilityUtils.getPaddingListView(context, controller.responsiveUtils), - child: PlatformInfo.isMobile - ? _buildListMailbox(context) - : ScrollbarListView( - scrollController: controller.mailboxListScrollController, - child: _buildListMailbox(context) - ) + child: _buildListMailbox(context) )) ] ), diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index 405b087498..1cc6b2404b 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -10,13 +10,11 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:model/model.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:server_settings/server_settings/capability_server_settings.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_user_profile_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; @@ -45,8 +43,6 @@ class ManageAccountDashBoardController extends ReloadableController { GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; - final appInformation = Rxn(); - final userProfile = Rxn(); final accountId = Rxn(); final accountMenuItemSelected = AccountMenuItem.profiles.obs; final settingsPageLevel = SettingsPageLevel.universal.obs; @@ -68,15 +64,12 @@ class ManageAccountDashBoardController extends ReloadableController { void onReady() { _initialPageLevel(); _getArguments(); - _getAppVersion(); super.onReady(); } @override void handleSuccessViewState(Success success) { - if (success is GetUserProfileSuccess) { - userProfile.value = success.userProfile; - } else if (success is GetAllVacationSuccess) { + if (success is GetAllVacationSuccess) { if (success.listVacationResponse.isNotEmpty) { vacationResponse.value = success.listVacationResponse.first; } @@ -92,7 +85,6 @@ class ManageAccountDashBoardController extends ReloadableController { log('ManageAccountDashBoardController::handleReloaded:'); sessionCurrent = session; accountId.value = session.personalAccount.accountId; - _getUserProfile(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); _getParametersRouter(); @@ -104,7 +96,6 @@ class ManageAccountDashBoardController extends ReloadableController { sessionCurrent = arguments.session; accountId.value = arguments.session?.personalAccount.accountId; previousUri = arguments.previousUri; - _getUserProfile(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); if (arguments.menuSettingCurrent != null) { @@ -157,17 +148,6 @@ class ManageAccountDashBoardController extends ReloadableController { } } - Future _getAppVersion() async { - final info = await PackageInfo.fromPlatform(); - log('ManageAccountDashBoardController::_getAppVersion(): ${info.version}'); - appInformation.value = info; - } - - void _getUserProfile() async { - log('ManageAccountDashBoardController::_getUserProfile(): $sessionCurrent'); - userProfile.value = sessionCurrent != null ? UserProfile(sessionCurrent!.username.value) : null; - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index 1cd383a8a3..8d3a0096db 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -7,7 +7,9 @@ import 'package:core/presentation/views/text/slogan_builder.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; +import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/always_read_receipt/always_read_receipt_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; @@ -54,18 +56,9 @@ class ManageAccountDashBoardView extends GetWidget controller.backToMailboxDashBoard(context: context), ), - Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v.${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - )); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.applicationManager + ) ]) ), Expanded(child: Padding( @@ -139,7 +132,10 @@ class ManageAccountDashBoardView extends GetWidget (AvatarBuilder() - ..text(controller.userProfile.value?.getAvatarText() ?? '') + ..text(controller.accountId.value != null + ? controller.sessionCurrent?.username.firstCharacter ?? '' + : '' + ) ..backgroundColor(Colors.white) ..textColor(Colors.black) ..context(context) @@ -149,7 +145,7 @@ class ManageAccountDashBoardView extends GetWidget { textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), logoSVG: controller.imagePaths.icTMailLogo ), - Obx(() { - if (controller.dashBoardController.appInformation.value != null) { - return Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - 'v.${controller.dashBoardController.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - ), - ); - } else { - return const SizedBox.shrink(); - } - }), + ApplicationVersionWidget( + applicationManager: controller.dashBoardController.applicationManager, + padding: const EdgeInsets.only(top: 4), + ) ]) ), if (!controller.responsiveUtils.isWebDesktop(context)) diff --git a/lib/features/manage_account/presentation/menu/settings/settings_controller.dart b/lib/features/manage_account/presentation/menu/settings/settings_controller.dart index 7374417010..d08766d9f4 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_controller.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_controller.dart @@ -1,6 +1,5 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; @@ -9,15 +8,8 @@ class SettingsController extends GetxController { final manageAccountDashboardController = Get.find(); final responsiveUtils = Get.find(); final imagePaths = Get.find(); - final settingScrollController = ScrollController(); void selectSettings(AccountMenuItem accountMenuItem) => manageAccountDashboardController.selectSettings(accountMenuItem); void backToUniversalSettings() => manageAccountDashboardController.backToUniversalSettings(); - - @override - void onClose() { - settingScrollController.dispose(); - super.onClose(); - } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index c943aa0b23..fb145a0208 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -1,8 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; @@ -15,11 +13,12 @@ class SettingsFirstLevelView extends GetWidget { @override Widget build(BuildContext context) { - final child = SingleChildScrollView( - controller: PlatformInfo.isMobile ? null : controller.settingScrollController, + return SingleChildScrollView( child: Column(children: [ Obx(() => UserInformationWidget( - userProfile: controller.manageAccountDashboardController.userProfile.value, + userName: controller.manageAccountDashboardController.accountId.value != null + ? controller.manageAccountDashboardController.sessionCurrent?.username + : null, padding: SettingsUtils.getPaddingInFirstLevel(context, controller.responsiveUtils), titlePadding: const EdgeInsetsDirectional.only(start: 16))), Divider( @@ -154,17 +153,5 @@ class SettingsFirstLevelView extends GetWidget { ), ]), ); - - if (PlatformInfo.isMobile) { - return child; - } else { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ScrollbarListView( - scrollController: controller.settingScrollController, - child: child - ), - ); - } } } diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart index 34beda441b..e7abe242d2 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart @@ -142,10 +142,9 @@ class IdentitiesController extends BaseController { void goToCreateNewIdentity(BuildContext context) async { final accountId = _accountDashBoardController.accountId.value; - final userProfile = _accountDashBoardController.userProfile.value; final session = _accountDashBoardController.sessionCurrent; - if (accountId != null && session != null && userProfile != null) { - final arguments = IdentityCreatorArguments(accountId, session, userProfile); + if (accountId != null && session != null) { + final arguments = IdentityCreatorArguments(accountId, session); final newIdentityArguments = PlatformInfo.isWeb ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.identityCreator, arguments: arguments) @@ -249,13 +248,11 @@ class IdentitiesController extends BaseController { void goToEditIdentity(BuildContext context, Identity identity) async { final accountId = _accountDashBoardController.accountId.value; - final userProfile = _accountDashBoardController.userProfile.value; final session = _accountDashBoardController.sessionCurrent; - if (accountId != null && session != null && userProfile != null) { + if (accountId != null && session != null) { final arguments = IdentityCreatorArguments( accountId, session, - userProfile, identity: identity, actionType: IdentityActionType.edit); diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart index a6e6e9fe34..a6e307f763 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart @@ -26,9 +26,8 @@ class DeleteIdentityDialogBuilder extends StatelessWidget { responsiveUtils: responsiveUtils, mobile: (_buildDeleteDialog(context) ..alignment(Alignment.bottomCenter) - ..outsideDialogPadding(const EdgeInsets.only(left: 0, right: 0, bottom: PlatformInfo.isWeb ? 42 : 16)) - ..widthDialog(MediaQuery.of(context).size.width - 16) - ..heightDialog(280)) + ..outsideDialogPadding(const EdgeInsets.only(bottom: PlatformInfo.isWeb ? 42 : 16)) + ..widthDialog(MediaQuery.of(context).size.width - 16)) .build(), landscapeMobile: _buildDeleteDialog(context).build(), tablet: _buildDeleteDialog(context).build(), diff --git a/lib/features/network_connection/presentation/web_network_connection_controller.dart b/lib/features/network_connection/presentation/web_network_connection_controller.dart index 4afdb5be73..c4bbde67e1 100644 --- a/lib/features/network_connection/presentation/web_network_connection_controller.dart +++ b/lib/features/network_connection/presentation/web_network_connection_controller.dart @@ -94,4 +94,6 @@ class NetworkConnectionController extends GetxController { ); } } + + Future hasInternetConnection() async => isNetworkConnectionAvailable(); } \ No newline at end of file diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart index 807536be99..1829ad0248 100644 --- a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -68,7 +68,7 @@ class SendEmailInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart index e9d2e7e570..3c0418b751 100644 --- a/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart +++ b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; -import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; @@ -25,7 +24,6 @@ extension SendingEmailHiveCacheExtension on SendingEmailHiveCache { emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(emailIdAnsweredOrForwarded), identityId: const IdentityIdNullableConverter().fromJson(identityId), mailboxNameRequest: const MailboxNameConverter().fromJson(mailboxNameRequest), - creationIdRequest: const IdNullableConverter().fromJson(creationIdRequest), sendingState: SendingState.values.firstWhere((value) => value.name == sendingState), previousEmailId: const EmailIdNullableConverter().fromJson(previousEmailId), ); diff --git a/lib/features/offline_mode/model/sending_email_hive_cache.dart b/lib/features/offline_mode/model/sending_email_hive_cache.dart index ee8cae8b66..34cdce08d6 100644 --- a/lib/features/offline_mode/model/sending_email_hive_cache.dart +++ b/lib/features/offline_mode/model/sending_email_hive_cache.dart @@ -36,12 +36,9 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { final String? mailboxNameRequest; @HiveField(9) - final String? creationIdRequest; - - @HiveField(10) final String sendingState; - @HiveField(11) + @HiveField(10) final String? previousEmailId; SendingEmailHiveCache( @@ -54,7 +51,6 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { this.emailIdAnsweredOrForwarded, this.identityId, this.mailboxNameRequest, - this.creationIdRequest, this.sendingState, this.previousEmailId, ); @@ -70,7 +66,6 @@ class SendingEmailHiveCache extends HiveObject with EquatableMixin { emailIdAnsweredOrForwarded, identityId, mailboxNameRequest, - creationIdRequest, sendingState, previousEmailId, ]; diff --git a/lib/features/offline_mode/work_manager/sending_email_worker.dart b/lib/features/offline_mode/work_manager/sending_email_worker.dart index 530dfb57f2..cc8064914c 100644 --- a/lib/features/offline_mode/work_manager/sending_email_worker.dart +++ b/lib/features/offline_mode/work_manager/sending_email_worker.dart @@ -193,10 +193,8 @@ class SendingEmailWorker extends Worker { } CreateNewMailboxRequest? _getMailboxRequest() { - if (_sendingEmail.mailboxNameRequest != null && - _sendingEmail.creationIdRequest != null) { + if (_sendingEmail.mailboxNameRequest != null) { return CreateNewMailboxRequest( - _sendingEmail.creationIdRequest!, _sendingEmail.mailboxNameRequest!); } else { return null; diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index 45f6c34907..9b31a99069 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -105,7 +105,7 @@ class FcmInteractorBindings extends InteractorsBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find())); + Get.find())); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 03f58018ec..c0465e517c 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -24,7 +24,6 @@ import 'package:model/extensions/presentation_email_extension.dart'; import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; -import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; @@ -112,8 +111,6 @@ class SearchEmailController extends BaseController Session? get session => mailboxDashBoardController.sessionCurrent; - UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; - SearchQuery? get searchQuery => simpleSearchFilter.value.text; RxList get listResultSearch => mailboxDashBoardController.listResultSearch; diff --git a/lib/features/search/email/presentation/search_email_view.dart b/lib/features/search/email/presentation/search_email_view.dart index c311767f71..2f2d5600bb 100644 --- a/lib/features/search/email/presentation/search_email_view.dart +++ b/lib/features/search/email/presentation/search_email_view.dart @@ -10,7 +10,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -497,7 +496,7 @@ class SearchEmailView extends GetWidget searchQuery: controller.searchQuery, isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, isSearchEmailRunning: true, - padding: SearchEmailUtils.getPaddingItemListMobile(context, controller.responsiveUtils), + padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), mailboxContain: currentPresentationEmail.mailboxContain, emailActionClick: (action, email) { controller.pressEmailAction( @@ -533,61 +532,58 @@ class SearchEmailView extends GetWidget } }, ) - : ScrollbarListView( - scrollController: controller.resultSearchScrollController, - child: ListView.separated( - controller: controller.resultSearchScrollController, - physics: const AlwaysScrollableScrollPhysics(), - key: const PageStorageKey('list_presentation_email_in_search_view'), - itemCount: listPresentationEmail.length, - itemBuilder: (context, index) { - final currentPresentationEmail = listPresentationEmail[index]; - return Obx(() => EmailTileBuilder( - presentationEmail: currentPresentationEmail, - selectAllMode: controller.selectionMode.value, - searchQuery: controller.searchQuery, - isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, - isSearchEmailRunning: true, - padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - mailboxContain: currentPresentationEmail.mailboxContain, - emailActionClick: (action, email) { - controller.pressEmailAction( - context, - action, - email, - mailboxContain: currentPresentationEmail.mailboxContain - ); - }, - onMoreActionClick: (email, position) { - if (controller.responsiveUtils.isScreenWithShortestSide(context)) { - controller.openContextMenuAction( - context, - _contextMenuActionTile(context, email) - ); - } else { - controller.openPopupMenuAction( - context, - position, - _popupMenuActionTile(context, email) - ); - } - }, - - )); - }, - separatorBuilder: (context, index) { - return Padding( - padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), - child: Divider( - color: index < listPresentationEmail.length - 1 && - controller.selectionMode.value == SelectMode.INACTIVE - ? null - : Colors.white - ) + : ListView.separated( + controller: controller.resultSearchScrollController, + physics: const AlwaysScrollableScrollPhysics(), + key: const PageStorageKey('list_presentation_email_in_search_view'), + itemCount: listPresentationEmail.length, + itemBuilder: (context, index) { + final currentPresentationEmail = listPresentationEmail[index]; + return Obx(() => EmailTileBuilder( + presentationEmail: currentPresentationEmail, + selectAllMode: controller.selectionMode.value, + searchQuery: controller.searchQuery, + isShowingEmailContent: controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, + isSearchEmailRunning: true, + padding: SearchEmailUtils.getPaddingSearchResultList(context, controller.responsiveUtils), + mailboxContain: currentPresentationEmail.mailboxContain, + emailActionClick: (action, email) { + controller.pressEmailAction( + context, + action, + email, + mailboxContain: currentPresentationEmail.mailboxContain ); }, - ) - ) + onMoreActionClick: (email, position) { + if (controller.responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction( + context, + _contextMenuActionTile(context, email) + ); + } else { + controller.openPopupMenuAction( + context, + position, + _popupMenuActionTile(context, email) + ); + } + }, + + )); + }, + separatorBuilder: (context, index) { + return Padding( + padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), + child: Divider( + color: index < listPresentationEmail.length - 1 && + controller.selectionMode.value == SelectMode.INACTIVE + ? null + : Colors.white + ) + ); + }, + ) ); } diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 275808a626..83a0fe9d88 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -11,7 +11,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -82,7 +81,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final currentSearchQuery = RxString(''); final listMailboxSearched = RxList(); final textInputSearchController = TextEditingController(); - final scrollbarController = ScrollController(); late Debouncer _deBouncerTime; PresentationMailbox? get selectedMailbox => dashboardController.selectedMailbox.value; @@ -676,9 +674,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - final generateCreateId = Id(uuid.v1()); _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - generateCreateId, result.newName, parentId: result.mailboxLocation?.id)); } @@ -733,7 +729,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void onClose() { textInputSearchController.dispose(); _deBouncerTime.cancel(); - scrollbarController.dispose(); super.onClose(); } } \ No newline at end of file diff --git a/lib/features/search/mailbox/presentation/search_mailbox_view.dart b/lib/features/search/mailbox/presentation/search_mailbox_view.dart index ff83bc6b66..beeb620712 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_view.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_view.dart @@ -11,7 +11,6 @@ import 'package:get/get.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/search_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/context_item_mailbox_action.dart'; @@ -58,12 +57,7 @@ class SearchMailboxView extends GetWidget const Divider(color: AppColor.colorDividerComposer, height: 1), _buildLoadingView(), Expanded( - child: PlatformInfo.isMobile - ? _buildMailboxListView(context) - : ScrollbarListView( - scrollController: controller.scrollbarController, - child: _buildMailboxListView(context) - ) + child: _buildMailboxListView(context) ) ]), ); @@ -179,7 +173,6 @@ class SearchMailboxView extends GetWidget padding: SearchMailboxUtils.getPaddingListViewMailboxSearched(context, controller.responsiveUtils), key: const Key('list_mailbox_searched'), itemCount: controller.listMailboxSearched.length, - controller: controller.scrollbarController, shrinkWrap: true, primary: false, itemBuilder: (context, index) { diff --git a/lib/features/sending_queue/domain/extensions/sending_email_extension.dart b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart index 6b01c6dbf6..f907fe2cf8 100644 --- a/lib/features/sending_queue/domain/extensions/sending_email_extension.dart +++ b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart @@ -21,7 +21,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded?.asString, identityId?.asString, mailboxNameRequest?.name, - creationIdRequest?.value, sendingState.name, previousEmailId?.asString, ); @@ -51,7 +50,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: sendingState, selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, previousEmailId: previousEmailId, @@ -69,7 +67,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: sendingState, selectMode: SelectMode.INACTIVE, previousEmailId: previousEmailId, @@ -87,7 +84,6 @@ extension SendingEmailExtension on SendingEmail { emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, identityId: identityId, mailboxNameRequest: mailboxNameRequest, - creationIdRequest: creationIdRequest, sendingState: newState, selectMode: selectMode, previousEmailId: previousEmailId, diff --git a/lib/features/sending_queue/domain/model/sending_email.dart b/lib/features/sending_queue/domain/model/sending_email.dart index 13f96059da..1a2af61162 100644 --- a/lib/features/sending_queue/domain/model/sending_email.dart +++ b/lib/features/sending_queue/domain/model/sending_email.dart @@ -5,11 +5,9 @@ import 'package:core/utils/platform_info.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_date_range_picker/flutter_date_range_picker.dart'; import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; -import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -28,7 +26,6 @@ class SendingEmail with EquatableMixin { final IdentityId? identityId; final EmailActionType emailActionType; final MailboxName? mailboxNameRequest; - final Id? creationIdRequest; final DateTime createTime; final SelectMode selectMode; final SendingState sendingState; @@ -44,7 +41,6 @@ class SendingEmail with EquatableMixin { this.emailIdAnsweredOrForwarded, this.identityId, this.mailboxNameRequest, - this.creationIdRequest, this.selectMode = SelectMode.INACTIVE, this.sendingState = SendingState.waiting, this.previousEmailId, @@ -68,7 +64,6 @@ class SendingEmail with EquatableMixin { writeNotNull('emailIdAnsweredOrForwarded', const EmailIdNullableConverter().toJson(emailIdAnsweredOrForwarded)); writeNotNull('identityId', const IdentityIdNullableConverter().toJson(identityId)); writeNotNull('mailboxNameRequest', mailboxNameRequest?.name); - writeNotNull('creationIdRequest', const IdNullableConverter().toJson(creationIdRequest)); writeNotNull('previousEmailId', const EmailIdNullableConverter().toJson(previousEmailId)); return val; @@ -91,7 +86,6 @@ class SendingEmail with EquatableMixin { emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(json['emailIdAnsweredOrForwarded'] as String?), identityId: const IdentityIdNullableConverter().fromJson(json['identityId'] as String?), mailboxNameRequest: const MailboxNameConverter().fromJson(json['mailboxNameRequest'] as String?), - creationIdRequest: const IdNullableConverter().fromJson(json['creationIdRequest'] as String?), previousEmailId: const EmailIdNullableConverter().fromJson(json['previousEmailId'] as String?), ); } @@ -134,7 +128,6 @@ class SendingEmail with EquatableMixin { emailIdAnsweredOrForwarded, identityId, mailboxNameRequest, - creationIdRequest, selectMode, sendingState, previousEmailId, diff --git a/lib/features/sending_queue/presentation/sending_queue_controller.dart b/lib/features/sending_queue/presentation/sending_queue_controller.dart index 9ed699ed10..9606ce1319 100644 --- a/lib/features/sending_queue/presentation/sending_queue_controller.dart +++ b/lib/features/sending_queue/presentation/sending_queue_controller.dart @@ -283,11 +283,8 @@ class SendingQueueController extends BaseController with MessageDialogActionMixi } CreateNewMailboxRequest? _getMailboxRequest(SendingEmail sendingEmail) { - if (sendingEmail.mailboxNameRequest != null && - sendingEmail.creationIdRequest != null - ) { + if (sendingEmail.mailboxNameRequest != null) { return CreateNewMailboxRequest( - sendingEmail.creationIdRequest!, sendingEmail.mailboxNameRequest! ); } else { diff --git a/lib/features/thread/presentation/mixin/base_email_item_tile.dart b/lib/features/thread/presentation/mixin/base_email_item_tile.dart index cf37c63d30..493bac458d 100644 --- a/lib/features/thread/presentation/mixin/base_email_item_tile.dart +++ b/lib/features/thread/presentation/mixin/base_email_item_tile.dart @@ -1,12 +1,10 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/text/rich_text_builder.dart'; import 'package:core/presentation/views/text/text_overflow_builder.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -88,20 +86,20 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? informationSender(email, mailbox) - : informationSender(email, mailbox).overflow, - query?.value ?? '', - TextStyle( - fontSize: 15, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)), - TextStyle( - fontSize: 15, - color: buildTextColorForReadEmail(email), - backgroundColor: AppColor.bgWordSearch, - fontWeight: buildFontForReadEmail(email)) - ).build(); + textOrigin: informationSender(email, mailbox), + wordToStyle: query?.value ?? '', + styleOrigin: TextStyle( + fontSize: 15, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ), + styleWord: TextStyle( + fontSize: 15, + color: buildTextColorForReadEmail(email), + backgroundColor: AppColor.bgWordSearch, + fontWeight: buildFontForReadEmail(email) + ) + ); } else { return TextOverflowBuilder( informationSender(email, mailbox), @@ -122,20 +120,20 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? email.getEmailTitle() - : email.getEmailTitle().overflow, - query?.value ?? '', - TextStyle( - fontSize: 13, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)), - TextStyle( - fontSize: 13, - backgroundColor: AppColor.bgWordSearch, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)) - ).build(); + textOrigin: email.getEmailTitle(), + wordToStyle: query?.value ?? '', + styleOrigin: TextStyle( + fontSize: 13, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ), + styleWord: TextStyle( + fontSize: 13, + backgroundColor: AppColor.bgWordSearch, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email) + ) + ); } else { return TextOverflowBuilder( email.getEmailTitle(), @@ -155,19 +153,19 @@ mixin BaseEmailItemTile { ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - DirectionUtils.isDirectionRTLByLanguage(context) - ? email.getPartialContent() - : email.getPartialContent().overflow, - query?.value ?? '', - const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.normal), - const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - backgroundColor: AppColor.bgWordSearch) - ).build(); + textOrigin: email.getPartialContent(), + wordToStyle: query?.value ?? '', + styleOrigin: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.normal + ), + styleWord: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + backgroundColor: AppColor.bgWordSearch + ) + ); } else { return TextOverflowBuilder( email.getPartialContent(), diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 32edf5aefd..a5861163e3 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -415,10 +415,10 @@ class ThreadController extends BaseController with EmailActionController { } _getAllEmailAction(); } else if (error is MethodLevelErrors) { - if (currentOverlayContext != null && error.message?.isNotEmpty == true) { + if (currentOverlayContext != null && error.message != null) { appToast.showToastErrorMessage( currentOverlayContext!, - error.message! + error.message?.toString() ?? '' ); } clearState(); diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 0444ce05ea..3d03d3453e 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -8,7 +8,6 @@ import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'; -import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; @@ -381,38 +380,35 @@ class ThreadView extends GetWidget focusNode: controller.focusNodeKeyBoard, autofocus: true, onKey: controller.handleKeyEvent, - child: ScrollbarListView( - scrollController: controller.listEmailController, - child: ListView.separated( - key: const PageStorageKey('list_presentation_email_in_threads'), - controller: controller.listEmailController, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length + 2, - itemBuilder: (context, index) => Obx(() { - if (index == listPresentationEmail.length) { - return _buildLoadMoreButton( - context, - controller.loadingMoreStatus.value); - } - if (index == listPresentationEmail.length + 1) { - return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); - } - return _buildEmailItem( + child: ListView.separated( + key: const PageStorageKey('list_presentation_email_in_threads'), + controller: controller.listEmailController, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: listPresentationEmail.length + 2, + itemBuilder: (context, index) => Obx(() { + if (index == listPresentationEmail.length) { + return _buildLoadMoreButton( context, - listPresentationEmail[index]); - }), - separatorBuilder: (context, index) { - return Padding( - padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), - child: Divider( - color: index < listPresentationEmail.length - 1 && - controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE - ? null - : Colors.white, - ) - ); - }, - ), + controller.loadingMoreStatus.value); + } + if (index == listPresentationEmail.length + 1) { + return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); + } + return _buildEmailItem( + context, + listPresentationEmail[index]); + }), + separatorBuilder: (context, index) { + return Padding( + padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), + child: Divider( + color: index < listPresentationEmail.length - 1 && + controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE + ? null + : Colors.white, + ) + ); + }, ), ) ); diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index 46fc1f8579..c6715357ff 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -27,10 +27,7 @@ import 'package:worker_manager/worker_manager.dart' as worker; class FileUploader { static const String uploadAttachmentExtraKey = 'upload-attachment'; - static const String platformExtraKey = 'platform'; - static const String bytesExtraKey = 'bytes'; - static const String typeExtraKey = 'type'; - static const String sizeExtraKey = 'size'; + static const String streamDataExtraKey = 'streamData'; static const String filePathExtraKey = 'path'; final DioClient _dioClient; @@ -94,10 +91,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'mobile', filePathExtraKey: argsUpload.mobileFileUpload.filePath, - typeExtraKey: argsUpload.mobileFileUpload.mimeType, - sizeExtraKey: argsUpload.mobileFileUpload.fileSize, } }; @@ -109,7 +103,7 @@ class FileUploader { ), data: File(argsUpload.mobileFileUpload.filePath).openRead(), onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); sendPort.send( UploadingAttachmentUploadState( argsUpload.uploadId, @@ -139,10 +133,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'web', - bytesExtraKey: fileInfo.bytes, - typeExtraKey: fileInfo.mimeType, - sizeExtraKey: fileInfo.fileSize, + streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!), } }; @@ -155,7 +146,7 @@ class FileUploader { data: BodyBytesStream.fromBytes(fileInfo.bytes!), cancelToken: cancelToken, onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); onSendController.add( Right(UploadingAttachmentUploadState( uploadId, diff --git a/lib/features/upload/domain/exceptions/pick_file_exception.dart b/lib/features/upload/domain/exceptions/pick_file_exception.dart new file mode 100644 index 0000000000..0620cf3a1e --- /dev/null +++ b/lib/features/upload/domain/exceptions/pick_file_exception.dart @@ -0,0 +1 @@ +class PickFileCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/file_info_extension.dart b/lib/features/upload/domain/extensions/file_info_extension.dart index 58866921c4..ce079bacdf 100644 --- a/lib/features/upload/domain/extensions/file_info_extension.dart +++ b/lib/features/upload/domain/extensions/file_info_extension.dart @@ -3,5 +3,15 @@ import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; extension FileInfoExtension on FileInfo { - MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType); + MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType); + + FileInfo withInline() => FileInfo( + fileName: fileName, + fileSize: fileSize, + filePath: filePath, + bytes: bytes, + readStream: readStream, + type: type, + isInline: true, + isShared: isShared); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/list_file_info_extension.dart b/lib/features/upload/domain/extensions/list_file_info_extension.dart new file mode 100644 index 0000000000..3934f631d6 --- /dev/null +++ b/lib/features/upload/domain/extensions/list_file_info_extension.dart @@ -0,0 +1,7 @@ +import 'package:model/upload/file_info.dart'; + +extension ListFileInfoExtension on List { + List get listSize => map((file) => file.fileSize).toList(); + + num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size); +} \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/media_type_extension.dart b/lib/features/upload/domain/extensions/media_type_extension.dart new file mode 100644 index 0000000000..e3bfcc8d80 --- /dev/null +++ b/lib/features/upload/domain/extensions/media_type_extension.dart @@ -0,0 +1,28 @@ +import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:http_parser/http_parser.dart'; + +extension MediaTypeExtension on MediaType { + String getIcon(ImagePaths imagePaths, {String? fileName}) { + if (validatePDFIcon(fileName: fileName) == true) { + return imagePaths.icFilePdf; + } else if (isDocFile()) { + return imagePaths.icFileDocx; + } else if (isExcelFile()) { + return imagePaths.icFileXlsx; + } else if (isPowerPointFile()) { + return imagePaths.icFilePptx; + } else if (isPdfFile()) { + return imagePaths.icFilePdf; + } else if (isZipFile()) { + return imagePaths.icFileZip; + } else if (isImageFile()) { + return imagePaths.icFilePng; + } else { + return imagePaths.icFileEPup; + } + } + + bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' || + (mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true); +} diff --git a/lib/features/upload/domain/extensions/platform_file_extension.dart b/lib/features/upload/domain/extensions/platform_file_extension.dart new file mode 100644 index 0000000000..f24da3baa3 --- /dev/null +++ b/lib/features/upload/domain/extensions/platform_file_extension.dart @@ -0,0 +1,13 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:model/upload/file_info.dart'; + +extension PlatformFileExtension on PlatformFile { + FileInfo toFileInfo() => FileInfo( + fileName: name, + fileSize: size, + filePath: PlatformInfo.isWeb ? '' : path ?? '', + bytes: bytes, + readStream: readStream + ); +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/attachment_upload_state.dart b/lib/features/upload/domain/state/attachment_upload_state.dart index b5f0ee2f57..5b5b20cdfe 100644 --- a/lib/features/upload/domain/state/attachment_upload_state.dart +++ b/lib/features/upload/domain/state/attachment_upload_state.dart @@ -31,15 +31,11 @@ class SuccessAttachmentUploadState extends Success { final UploadTaskId uploadId; final Attachment attachment; final FileInfo fileInfo; - final bool fromFileShared; SuccessAttachmentUploadState( this.uploadId, this.attachment, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -47,7 +43,6 @@ class SuccessAttachmentUploadState extends Success { uploadId, attachment, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/upload/domain/state/local_file_picker_state.dart b/lib/features/upload/domain/state/local_file_picker_state.dart index a2d948fe58..37745e9897 100644 --- a/lib/features/upload/domain/state/local_file_picker_state.dart +++ b/lib/features/upload/domain/state/local_file_picker_state.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/upload/file_info.dart'; +class LocalFilePickerLoading extends LoadingState {} + class LocalFilePickerSuccess extends UIState { final List pickedFiles; @@ -14,6 +16,4 @@ class LocalFilePickerSuccess extends UIState { class LocalFilePickerFailure extends FeatureFailure { LocalFilePickerFailure(dynamic exception) : super(exception: exception); -} - -class LocalFilePickerCancel extends FeatureFailure {} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/local_image_picker_state.dart b/lib/features/upload/domain/state/local_image_picker_state.dart new file mode 100644 index 0000000000..d48ac02453 --- /dev/null +++ b/lib/features/upload/domain/state/local_image_picker_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; + +class LocalImagePickerLoading extends LoadingState {} + +class LocalImagePickerSuccess extends UIState { + final FileInfo fileInfo; + + LocalImagePickerSuccess(this.fileInfo); + + @override + List get props => [fileInfo]; +} + +class LocalImagePickerFailure extends FeatureFailure { + + LocalImagePickerFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart index c617892676..dd7cf46fab 100644 --- a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart +++ b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart @@ -4,7 +4,8 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; class LocalFilePickerInteractor { @@ -13,23 +14,22 @@ class LocalFilePickerInteractor { Stream> execute({FileType fileType = FileType.any}) async* { try { + yield Right(LocalFilePickerLoading()); + final filesResult = await FilePicker.platform.pickFiles( type: fileType, allowMultiple: true, - withData: PlatformInfo.isWeb + withData: PlatformInfo.isWeb, + withReadStream: PlatformInfo.isMobile ); - if (filesResult != null && filesResult.files.isNotEmpty) { - final fileInfoResults = filesResult.files - .map((platformFile) => FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null - )) - .toList(); - yield Right(LocalFilePickerSuccess(fileInfoResults)); + + if (filesResult?.files.isNotEmpty == true) { + final listFileInfo = filesResult!.files + .map((platformFile) => platformFile.toFileInfo()) + .toList(); + yield Right(LocalFilePickerSuccess(listFileInfo)); } else { - yield Left(LocalFilePickerCancel()); + yield Left(LocalFilePickerFailure(PickFileCanceledException())); } } catch (exception) { yield Left(LocalFilePickerFailure(exception)); diff --git a/lib/features/upload/domain/usecases/local_image_picker_interactor.dart b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart new file mode 100644 index 0000000000..ca91a31be7 --- /dev/null +++ b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart @@ -0,0 +1,35 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; + +class LocalImagePickerInteractor { + + LocalImagePickerInteractor(); + + Stream> execute() async* { + try { + yield Right(LocalImagePickerLoading()); + + final filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + withReadStream: PlatformInfo.isMobile, + withData: PlatformInfo.isWeb + ); + + if (filePickerResult?.files.isNotEmpty == true) { + final fileInfo = filePickerResult!.files.first.toFileInfo(); + yield Right(LocalImagePickerSuccess(fileInfo)); + } else { + yield Left(LocalImagePickerFailure(PickFileCanceledException())); + } + } catch (exception) { + yield Left(LocalImagePickerFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 501643474b..626d7ced07 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -1,23 +1,26 @@ import 'package:async/async.dart'; import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:filesize/filesize.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_body_part.dart'; import 'package:model/email/attachment.dart'; import 'package:model/extensions/attachment_extension.dart'; -import 'package:model/extensions/list_attachment_extension.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/presentation/extensions/upload_attachment_extension.dart'; @@ -26,6 +29,7 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class UploadController extends BaseController { @@ -76,7 +80,7 @@ class UploadController extends BaseController { failure.uploadId, (currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed)); deleteFileUploaded(failure.uploadId); - _handleUploadAttachmentsFailure(failure); + _showToastMessageWhenUploadAttachmentsFailure(failure); } }, (success) { @@ -111,7 +115,7 @@ class UploadController extends BaseController { ); _refreshListUploadAttachmentState(); - _handleUploadAttachmentsSuccess(success); + _showToastMessageWhenUploadAttachmentsSuccess(success); } } ); @@ -158,7 +162,7 @@ class UploadController extends BaseController { ); final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId); - log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}'); + log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.file?.isShared}'); _uploadingStateInlineFiles.updateElementByUploadTaskId( success.uploadId, @@ -175,7 +179,6 @@ class UploadController extends BaseController { success.uploadId, inlineAttachment, success.fileInfo, - fromFileShared: uploadFileState?.fromFileShared ?? false ); _handleUploadInlineAttachmentsSuccess(newUploadSuccess); } @@ -199,27 +202,24 @@ class UploadController extends BaseController { _refreshListUploadAttachmentState(); } - Future justUploadAttachmentsAction(List uploadFiles, Uri uploadUri) { + Future justUploadAttachmentsAction({ + required List uploadFiles, + required Uri uploadUri, + }) { return Future.forEach(uploadFiles, (uploadFile) async { - await uploadFileAction(uploadFile, uploadUri); + await uploadFileAction(uploadFile: uploadFile, uploadUri: uploadUri); }); } - Future uploadFileAction( - FileInfo uploadFile, - Uri uploadUri, - { - bool isInline = false, - bool fromFileShared = false, - } - ) { - log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared'); + Future uploadFileAction({ + required FileInfo uploadFile, + required Uri uploadUri, + }) { + log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | mimeType: ${uploadFile.mimeType} | isInline: ${uploadFile.isInline} | fromFileShared: ${uploadFile.isShared}'); consumeState(_uploadAttachmentInteractor.execute( uploadFile, uploadUri, cancelToken: CancelToken(), - isInline: isInline, - fromFileShared: fromFileShared )); return Future.value(); } @@ -240,17 +240,26 @@ class UploadController extends BaseController { .toList(); } + List get attachmentsPicked { + if (listUploadAttachments.isEmpty) { + return List.empty(); + } + return listUploadAttachments + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Set? generateAttachments() { if (attachmentsUploaded.isEmpty) { return null; } return attachmentsUploaded - .map((attachment) => attachment.toEmailBodyPart( - disposition: ContentDisposition.attachment.value)) + .map((attachment) => attachment.toEmailBodyPart()) .toSet(); } - void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { + void _showToastMessageWhenUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { if (currentContext != null && currentOverlayContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -260,7 +269,7 @@ class UploadController extends BaseController { } } - void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { + void _showToastMessageWhenUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -270,31 +279,118 @@ class UploadController extends BaseController { } } - bool hasEnoughMaxAttachmentSize({num? fileInfoTotalSize}) { - final currentTotalAttachmentsSize = attachmentsUploaded.totalSize(); - final totalInlineAttachmentsSize = inlineAttachmentsUploaded.totalSize(); - log('UploadController::_validateAttachmentsSize(): $currentTotalAttachmentsSize'); - log('UploadController::_validateAttachmentsSize(): totalInlineAttachmentsSize: $totalInlineAttachmentsSize'); - num uploadedTotalSize = fileInfoTotalSize ?? 0; - - final totalSizeReadyToUpload = currentTotalAttachmentsSize + - totalInlineAttachmentsSize + - uploadedTotalSize; - log('UploadController::_validateAttachmentsSize(): totalSizeReadyToUpload: $totalSizeReadyToUpload'); - + bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) { + final currentTotalSize = attachmentsPicked.totalSize + inlineAttachmentsPicked.totalSize + totalSizePreparedFiles; final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value; + log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail'); if (maxSizeAttachmentsPerEmail != null) { - return totalSizeReadyToUpload <= maxSizeAttachmentsPerEmail; + return currentTotalSize > maxSizeAttachmentsPerEmail; } else { return false; } } - num getTotalSizeFromListFileInfo(List listFiles) { - final uploadedListSize = listFiles.map((file) => file.fileSize).toList(); - num totalSize = uploadedListSize.reduce((sum, size) => sum + size); - log('UploadController::_getTotalSizeFromListFileInfo():totalSize: $totalSize'); - return totalSize; + bool isExceededWarningAttachmentFileSizeInComposer({num totalSizePreparedFiles = 0}) { + final currentTotalSizeAttachments = attachmentsPicked.totalSize + totalSizePreparedFiles; + const maximumBytesSizeFileAttachedInComposer = AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; + log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer'); + return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer; + } + + void validateTotalSizeAttachmentsBeforeUpload({ + required num totalSizePreparedFiles, + num? totalSizePreparedFilesWithDispositionAttachment, + VoidCallback? onValidationSuccess + }) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles'); + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (currentContext == null) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!); + return; + } + + if (isExceededWarningAttachmentFileSizeInComposer(totalSizePreparedFiles: totalSizePreparedFilesWithDispositionAttachment ?? totalSizePreparedFiles)) { + if (currentContext == null) { + log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer( + context: currentContext!, + confirmAction: () async { + await Future.delayed( + const Duration(milliseconds: 100), + onValidationSuccess + ); + } + ); + return; + } + + onValidationSuccess?.call(); + } + + void validateTotalSizeInlineAttachmentsBeforeUpload({ + required num totalSizePreparedFiles, + VoidCallback? onValidationSuccess + }) { + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (currentContext == null) { + log('UploadController::validateTotalSizeInlineAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!); + return; + } + + onValidationSuccess?.call(); + } + + void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) { + final maxSizeAttachmentsPerEmail = filesize(_mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0); + showConfirmDialogAction( + context, + AppLocalizations.of(context).message_dialog_upload_attachments_exceeds_maximum_size(maxSizeAttachmentsPerEmail), + AppLocalizations.of(context).got_it, + title: AppLocalizations.of(context).maximum_files_size, + hasCancelButton: false); + } + + void _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer({ + required BuildContext context, + VoidCallback? confirmAction, + }) { + showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, + AppLocalizations.of(context).continueAction, + cancelTitle: AppLocalizations.of(context).cancel, + alignCenter: true, + onConfirmAction: confirmAction, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: Colors.black + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + )); } bool get allUploadAttachmentsCompleted { @@ -326,6 +422,17 @@ class UploadController extends BaseController { .toList(); } + List get inlineAttachmentsPicked { + if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) { + return List.empty(); + } + return _uploadingStateInlineFiles.uploadingStateFiles + .whereNotNull() + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Map get mapInlineAttachments { final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles .whereNotNull() @@ -347,42 +454,47 @@ class UploadController extends BaseController { return mapInlineAttachments; } + void _handleUploadAttachmentFailure(UploadAttachmentFailure failure) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + failure.fileInfo.isInline == true + ? AppLocalizations.of(currentContext!).thisImageCannotBeAdded + : AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: failure.fileInfo.isInline == true + ? imagePaths.icInsertImage + : imagePaths.icAttachment + ); + } + } + + void _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async { + if (success.uploadAttachment.fileInfo.isInline == true) { + _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); + } else { + _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); + _refreshListUploadAttachmentState(); + } + } + @override - void handleFailureViewState(Failure failure) async { - super.handleFailureViewState(failure); + void handleFailureViewState(Failure failure) { if (failure is UploadAttachmentFailure) { - if (failure.isInline) { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).thisImageCannotBeAdded, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icInsertImage); - } - } else { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icAttachment); - } - } + _handleUploadAttachmentFailure(failure); + } else { + super.handleFailureViewState(failure); } } @override void handleSuccessViewState(Success success) async { - super.handleSuccessViewState(success); if (success is UploadAttachmentSuccess) { - if (success.isInline) { - _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared)); - await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); - } else { - _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); - await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); - _refreshListUploadAttachmentState(); - } + _handleUploadAttachmentSuccess(success); + } else { + super.handleSuccessViewState(success); } } } \ No newline at end of file diff --git a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart index 4b45c9427b..64feed8fe7 100644 --- a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart +++ b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart @@ -4,12 +4,11 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta extension UploadAttachmentExtension on UploadAttachment { - UploadFileState toUploadFileState({bool fromFileShared = false}) { + UploadFileState toUploadFileState() { return UploadFileState( uploadTaskId, file: fileInfo, cancelToken: cancelToken, - fromFileShared: fromFileShared, ); } } \ No newline at end of file diff --git a/lib/features/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index 20876fbe43..f0e58b78c6 100644 --- a/lib/features/upload/presentation/model/upload_file_state.dart +++ b/lib/features/upload/presentation/model/upload_file_state.dart @@ -1,11 +1,12 @@ import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; @@ -17,7 +18,6 @@ class UploadFileState with EquatableMixin { final int uploadingProgress; final Attachment? attachment; final CancelToken? cancelToken; - final bool fromFileShared; UploadFileState( this.uploadTaskId, @@ -27,7 +27,6 @@ class UploadFileState with EquatableMixin { this.uploadingProgress = 0, this.attachment, this.cancelToken, - this.fromFileShared = false, } ); @@ -38,7 +37,6 @@ class UploadFileState with EquatableMixin { int? uploadingProgress, Attachment? attachment, CancelToken? cancelToken, - bool? fromFileShared, }) { return UploadFileState( uploadTaskId ?? this.uploadTaskId, @@ -47,7 +45,6 @@ class UploadFileState with EquatableMixin { uploadingProgress: uploadingProgress ?? this.uploadingProgress, attachment: attachment ?? this.attachment, cancelToken: cancelToken ?? this.cancelToken, - fromFileShared: fromFileShared ?? this.fromFileShared ); } @@ -72,11 +69,16 @@ class UploadFileState with EquatableMixin { double get percentUploading => uploadingProgress / 100; String getIcon(ImagePaths imagePaths) { - var mediaType = attachment?.type; - if (mediaType == null && file != null) { - mediaType = MediaType.parse(file!.mimeType); + try { + MediaType? mediaType = attachment?.type; + if (mediaType == null && file != null) { + mediaType = MediaType.parse(file!.mimeType); + } + return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup; + } catch (e) { + logError('UploadFileState::getIcon: Exception: $e'); + return imagePaths.icFileEPup; } - return attachment?.getIcon(imagePaths, fileMediaType: mediaType) ?? imagePaths.icFileEPup; } @override @@ -87,6 +89,5 @@ class UploadFileState with EquatableMixin { uploadingProgress, attachment, cancelToken, - fromFileShared, ]; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ccd04c7928..9b9fd9e7a2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3238,5 +3238,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continue", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 357766de31..dbca897408 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -3310,7 +3310,7 @@ "placeholders_order": [], "placeholders": {} }, - "clean": "Nettoyer", + "clean": "Vider", "@clean": { "type": "text", "placeholders_order": [], @@ -3448,7 +3448,7 @@ "placeholders_order": [], "placeholders": {} }, - "clearFolder": "Nettoyer le dossier", + "clearFolder": "Vider le dossier", "@clearFolder": { "type": "text", "placeholders_order": [], @@ -3935,5 +3935,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "warningMessageWhenExceedGenerallySizeInComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", + "@warningMessageWhenExceedGenerallySizeInComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continuer", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 6076c18093..b563d04a00 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-05T12:00:06.899366", + "@@last_modified": "2024-03-19T12:10:23.549474", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3749,5 +3749,129 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Continue", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisFileCannotBePicked": "This file cannot be picked.", + "@thisFileCannotBePicked": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loadingPleaseWait": "Loading... Please wait!", + "@loadingPleaseWait": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "status": "Status", + "@status": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "progress": "Progress", + "@progress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingMessage": "Sending message", + "@sendingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenSendEmailFailure": "Sending of the message failed.\nAn error occurred while sending mail.", + "@warningMessageWhenSendEmailFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "closeAnyway": "Close anyway", + "@closeAnyway": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveMessage": "Save message", + "@saveMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "discardChanges": "Discard changes", + "@discardChanges": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenClickCloseComposer": "Save this message to your drafts folder and close composer?", + "@warningMessageWhenClickCloseComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "savingMessage": "Saving message", + "@savingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "creatingMessage": "Creating message", + "@creatingMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "savingMessageToDraftFolder": "Saving message to draft folder", + "@savingMessageToDraftFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenSaveEmailToDraftsFailure": "Saving of the message to drafts folder failed.\nAn error occurred while saving mail.", + "@warningMessageWhenSaveEmailToDraftsFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canceling": "Canceling", + "@canceling": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailToAttendees": "Mail to attendees", + "@mailToAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMore": "Show more (+{count})", + "@showMore": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showLess": "Show less", + "@showLess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index d2b6308f8a..c17bd48988 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3941,5 +3941,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", + "@warningMessageWhenExceedGenerallySizeInComposer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "continueAction": "Tiếp tục", + "@continueAction": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 389a91fcb9..fea26aee3c 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -3,6 +3,7 @@ import 'package:core/data/utils/device_manager.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:core/utils/config/app_config_loader.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; @@ -62,6 +63,7 @@ class CoreBindings extends Bindings { Get.put(AppConfigLoader()); Get.put(FileUtils()); Get.put(PrintUtils()); + Get.put(ApplicationManager(Get.find())); } void _bindingIsolate() { diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index f2b7699b19..82abf014d2 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -71,7 +71,7 @@ class CredentialBindings extends InteractorsBindings { Get.find(), Get.find()) ); - Get.put(AuthenticationDataSourceImpl()); + Get.put(AuthenticationDataSourceImpl(Get.find())); Get.put(AuthenticationOIDCDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 2a015626b8..208c47854d 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -36,7 +36,6 @@ import 'package:tmail_ui_user/features/server_settings/data/network/server_setti import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart'; -import 'package:tmail_ui_user/main/localizations/locale_interceptor.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; @@ -90,13 +89,11 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), )); - Get.put(LocaleInterceptor()); Get.find().interceptors.add(Get.find()); Get.find().interceptors.add(Get.find()); if (kDebugMode) { Get.find().interceptors.add(LogInterceptor(requestBody: true)); } - Get.find().interceptors.add(Get.find()); } void _bindingApi() { diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 91f8862b5e..a7b81d0fdb 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_work import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; -import 'package:tmail_ui_user/main/localizations/locale_interceptor.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -51,7 +50,6 @@ class NetworkIsolateBindings extends Bindings { if (kDebugMode) { dio.interceptors.add(LogInterceptor(requestBody: true)); } - dio.interceptors.add(Get.find()); } void _bindingApi() { diff --git a/lib/main/exceptions/remote_exception.dart b/lib/main/exceptions/remote_exception.dart index 69a8732f8b..235be7a938 100644 --- a/lib/main/exceptions/remote_exception.dart +++ b/lib/main/exceptions/remote_exception.dart @@ -11,7 +11,7 @@ abstract class RemoteException with EquatableMixin implements Exception { static const badCredentials = 'Bad credentials'; static const socketException = 'Socket exception'; - final String? message; + final Object? message; final int? code; const RemoteException({this.code, this.message}); @@ -25,7 +25,7 @@ class BadCredentialsException extends RemoteException { } class UnknownError extends RemoteException { - const UnknownError({int? code, String? message}) : super(code: code, message: message); + const UnknownError({int? code, Object? message}) : super(code: code, message: message); @override List get props => [code, message]; diff --git a/lib/main/exceptions/remote_exception_thrower.dart b/lib/main/exceptions/remote_exception_thrower.dart index 1a37115aa7..885133b92b 100644 --- a/lib/main/exceptions/remote_exception_thrower.dart +++ b/lib/main/exceptions/remote_exception_thrower.dart @@ -53,7 +53,7 @@ class RemoteExceptionThrower extends ExceptionThrower { if (error.error is SocketException) { throw const SocketError(); } else if (error.error != null) { - throw UnknownError(message: error.error!.toString()); + throw UnknownError(message: error.error); } else { throw const UnknownError(); } diff --git a/lib/main/exceptions/send_email_exception_thrower.dart b/lib/main/exceptions/send_email_exception_thrower.dart index d5d908cb4b..2e1bf96e36 100644 --- a/lib/main/exceptions/send_email_exception_thrower.dart +++ b/lib/main/exceptions/send_email_exception_thrower.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; -import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5ceea1c55d..5dcf3481c2 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3916,4 +3916,131 @@ class AppLocalizations { name: 'zoomOut', ); } + + String get warningMessageWhenExceedGenerallySizeInComposer { + return Intl.message( + 'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.', + name: 'warningMessageWhenExceedGenerallySizeInComposer', + ); + } + + String get continueAction { + return Intl.message( + 'Continue', + name: 'continueAction', + ); + } + + String get thisFileCannotBePicked { + return Intl.message( + 'This file cannot be picked.', + name: 'thisFileCannotBePicked', + ); + } + + String get loadingPleaseWait { + return Intl.message( + 'Loading... Please wait!', + name: 'loadingPleaseWait', + ); + } + + String get status { + return Intl.message( + 'Status', + name: 'status'); + } + + String get progress { + return Intl.message( + 'Progress', + name: 'progress'); + } + + String get sendingMessage { + return Intl.message( + 'Sending message', + name: 'sendingMessage'); + } + + String get warningMessageWhenSendEmailFailure { + return Intl.message( + 'Sending of the message failed.\nAn error occurred while sending mail.', + name: 'warningMessageWhenSendEmailFailure'); + } + + String get closeAnyway { + return Intl.message( + 'Close anyway', + name: 'closeAnyway'); + } + + String get saveMessage { + return Intl.message( + 'Save message', + name: 'saveMessage'); + } + + String get discardChanges { + return Intl.message( + 'Discard changes', + name: 'discardChanges'); + } + + String get warningMessageWhenClickCloseComposer { + return Intl.message( + 'Save this message to your drafts folder and close composer?', + name: 'warningMessageWhenClickCloseComposer'); + } + + String get savingMessage { + return Intl.message( + 'Saving message', + name: 'savingMessage'); + } + + String get creatingMessage { + return Intl.message( + 'Creating message', + name: 'creatingMessage'); + } + + String get savingMessageToDraftFolder { + return Intl.message( + 'Saving message to draft folder', + name: 'savingMessageToDraftFolder'); + } + + String get warningMessageWhenSaveEmailToDraftsFailure { + return Intl.message( + 'Saving of the message to drafts folder failed.\nAn error occurred while saving mail.', + name: 'warningMessageWhenSaveEmailToDraftsFailure'); + } + + String get canceling { + return Intl.message( + 'Canceling', + name: 'canceling' + ); + } + + String get mailToAttendees { + return Intl.message( + 'Mail to attendees', + name: 'mailToAttendees' + ); + } + + String showMore(int count) { + return Intl.message( + 'Show more (+$count)', + name: 'showMore', + args: [count]); + } + + String get showLess { + return Intl.message( + 'Show less', + name: 'showLess'); + } } \ No newline at end of file diff --git a/lib/main/localizations/locale_interceptor.dart b/lib/main/localizations/locale_interceptor.dart deleted file mode 100644 index 82285d00b2..0000000000 --- a/lib/main/localizations/locale_interceptor.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:io'; - -import 'package:core/utils/app_logger.dart'; -import 'package:dio/dio.dart'; -import 'package:tmail_ui_user/main/localizations/localization_service.dart'; - -class LocaleInterceptor extends InterceptorsWrapper { - - @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - final currentLocale = LocalizationService.getLocaleFromLanguage(); - log('LocaleInterceptor::onRequest:currentLocale: $currentLocale'); - options.headers[HttpHeaders.acceptLanguageHeader] = LocalizationService.supportedLocalesToLanguageTags(); - options.headers[HttpHeaders.contentLanguageHeader] = currentLocale.toLanguageTag(); - super.onRequest(options, handler); - } -} \ No newline at end of file diff --git a/lib/main/localizations/localization_service.dart b/lib/main/localizations/localization_service.dart index eb787db761..f58a3773ba 100644 --- a/lib/main/localizations/localization_service.dart +++ b/lib/main/localizations/localization_service.dart @@ -38,17 +38,22 @@ class LocalizationService extends Translations { } static Locale getLocaleFromLanguage({String? langCode}) { - final languageCacheManager = getBinding(); - log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); - final localeStored = languageCacheManager?.getStoredLanguage(); - log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); - if (localeStored != null) { - return localeStored; - } else { - final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; - log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); - final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); - return localeSelected ?? Get.deviceLocale ?? defaultLocale; + try { + final languageCacheManager = getBinding(); + log('LocalizationService::_getLocaleFromLanguage:languageCacheManager: $languageCacheManager'); + final localeStored = languageCacheManager?.getStoredLanguage(); + log('LocalizationService::_getLocaleFromLanguage():localeStored: $localeStored'); + if (localeStored != null) { + return localeStored; + } else { + final languageCodeCurrent = langCode ?? Get.deviceLocale?.languageCode; + log('LocalizationService::_getLocaleFromLanguage():languageCodeCurrent: $languageCodeCurrent'); + final localeSelected = supportedLocales.firstWhereOrNull((locale) => locale.languageCode == languageCodeCurrent); + return localeSelected ?? Get.deviceLocale ?? defaultLocale; + } + } catch (e) { + logError('LocalizationService::getLocaleFromLanguage: Exception: $e'); + return Get.deviceLocale ?? defaultLocale; } } diff --git a/lib/main/pages/app_pages.dart b/lib/main/pages/app_pages.dart index e13239bc97..83e23838f0 100644 --- a/lib/main/pages/app_pages.dart +++ b/lib/main/pages/app_pages.dart @@ -85,7 +85,6 @@ class AppPages { ...[ GetPage( name: AppRoutes.composer, - opaque: false, page: () => DeferredWidget( composer.loadLibrary, () => composer.ComposerView()), diff --git a/lib/main/routes/route_navigation.dart b/lib/main/routes/route_navigation.dart index 3130fd4525..cfd6cbca53 100644 --- a/lib/main/routes/route_navigation.dart +++ b/lib/main/routes/route_navigation.dart @@ -18,8 +18,8 @@ Future pushAndPopAll(String routeName, {dynamic arguments}) async { return Get.offAllNamed(routeName, arguments: arguments); } -void popBack({dynamic result}) { - Get.back(result: result); +void popBack({dynamic result, bool closeOverlays = false}) { + Get.back(closeOverlays: closeOverlays, result: result); } bool canBack(BuildContext context) { diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index a4634f5a16..bff358776c 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class AppConfig { static const int limitCharToStartSearch = 3; + static const int warningAttachmentFileSizeInMegabytes = 10; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appFCMConfigurationPath = "configurations/env.fcm"; diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 9819bdf81b..4347b829f3 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -68,19 +68,6 @@ enum ContentDisposition { other } -extension ContentDispositionExtension on ContentDisposition { - String get value { - switch(this) { - case ContentDisposition.inline: - return 'inline'; - case ContentDisposition.attachment: - return 'attachment'; - case ContentDisposition.other: - return toString(); - } - } -} - extension DispositionStringExtension on String? { ContentDisposition? toContentDisposition() { if (this != null) { diff --git a/model/lib/extensions/attachment_extension.dart b/model/lib/extensions/attachment_extension.dart index dcc25b7b4b..8b5d631622 100644 --- a/model/lib/extensions/attachment_extension.dart +++ b/model/lib/extensions/attachment_extension.dart @@ -1,9 +1,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:model/model.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; extension AttachmentExtension on Attachment { - EmailBodyPart toEmailBodyPart({String? disposition, String? charset}) => EmailBodyPart( + EmailBodyPart toEmailBodyPart({String? charset}) => EmailBodyPart( partId: partId, blobId: blobId, size: size, @@ -11,7 +12,7 @@ extension AttachmentExtension on Attachment { type: type, cid: cid, charset: charset, - disposition: disposition ?? this.disposition?.value); + disposition: disposition?.name ?? ContentDisposition.attachment.name); Attachment toAttachmentWithDisposition({ ContentDisposition? disposition, diff --git a/model/lib/extensions/list_attachment_extension.dart b/model/lib/extensions/list_attachment_extension.dart index e882fa117f..e8f788170a 100644 --- a/model/lib/extensions/list_attachment_extension.dart +++ b/model/lib/extensions/list_attachment_extension.dart @@ -6,7 +6,7 @@ import 'package:model/extensions/attachment_extension.dart'; extension ListAttachmentExtension on List { - num totalSize() { + num get totalSize { if (isNotEmpty) { final currentListSize = map((attachment) => attachment.size?.value ?? 0).toList(); final totalSize = currentListSize.reduce((sum, size) => sum + size); diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 1ca8061f4b..fc3bf0b6f8 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -32,7 +32,6 @@ extension PresentationEmailExtension on PresentationEmail { int numberOfAllEmailAddress() => to.numberEmailAddress() + cc.numberEmailAddress() + bcc.numberEmailAddress(); String getReceivedAt(String newLocale, {String? pattern}) { - log('PresentationEmailExtension::getReceivedAt: newLocale = $newLocale | pattern = $pattern'); final emailTime = receivedAt; if (emailTime != null) { return emailTime.formatDateToLocal( diff --git a/model/lib/extensions/user_profile_extension.dart b/model/lib/extensions/user_profile_extension.dart deleted file mode 100644 index b9f332d1a7..0000000000 --- a/model/lib/extensions/user_profile_extension.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:model/model.dart'; - -extension UserProfileExtension on UserProfile { - UserProfileResponse toUserProfileResponse() { - return UserProfileResponse(email); - } -} \ No newline at end of file diff --git a/model/lib/extensions/username_extension.dart b/model/lib/extensions/username_extension.dart new file mode 100644 index 0000000000..ffcd436bab --- /dev/null +++ b/model/lib/extensions/username_extension.dart @@ -0,0 +1,8 @@ +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; + +extension UsernameExtension on UserName { + String get firstCharacter => value.isNotEmpty ? value[0].toUpperCase() : ''; + + EmailAddress toEmailAddress() => EmailAddress(null, value); +} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 760cccb81e..2835ec4dfa 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -57,7 +57,7 @@ export 'extensions/presentation_email_extension.dart'; export 'extensions/presentation_mailbox_extension.dart'; export 'extensions/properties_extension.dart'; export 'extensions/session_extension.dart'; -export 'extensions/user_profile_extension.dart'; +export 'extensions/username_extension.dart'; export 'extensions/utc_date_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; @@ -79,7 +79,4 @@ export 'oidc/token_id.dart'; export 'oidc/token_oidc.dart'; // Upload export 'upload/file_info.dart'; -export 'upload/upload_response.dart'; -// User -export 'user/user_profile.dart'; -export 'user/user_profile_response.dart'; \ No newline at end of file +export 'upload/upload_response.dart'; \ No newline at end of file diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index 924f5e86e2..fa79b85fcf 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -1,32 +1,65 @@ - import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:mime/mime.dart'; class FileInfo with EquatableMixin { final String fileName; - final String filePath; final int fileSize; + final String? filePath; final Uint8List? bytes; + final Stream>? readStream; + final String? type; + final bool? isInline; + final bool? isShared; - FileInfo(this.fileName, this.filePath, this.fileSize, {this.bytes}); - - factory FileInfo.empty() { - return FileInfo('', '', 0); - } + FileInfo({ + required this.fileName, + required this.fileSize, + this.filePath, + this.bytes, + this.readStream, + this.type, + this.isInline, + this.isShared, + }); factory FileInfo.fromBytes({ required Uint8List bytes, String? name, - int? size + int? size, + String? type, }) { - return FileInfo(name ?? '', '', size ?? 0, bytes: bytes); + return FileInfo( + fileName: name ?? '', + fileSize: size ?? 0, + bytes: bytes, + type: type + ); } String get fileExtension => fileName.split('.').last; - String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream'; + String get mimeType { + if (type?.isNotEmpty == true) { + return type!; + } else if (filePath?.isNotEmpty == true){ + final matchedType = lookupMimeType(filePath!, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } else { + final matchedType = lookupMimeType(fileName, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } + } @override - List get props => [fileName, filePath, fileSize, bytes]; + List get props => [ + fileName, + filePath, + fileSize, + bytes, + readStream, + type, + isInline, + isShared, + ]; } \ No newline at end of file diff --git a/model/lib/user/avatar_id.dart b/model/lib/user/avatar_id.dart deleted file mode 100644 index 2710f8aa85..0000000000 --- a/model/lib/user/avatar_id.dart +++ /dev/null @@ -1,15 +0,0 @@ - -import 'package:equatable/equatable.dart'; - -class AvatarId with EquatableMixin { - final String id; - - AvatarId(this.id); - - factory AvatarId.initial() { - return AvatarId(''); - } - - @override - List get props => [id]; -} \ No newline at end of file diff --git a/model/lib/user/user_profile.dart b/model/lib/user/user_profile.dart deleted file mode 100644 index 80609cd21b..0000000000 --- a/model/lib/user/user_profile.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class UserProfile with EquatableMixin { - - final String email; - - UserProfile(this.email); - - String getAvatarText() { - return email[0].toUpperCase(); - } - - @override - List get props => [email]; -} \ No newline at end of file diff --git a/model/lib/user/user_profile_response.dart b/model/lib/user/user_profile_response.dart deleted file mode 100644 index 10f4842cbc..0000000000 --- a/model/lib/user/user_profile_response.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:model/model.dart'; - -part 'user_profile_response.g.dart'; - -@JsonSerializable() -class UserProfileResponse with EquatableMixin { - - final String email; - - UserProfileResponse(this.email); - - factory UserProfileResponse.fromJson(Map json) => _$UserProfileResponseFromJson(json); - - Map toJson() => _$UserProfileResponseToJson(this); - - @override - List get props => [email]; -} - -extension UserProfileResponseExtension on UserProfileResponse { - UserProfile toUserProfile() { - return UserProfile(email); - } -} \ No newline at end of file diff --git a/model/pubspec.lock b/model/pubspec.lock index 2361d5268e..fb784ca817 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -304,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fk_user_agent: + dependency: transitive + description: + name: fk_user_agent + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" + source: hosted + version: "2.1.0" flex_color_picker: dependency: transitive description: @@ -555,8 +563,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -648,6 +656,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/model/pubspec.yaml b/model/pubspec.yaml index a37203b6ca..b8eef5797d 100644 --- a/model/pubspec.yaml +++ b/model/pubspec.yaml @@ -30,10 +30,12 @@ dependencies: path: ../core ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/pubspec.lock b/pubspec.lock index 8a2ef32cdb..942919c5a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -360,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + url: "https://pub.dev" + source: hosted + version: "0.4.4" device_info_plus: dependency: "direct main" description: @@ -420,8 +428,8 @@ packages: dependency: transitive description: path: "." - ref: email_supported - resolved-ref: f13a35eb76fafb2ee1e686ca19c4c742d0efa78a + ref: cnb_supported + resolved-ref: "6409f47b01c5992906971f2d8fdb8be3278bf787" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" @@ -513,7 +521,7 @@ packages: source: hosted version: "6.1.4" file_picker: - dependency: "direct overridden" + dependency: "direct main" description: path: "." ref: "email_supported_5.3.1" @@ -586,7 +594,7 @@ packages: source: hosted version: "1.1.0" fk_user_agent: - dependency: "direct main" + dependency: transitive description: name: fk_user_agent sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d @@ -990,6 +998,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + future_loading_dialog: + dependency: "direct main" + description: + name: future_loading_dialog + sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e" + url: "https://pub.dev" + source: hosted + version: "0.3.0" get: dependency: "direct main" description: @@ -1042,8 +1058,8 @@ packages: dependency: "direct main" description: path: "." - ref: "bugfix/composer-reply-memory-leak" - resolved-ref: "7a20f8e924e403eb54f4a6177c3c8ecb0c7b3687" + ref: cnb_supported + resolved-ref: "978886d768e6540fc7dbe016dd83733c56ffb220" url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "2.5.1" @@ -1131,8 +1147,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -1256,7 +1272,7 @@ packages: source: hosted version: "2.1.0" package_info_plus: - dependency: "direct main" + dependency: transitive description: name: package_info_plus sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" @@ -1533,8 +1549,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "867d22aaa2da242bb1c5929e2804e3cb1d4acaf6" + ref: cnb_supported + resolved-ref: ef8bcdd1824badd7262aa63952d4b83a47ef0dc4 url: "https://github.com/linagora/rich-text-composer.git" source: git version: "0.0.2" @@ -1722,7 +1738,7 @@ packages: description: path: "." ref: master - resolved-ref: dd81c8f1e870bacdffddda5b360c5fbe8a188bbb + resolved-ref: "1954bb41a3a12899c426d17a7b1c9e561cccce06" url: "https://github.com/dab246/super_tag_editor.git" source: git version: "0.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7acf157001..dd8335bff2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,17 +55,19 @@ dependencies: rich_text_composer: git: url: https://github.com/linagora/rich-text-composer.git - ref: master + ref: cnb_supported html_editor_enhanced: git: url: https://github.com/linagora/html-editor-enhanced.git - ref: bugfix/composer-reply-memory-leak + ref: cnb_supported + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun contacts_service: git: @@ -152,16 +154,12 @@ dependencies: hive: 2.2.3 - fk_user_agent: 2.1.0 - pointer_interceptor: 0.9.1 rxdart: 0.27.7 connectivity_plus: 3.0.3 - package_info_plus: 4.2.0 - dropdown_button2: 2.0.0 flutter_staggered_grid_view: 0.6.2 @@ -240,6 +238,12 @@ dependencies: mime: 1.0.4 + desktop_drop: 0.4.4 + + future_loading_dialog: 0.3.0 + + file_picker: 5.3.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index 54a8878c9c..f3f4194524 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -295,8 +295,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "21d15bc1a6a75e048ee1e3cd751dd0815492ce20" + ref: migrate_cnb_to_master_5Jun + resolved-ref: "49305a382ca77211a0668fd9fe196a3c057cdc8e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/rule_filter/pubspec.yaml b/rule_filter/pubspec.yaml index 3406395aad..ac8d6dd110 100644 --- a/rule_filter/pubspec.yaml +++ b/rule_filter/pubspec.yaml @@ -12,10 +12,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/server_settings/pubspec.yaml b/server_settings/pubspec.yaml index d4570b6a55..6d263ac4e1 100644 --- a/server_settings/pubspec.yaml +++ b/server_settings/pubspec.yaml @@ -11,10 +11,12 @@ dependencies: sdk: flutter ### Dependencies from git ### + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/linagora/jmap-dart-client/pull/87 jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git - ref: master + ref: migrate_cnb_to_master_5Jun ### Dependencies from pub.dev ### equatable: 2.0.5 diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index a64474e935..82ae55253c 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -1,7 +1,9 @@ import 'dart:ui'; import 'package:core/core.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:dartz/dartz.dart' hide State; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -86,6 +88,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -118,6 +121,7 @@ void main() { final printEmailInteractor = MockPrintEmailInteractor(); final storeEventAttendanceStatusInteractor = MockStoreEventAttendanceStatusInteractor(); final printUtils = MockPrintUtils(); + final applicationManager = MockApplicationManager(); late SingleEmailController singleEmailController; @@ -151,6 +155,7 @@ void main() { Get.put(responsiveUtils); Get.put(uuid); Get.put(printUtils); + Get.put(applicationManager); when(mailboxDashboardController.accountId).thenReturn(Rxn(testAccountId)); when(uuid.v4()).thenReturn(testTaskId); @@ -181,6 +186,7 @@ void main() { setUp(() { Get.locale = locale; + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; }); group('accept test:', () { @@ -257,7 +263,7 @@ void main() { BlobCalendarEvent( blobId: blobId, calendarEventList: [calendarEvent])])); - + // act singleEmailController.onCalendarEventReplyAction(EventActionType.no, emailId); await untilCalled(rejectCalendarEventInteractor.execute(any, any, any, any)); @@ -266,9 +272,18 @@ void main() { verify(rejectCalendarEventInteractor.execute(testAccountId, {blobId}, emailId, locale.languageCode)).called(1); }); }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); }); group('StoreEventAttendanceStatusInteractor test', () { + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + }); + group('calendarEventSuccess method test', () { test( 'SHOULD call execute on StoreEventAttendanceStatusInteractor\n' @@ -339,5 +354,9 @@ void main() { )).called(1); }); }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); }); } diff --git a/test/features/features/composer/recipient_composer_widget_test.dart b/test/features/features/composer/recipient_composer_widget_test.dart new file mode 100644 index 0000000000..679b90c0c8 --- /dev/null +++ b/test/features/features/composer/recipient_composer_widget_test.dart @@ -0,0 +1,605 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:model/mailbox/expand_mode.dart'; +import 'package:super_tag_editor/tag_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; + +void main() { + group('recipient_composer_widget test', () { + final imagePaths = ImagePaths(); + final keyEmailTagEditor = GlobalKey(); + const prefix = PrefixEmailAddress.to; + + Widget makeTestableWidget({required Widget child}) { + return GetMaterialApp( + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + home: Scaffold(body: child), + ); + } + + testWidgets('RecipientComposerWidget renders correctly', (tester) async { + final listEmailAddress = []; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientComposerWidgetFinder = find.byType(RecipientComposerWidget); + + expect(recipientComposerWidgetFinder, findsOneWidget); + }); + + testWidgets('RecipientComposerWidget renders list email address correctly', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test1@example.com'), + EmailAddress(null, 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect(find.byType(RecipientTagItemWidget), findsNWidgets(2)); + expect(find.text('test1@example.com'), findsOneWidget); + expect(find.text('test2@example.com'), findsOneWidget); + }); + + testWidgets('RecipientTagItemWidget should have a `maxWidth` equal to the RecipientComposerWidget\'s `maxWidth`', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Container recipientTagItemWidget = tester.widget(recipientTagItemWidgetFinder); + + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect(recipientTagItemWidget.constraints!.maxWidth, 360); + }); + + testWidgets('WHEN EmailAddress has address is not empty AND display name is not empty\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, + lessThan(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect( + prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, + lessThan(360) + ); + }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + 'RecipientTagItemWidget should have all the components (Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test123456789123456789@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 392.7, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize'); + + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width, + lessThan(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is too long\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test12345678912345678909123456789', 'test1234567891234567895678909123456789@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 392.7, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: TagSize = $recipientTagItemWidgetSize | LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + + expect( + labelRecipientTagItemWidgetSize.width + deleteIconRecipientTagItemWidgetSize.width + avatarIconRecipientTagItemWidgetSize.width, + lessThan(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('WHEN To has multiple recipients AND expandMode is COLLAPSE\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon, CounterTag)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + EmailAddress('test2', 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + expandMode: ExpandMode.COLLAPSE, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + expect( + prefixRecipientComposerWidgetSize.width + recipientTagItemWidgetSize.width, + lessThan(360) + ); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0')); + final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0')); + final counterRecipientTagItemWidgetFinder = find.byKey(Key('counter_recipient_tag_item_${prefix.name}_0')); + + final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder); + final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder); + final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder); + final Size counterRecipientTagItemWidgetSize = tester.getSize(counterRecipientTagItemWidgetFinder); + + log('recipient_composer_widget_test::main: LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize | CounterTagSize = $counterRecipientTagItemWidgetSize'); + + expect(labelRecipientTagItemWidgetFinder, findsOneWidget); + expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget); + expect(counterRecipientTagItemWidgetFinder, findsOneWidget); + + final totalSizeOfAllComponents = labelRecipientTagItemWidgetSize.width + + deleteIconRecipientTagItemWidgetSize.width + + avatarIconRecipientTagItemWidgetSize.width; + counterRecipientTagItemWidgetSize.width; + + expect( + totalSizeOfAllComponents, + lessThan(recipientTagItemWidgetSize.width) + ); + }); + + testWidgets('WHEN To has multiple recipients AND expandMode is EXPAND\n' + 'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + EmailAddress('test2', 'test2@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + expandMode: ExpandMode.EXPAND, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byType(RecipientTagItemWidget); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsNWidgets(2)); + }); + + testWidgets('WHEN EmailAddress has address is too long AND display name is NULL\n' + 'RecipientTagItemWidget SHOULD have text that overflows', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test12345678901234567890@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + + final labelRecipientTagItemWidget = tester.widget(labelRecipientTagItemWidgetFinder); + final labelTagWidth = tester.getSize(labelRecipientTagItemWidgetFinder).width; + + expect(labelRecipientTagItemWidget.overflow, equals(TextOverflow.ellipsis)); + + final TextPainter textPainter = TextPainter( + maxLines: labelRecipientTagItemWidget.maxLines, + textDirection: labelRecipientTagItemWidget.textDirection ?? TextDirection.ltr, + text: TextSpan( + text: labelRecipientTagItemWidget.data, + style: labelRecipientTagItemWidget.style, + locale: labelRecipientTagItemWidget.locale + ), + ); + textPainter.layout(maxWidth: labelTagWidth); + bool isExceededTextOverflow = textPainter.didExceedMaxLines; + log('recipient_composer_widget_test::main: LABEL_TAB_WIDTH = $labelTagWidth | TextPainterWidth = ${textPainter.width} | isExceededTextOverflow = $isExceededTextOverflow'); + + expect(isExceededTextOverflow, equals(true)); + }); + + testWidgets('WHEN EmailAddress has address short AND display name is NULL\n' + 'RecipientTagItemWidget SHOULD have text display full', (tester) async { + final listEmailAddress = [ + EmailAddress(null, 'test123@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0')); + + final labelRecipientTagItemWidget = tester.widget(labelRecipientTagItemWidgetFinder); + final labelTagWidth = tester.getSize(labelRecipientTagItemWidgetFinder).width; + + expect(labelRecipientTagItemWidget.overflow, equals(TextOverflow.ellipsis)); + + final TextPainter textPainter = TextPainter( + maxLines: labelRecipientTagItemWidget.maxLines, + textDirection: labelRecipientTagItemWidget.textDirection ?? TextDirection.ltr, + text: TextSpan( + text: labelRecipientTagItemWidget.data, + style: labelRecipientTagItemWidget.style, + locale: labelRecipientTagItemWidget.locale + ), + ); + textPainter.layout(maxWidth: labelTagWidth); + bool isExceededTextOverflow = textPainter.didExceedMaxLines; + log('recipient_composer_widget_test::main: LABEL_TAB_WIDTH = $labelTagWidth | TextPainterWidth = ${textPainter.width} | isExceededTextOverflow = $isExceededTextOverflow'); + + expect(isExceededTextOverflow, equals(false)); + }); + + testWidgets('ToRecipientComponentWidget should display prefix To label correctly when the locale is fr-FR', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + Get.updateLocale(const Locale(LanguageCodeConstants.french, 'FR')); + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final prefixRecipientComposerWidget = tester.widget(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PREFIX_LABEL = ${prefixRecipientComposerWidget.data}'); + + expect(prefixRecipientComposerWidget.data, equals('À:')); + }); + + testWidgets('ToRecipientComponentWidget should display prefix To label correctly when the locale is vi-VN', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + Get.updateLocale(const Locale(LanguageCodeConstants.vietnamese, 'VN')); + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final prefixRecipientComposerWidget = tester.widget(prefixRecipientComposerWidgetFinder); + + log('recipient_composer_widget_test::main: PREFIX_LABEL = ${prefixRecipientComposerWidget.data}'); + + expect(prefixRecipientComposerWidget.data, equals('Đến:')); + }); + + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, ExpandButton) on mobile platform', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + final recipientExpandButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_expand_button')); + + final Size recipientExpandButtonSize = tester.getSize(recipientExpandButtonFinder); + + log('recipient_composer_widget_test::main: ExpandButtonSize = $recipientExpandButtonSize'); + + expect(recipientExpandButtonFinder, findsOneWidget); + + final totalComponentsSize = prefixRecipientComposerWidgetSize.width + + recipientTagItemWidgetSize.width + + recipientExpandButtonSize.width; + + log('recipient_composer_widget_test::main: totalComponentsSize = $totalComponentsSize'); + + expect(totalComponentsSize, lessThan(360)); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, FromButton, CCButton, BccButton) on web platform', (tester) async { + final listEmailAddress = [ + EmailAddress('test1', 'test1@example.com'), + ]; + + final widget = makeTestableWidget( + child: RecipientComposerWidget( + prefix: prefix, + listEmailAddress: listEmailAddress, + imagePaths: imagePaths, + maxWidth: 360, + keyTagEditor: keyEmailTagEditor, + isTestingForWeb: true, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + final prefixRecipientComposerWidgetFinder = find.byKey(Key('prefix_${prefix.name}_recipient_composer_widget')); + final recipientTagItemWidgetFinder = find.byKey(Key('recipient_tag_item_${prefix.name}_0')); + + final Size prefixRecipientComposerWidgetSize = tester.getSize(prefixRecipientComposerWidgetFinder); + final Size recipientTagItemWidgetSize = tester.getSize(recipientTagItemWidgetFinder); + + expect(prefixRecipientComposerWidgetFinder, findsOneWidget); + expect(recipientTagItemWidgetFinder, findsOneWidget); + + log('recipient_composer_widget_test::main: PrefixLabelSize = $prefixRecipientComposerWidgetSize | TagSize = $recipientTagItemWidgetSize'); + + final recipientFromButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_from_button')); + final recipientCcButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_cc_button')); + final recipientBccButtonFinder = find.byKey(Key('prefix_${prefix.name}_recipient_bcc_button')); + + final Size recipientFromButtonSize = tester.getSize(recipientFromButtonFinder); + final Size recipientCcButtonSize = tester.getSize(recipientCcButtonFinder); + final Size recipientBccButtonSize = tester.getSize(recipientBccButtonFinder); + + log('recipient_composer_widget_test::main: FromButtonSize = $recipientFromButtonSize | CcButtonSize = $recipientCcButtonSize | BccButtonSize = $recipientBccButtonSize'); + + expect(recipientFromButtonFinder, findsOneWidget); + expect(recipientCcButtonFinder, findsOneWidget); + expect(recipientBccButtonFinder, findsOneWidget); + }); + }); +} diff --git a/test/features/home/presentation/home_controller_test.dart b/test/features/home/presentation/home_controller_test.dart index 77f089ec21..fff4bf85f7 100644 --- a/test/features/home/presentation/home_controller_test.dart +++ b/test/features/home/presentation/home_controller_test.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -51,6 +52,7 @@ import 'home_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -77,6 +79,7 @@ void main() { late MockImagePaths mockImagePaths; late MockResponsiveUtils mockResponsiveUtils; late MockUuid mockUuid; + late MockApplicationManager mockApplicationManager; setUpAll(() { cleanupEmailCacheInteractor = MockCleanupEmailCacheInteractor(); @@ -102,6 +105,7 @@ void main() { mockImagePaths = MockImagePaths(); mockResponsiveUtils = MockResponsiveUtils(); mockUuid = MockUuid(); + mockApplicationManager = MockApplicationManager(); Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); @@ -122,6 +126,7 @@ void main() { Get.put(mockImagePaths); Get.put(mockResponsiveUtils); Get.put(mockUuid); + Get.put(mockApplicationManager); Get.testMode = true; homeController = HomeController( diff --git a/test/features/login/presentation/login_controller_test.dart b/test/features/login/presentation/login_controller_test.dart index 745551b988..0c9750e576 100644 --- a/test/features/login/presentation/login_controller_test.dart +++ b/test/features/login/presentation/login_controller_test.dart @@ -3,6 +3,7 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:mockito/annotations.dart'; @@ -65,6 +66,7 @@ import 'login_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { late MockAuthenticationInteractor mockAuthenticationInteractor; @@ -94,6 +96,7 @@ void main() { late MockImagePaths mockImagePaths; late MockResponsiveUtils mockResponsiveUtils; late MockUuid mockUuid; + late MockApplicationManager mockApplicationManager; late LoginController loginController; @@ -130,6 +133,7 @@ void main() { mockImagePaths = MockImagePaths(); mockResponsiveUtils = MockResponsiveUtils(); mockUuid = MockUuid(); + mockApplicationManager = MockApplicationManager(); Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); @@ -149,6 +153,7 @@ void main() { Get.put(mockImagePaths); Get.put(mockResponsiveUtils); Get.put(mockUuid); + Get.put(mockApplicationManager); Get.testMode = true; loginController = LoginController( diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index d8b85c58a7..dd1cc34f96 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -2,6 +2,7 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/application_manager.dart'; import 'package:flutter/widgets.dart' hide State; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -16,13 +17,9 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:model/user/user_profile.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:rxdart/subjects.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/delete_email_permanently_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_restored_deleted_message_interactor.dart'; @@ -54,6 +51,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_na import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart'; @@ -68,6 +66,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart'; import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart'; @@ -123,8 +122,6 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), - MockSpec(), - MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -169,6 +166,9 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -195,8 +195,6 @@ void main() { final getAllSendingEmailInteractor = MockGetAllSendingEmailInteractor(); final storeSessionInteractor = MockStoreSessionInteractor(); final emptySpamFolderInteractor = MockEmptySpamFolderInteractor(); - final saveEmailAsDraftsInteractor = MockSaveEmailAsDraftsInteractor(); - final updateEmailDraftsInteractor = MockUpdateEmailDraftsInteractor(); final deleteSendingEmailInteractor = MockDeleteSendingEmailInteractor(); final unsubscribeEmailInteractor = MockUnsubscribeEmailInteractor(); final restoreDeletedMessageInteractor = @@ -231,6 +229,7 @@ void main() { final imagePaths = MockImagePaths(); final responsiveUtils = MockResponsiveUtils(); final uuid = MockUuid(); + final applicationManager = MockApplicationManager(); // mock reloadable controller Get dependencies final getSessionInteractor = MockGetSessionInteractor(); @@ -249,6 +248,8 @@ void main() { final verifyNameInteractor = MockVerifyNameInteractor(); final getAllMailboxInteractor = MockGetAllMailboxInteractor(); final refreshAllMailboxInteractor = MockRefreshAllMailboxInteractor(); + final removeComposerCacheOnWebInteractor = MockRemoveComposerCacheOnWebInteractor(); + final getAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); late MailboxController mailboxController; // mock thread controller direct dependencies @@ -295,17 +296,14 @@ void main() { Get.put(imagePaths); Get.put(responsiveUtils); Get.put(uuid); + Get.put(applicationManager); Get.put(getSessionInteractor); Get.put(getAuthenticatedAccountInteractor); Get.put(updateAuthenticationAccountInteractor); + Get.put(getAllIdentitiesInteractor); + Get.put(removeComposerCacheOnWebInteractor); Get.testMode = true; - PackageInfo.setMockInitialValues( - appName: '', - packageName: '', - version: '', - buildNumber: '', - buildSignature: ''); when(emailReceiveManager.pendingEmailAddressInfo).thenAnswer((_) => BehaviorSubject.seeded(null)); when(emailReceiveManager.pendingEmailContentInfo).thenAnswer((_) => BehaviorSubject.seeded(null)); @@ -336,12 +334,13 @@ void main() { getAllSendingEmailInteractor, storeSessionInteractor, emptySpamFolderInteractor, - saveEmailAsDraftsInteractor, - updateEmailDraftsInteractor, deleteSendingEmailInteractor, unsubscribeEmailInteractor, restoreDeletedMessageInteractor, - getRestoredDeletedMessageInteractor); + getRestoredDeletedMessageInteractor, + removeComposerCacheOnWebInteractor, + getAllIdentitiesInteractor, + ); Get.put(mailboxDashboardController); mailboxDashboardController.onReady(); @@ -372,7 +371,6 @@ void main() { mailboxDashboardController.sessionCurrent = testSession; mailboxDashboardController.filterMessageOption.value = FilterMessageOption.all; - mailboxDashboardController.userProfile.value = UserProfile('test@gmail.com'); mailboxDashboardController.accountId.value = testAccountId; }); diff --git a/test/features/model/attachment_extension_test.dart b/test/features/model/attachment_extension_test.dart index 33b050a38c..2b686ec274 100644 --- a/test/features/model/attachment_extension_test.dart +++ b/test/features/model/attachment_extension_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; void main() { final attachmentA = Attachment( @@ -34,7 +34,7 @@ void main() { 'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentA.isDisplayedPDFIcon; + bool result = attachmentA.type!.validatePDFIcon(fileName: null); expect(result, isTrue); }); @@ -45,7 +45,7 @@ void main() { 'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentB.isDisplayedPDFIcon; + bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf'); expect(result, isTrue); }); @@ -56,7 +56,7 @@ void main() { 'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentC.isDisplayedPDFIcon; + bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx'); expect(result, isFalse); }); @@ -67,7 +67,7 @@ void main() { 'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentD.isDisplayedPDFIcon; + bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png'); expect(result, isFalse); }); @@ -78,7 +78,7 @@ void main() { 'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentE.isDisplayedPDFIcon; + bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf'); expect(result, isFalse); });