TF-105 [BUG] Fix loading is blinking when reading email

This commit is contained in:
dab246
2021-10-11 23:59:40 +07:00
committed by Dat H. Pham
parent ca28b55d24
commit e654fe2866
20 changed files with 359 additions and 213 deletions
@@ -1,4 +1,3 @@
import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -109,15 +108,7 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isGenerating) { if (_isGenerating) {
return Padding( return SizedBox.shrink();
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(color: AppColor.primaryColor)),
),
);
} }
if (widget.blockExternalImages != _wereExternalImagesBlocked) { if (widget.blockExternalImages != _wereExternalImagesBlocked) {
_generateHtml(widget.blockExternalImages); _generateHtml(widget.blockExternalImages);
@@ -10,7 +10,6 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:jmap_dart_client/jmap/core/id.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/mail/email/email.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_address.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
@@ -30,9 +29,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_inter
import 'package:tmail_ui_user/features/composer/domain/usecases/upload_mutiple_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_mutiple_attachment_interactor.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart'; import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_content_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.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_file_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_file_picker_interactor.dart';
@@ -157,19 +154,6 @@ class ComposerController extends BaseController {
return null; return null;
} }
Tuple3<List<MessageContent>, List<Attachment>, Session>? getContentEmailQuoted() {
if (composerArguments.value != null) {
final emailContent = composerArguments.value!.emailContent;
final session = composerArguments.value!.session;
if (emailContent != null) {
final messageContents = emailContent.getListMessageContent();
final attachmentsInline = emailContent.getListAttachmentInline();
return Tuple3(messageContents, attachmentsInline, session);
}
}
return null;
}
void expandEmailAddressAction() { void expandEmailAddressAction() {
final newExpandMode = expandMode.value == ExpandMode.COLLAPSE ? ExpandMode.EXPAND : ExpandMode.COLLAPSE; final newExpandMode = expandMode.value == ExpandMode.COLLAPSE ? ExpandMode.EXPAND : ExpandMode.COLLAPSE;
expandMode.value = newExpandMode; expandMode.value = newExpandMode;
@@ -229,7 +213,6 @@ class ComposerController extends BaseController {
String _getBodyEmailQuotedAsHtml(BuildContext context, HtmlMessagePurifier htmlMessagePurifier) { String _getBodyEmailQuotedAsHtml(BuildContext context, HtmlMessagePurifier htmlMessagePurifier) {
final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag()); final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag());
final contentEmail = getContentEmailQuoted();
final headerEmailQuotedAsHtml = headerEmailQuoted != null final headerEmailQuotedAsHtml = headerEmailQuoted != null
? AppLocalizations.of(context).header_email_quoted(headerEmailQuoted.value1, headerEmailQuoted.value2) ? AppLocalizations.of(context).header_email_quoted(headerEmailQuoted.value1, headerEmailQuoted.value2)
@@ -237,9 +220,12 @@ class ComposerController extends BaseController {
: ''; : '';
var trustAsHtml = ''; var trustAsHtml = '';
if (contentEmail != null && contentEmail.value1.isNotEmpty) {
final messageContent = contentEmail.value1.first; if (composerArguments.value != null) {
trustAsHtml = messageContent.content; final emailContents = composerArguments.value!.emailContents;
if (emailContents != null && emailContents.isNotEmpty) {
trustAsHtml = emailContents.first.content;
}
} }
final emailQuotedHtml = '</br></br></br>$headerEmailQuotedAsHtml${trustAsHtml.addBlockQuoteTag()}</br>'; final emailQuotedHtml = '</br></br></br>$headerEmailQuotedAsHtml${trustAsHtml.addBlockQuoteTag()}</br>';
@@ -281,7 +267,7 @@ class ComposerController extends BaseController {
Set<EmailBodyPart> _generateAttachments() { Set<EmailBodyPart> _generateAttachments() {
return attachments.map((attachment) => return attachments.map((attachment) =>
attachment.toEmailBodyPart(Attachment.dispositionAttachment)).toSet(); attachment.toEmailBodyPart(ContentDisposition.attachment.value)).toSet();
} }
void sendEmailAction(BuildContext context) async { void sendEmailAction(BuildContext context) async {
@@ -1,13 +1,14 @@
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:model/email/email_content.dart'; import 'package:model/model.dart';
class GetEmailContentSuccess extends UIState { class GetEmailContentSuccess extends UIState {
final EmailContent? emailContent; final List<EmailContent> emailContents;
final List<Attachment> attachments;
GetEmailContentSuccess(this.emailContent); GetEmailContentSuccess(this.emailContents, this.attachments);
@override @override
List<Object?> get props => [emailContent]; List<Object?> get props => [emailContents, attachments];
} }
class GetEmailContentFailure extends FeatureFailure { class GetEmailContentFailure extends FeatureFailure {
@@ -15,7 +15,9 @@ class GetEmailContentInteractor {
try { try {
yield Right<Failure, Success>(LoadingState()); yield Right<Failure, Success>(LoadingState());
final email = await emailRepository.getEmailContent(accountId, emailId); final email = await emailRepository.getEmailContent(accountId, emailId);
yield Right<Failure, Success>(GetEmailContentSuccess(email.toEmailContent())); yield Right<Failure, Success>(GetEmailContentSuccess(
email.emailContentList,
email.allAttachments));
} catch (e) { } catch (e) {
yield Left(GetEmailContentFailure(e)); yield Left(GetEmailContentFailure(e));
} }
@@ -26,6 +26,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/email/presentation/model/web_view_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart';
@@ -48,7 +49,8 @@ class EmailController extends BaseController {
final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; final emailAddressExpandMode = ExpandMode.COLLAPSE.obs;
final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; final attachmentsExpandMode = ExpandMode.COLLAPSE.obs;
final emailContent = Rxn<EmailContent>(); final emailContents = <EmailContent>[].obs;
final attachments = <Attachment>[].obs;
EmailId? _currentEmailId; EmailId? _currentEmailId;
EmailController( EmailController(
@@ -109,7 +111,7 @@ class EmailController extends BaseController {
}, },
(success) { (success) {
if (success is GetEmailContentSuccess) { if (success is GetEmailContentSuccess) {
emailContent.value = success.emailContent; _getEmailContentSuccess(success);
} else if (success is MarkAsEmailReadSuccess) { } else if (success is MarkAsEmailReadSuccess) {
_markAsEmailReadSuccess(success); _markAsEmailReadSuccess(success);
} else if (success is ExportAttachmentSuccess) { } else if (success is ExportAttachmentSuccess) {
@@ -126,10 +128,19 @@ class EmailController extends BaseController {
void onError(error) { void onError(error) {
} }
void _getEmailContentSuccess(GetEmailContentSuccess success) {
emailContents.value = success.emailContents;
attachments.value = success.attachments;
if (emailContents.isNotEmpty) {
dispatchState(Right(WebViewLoadingState()));
}
}
void _clearEmailContent() { void _clearEmailContent() {
toggleDisplayEmailAddressAction(expandMode: ExpandMode.COLLAPSE); toggleDisplayEmailAddressAction(expandMode: ExpandMode.COLLAPSE);
attachmentsExpandMode.value = ExpandMode.COLLAPSE; attachmentsExpandMode.value = ExpandMode.COLLAPSE;
emailContent.value = null; emailContents.clear();
attachments.clear();
} }
void toggleDisplayEmailAddressAction({required ExpandMode expandMode}) { void toggleDisplayEmailAddressAction({required ExpandMode expandMode}) {
@@ -150,13 +161,19 @@ class EmailController extends BaseController {
} }
mailboxDashBoardController.dispatchState(Right(success)); mailboxDashBoardController.dispatchState(Right(success));
if (success is MarkAsEmailReadSuccess && success.readActions == ReadActions.markAsUnread) { if (success is MarkAsEmailReadSuccess
&& success.readActions == ReadActions.markAsUnread
&& Get.context != null
&& !responsiveUtils.isDesktop(Get.context!)) {
backToThreadView(); backToThreadView();
} }
} }
void _markAsEmailReadFailure(Failure failure) { void _markAsEmailReadFailure(Failure failure) {
if (failure is MarkAsEmailReadFailure && failure.readActions == ReadActions.markAsUnread) { if (failure is MarkAsEmailReadFailure
&& failure.readActions == ReadActions.markAsUnread
&& Get.context != null
&& !responsiveUtils.isDesktop(Get.context!)) {
backToThreadView(); backToThreadView();
} }
} }
@@ -330,17 +347,25 @@ class EmailController extends BaseController {
&& mailboxDashBoardController.selectedEmail.value != null; && mailboxDashBoardController.selectedEmail.value != null;
void backToThreadView() { void backToThreadView() {
if (emailContents.isNotEmpty) {
dispatchState(Right(WebViewLoadCompletedState()));
}
popBack(); popBack();
} }
void pressEmailAction(EmailActionType emailActionType) { void pressEmailAction(EmailActionType emailActionType) {
if (canComposeEmail()) { if (canComposeEmail()) {
if (emailContents.isNotEmpty) {
clearState();
}
push( push(
AppRoutes.COMPOSER, AppRoutes.COMPOSER,
arguments: ComposerArguments( arguments: ComposerArguments(
emailActionType: emailActionType, emailActionType: emailActionType,
presentationEmail: mailboxDashBoardController.selectedEmail.value!, presentationEmail: mailboxDashBoardController.selectedEmail.value!,
emailContent: emailContent.value, emailContents: emailContents,
attachments: attachments,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role, mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role,
session: mailboxDashBoardController.sessionCurrent!, session: mailboxDashBoardController.sessionCurrent!,
userProfile: mailboxDashBoardController.userProfile.value!, userProfile: mailboxDashBoardController.userProfile.value!,
+31 -53
View File
@@ -8,13 +8,12 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
import 'package:tmail_ui_user/features/email/presentation/email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/email_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
import 'package:tmail_ui_user/features/email/presentation/model/web_view_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/web_view_state.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/app_bar_mail_widget_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/app_bar_mail_widget_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_content_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/list_attachment_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/list_attachment_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_content_place_holder_loading_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/sender_and_receiver_information_tile_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/sender_and_receiver_information_tile_builder.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
@@ -28,6 +27,11 @@ class EmailView extends GetView {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
emailController.viewState.value.map((success) {
if (success is WebViewLoadCompletedState && emailController.emailContents.isNotEmpty) {
emailController.dispatchState(Right(WebViewLoadingState()));
}
});
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
backgroundColor: AppColor.primaryLightColor, backgroundColor: AppColor.primaryLightColor,
@@ -41,7 +45,7 @@ class EmailView extends GetView {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_buildAppBar(context), _buildAppBar(context),
Expanded(child: _buildEmailContent(context)), Expanded(child: _buildEmailBody(context)),
_buildBottomBar(context), _buildBottomBar(context),
]) ])
) )
@@ -74,7 +78,7 @@ class EmailView extends GetView {
.build()); .build());
} }
Widget _buildEmailContent(BuildContext context) { Widget _buildEmailBody(BuildContext context) {
return Container( return Container(
color: AppColor.bgMessenger, color: AppColor.bgMessenger,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@@ -122,12 +126,7 @@ class EmailView extends GetView {
return Obx(() => emailController.viewState.value.fold( return Obx(() => emailController.viewState.value.fold(
(failure) => SizedBox.shrink(), (failure) => SizedBox.shrink(),
(success) => success is LoadingState || success is WebViewLoadingState (success) => success is LoadingState || success is WebViewLoadingState
? Center(child: Padding( ? EmailContentPlaceHolderLoading(responsiveUtils: responsiveUtils)
padding: EdgeInsets.only(top: 16, bottom: 16),
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(color: AppColor.primaryColor))))
: SizedBox.shrink())); : SizedBox.shrink()));
} }
@@ -150,29 +149,27 @@ class EmailView extends GetView {
emailController.emailAddressExpandMode.value) emailController.emailAddressExpandMode.value)
.onOpenExpandAddressReceiverActionClick(() => emailController.toggleDisplayEmailAddressAction(expandMode: ExpandMode.EXPAND)) .onOpenExpandAddressReceiverActionClick(() => emailController.toggleDisplayEmailAddressAction(expandMode: ExpandMode.EXPAND))
.build()), .build()),
_buildListAttachments(context),
_buildLoadingView(), _buildLoadingView(),
_buildListMessageContent(), _buildAttachments(context),
_buildEmailContent(),
], ],
) )
); );
} }
Widget _buildListAttachments(BuildContext context) { Widget _buildAttachments(BuildContext context) {
if (emailController.mailboxDashBoardController.selectedEmail.value?.hasAttachment == true) { return Obx(() => emailController.viewState.value.fold(
return Obx(() { (failure) => SizedBox.shrink(),
if (emailController.emailContent.value != null) { (success) {
final attachments = emailController.emailContent.value!.getListAttachment(); if (success is LoadingState || success is WebViewLoadingState) {
return attachments.isNotEmpty return SizedBox.shrink();
? _buildAttachmentsBody(context, attachments) } else {
: SizedBox.shrink(); final attachments = emailController.attachments.attachmentsWithDispositionAttachment;
} else { return attachments.isNotEmpty
return SizedBox.shrink(); ? _buildAttachmentsBody(context, attachments)
} : SizedBox.shrink();
}); }
} else { }));
return SizedBox.shrink();
}
} }
int _getAttachmentLimitDisplayed(BuildContext context) { int _getAttachmentLimitDisplayed(BuildContext context) {
@@ -281,31 +278,12 @@ class EmailView extends GetView {
); );
} }
Widget _buildListMessageContent() { Widget _buildEmailContent() {
return Obx(() { return Obx(() => emailController.emailContents.isNotEmpty
final messageContents = emailController.emailContent.value?.getListMessageContent(); ? HtmlContentViewer(
message: emailController.emailContents.first.content,
if (messageContents != null && messageContents.isNotEmpty) { onWebViewLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())),
return messageContents.first.textFormat == TextFormat.PLAIN mailtoDelegate: (uri) async {})
? Padding( : SizedBox.shrink());
padding: EdgeInsets.only(top: 16),
child: Text(
'${messageContents.first.content}',
style: TextStyle(fontSize: 12,
color: AppColor.mailboxTextColor)))
: HtmlContentViewer(
message: messageContents.first.content,
onWebViewLoadStart: (webController, uri) {
emailController.dispatchState(Right(WebViewLoadingState()));
},
onWebViewLoadStop: (webController, uri) {
emailController.clearState();
},
mailtoDelegate: (uri) async {},
);
} else {
return SizedBox.shrink();
}
});
} }
} }
@@ -1,47 +0,0 @@
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
extension EmailContentExtension on EmailContent {
List<MessageContent> getListMessageContent() {
Map<PartId, TextFormat> mapHtmlBody = Map();
List<MessageContent> listMessageContent = [];
htmlBody?.forEach((element) {
if (element.partId != null && element.type != null) {
mapHtmlBody[element.partId!] = element.type!.mimeType == EmailConstants.HTML_TEXT
? TextFormat.HTML
: TextFormat.PLAIN;
}
});
bodyValues?.forEach((key, value) {
listMessageContent.add(MessageContent(mapHtmlBody[key] ?? TextFormat.PLAIN, value.value));
});
return listMessageContent;
}
List<Attachment> getListAttachment() {
if (attachments != null) {
return attachments!
.where((element) => (element.disposition != null && element.disposition != 'inline'))
.map((item) => item.toAttachment())
.toList();
}
return [];
}
List<Attachment> getListAttachmentInline() {
if (attachments != null) {
return attachments!
.where((element) => element.disposition == 'inline')
.map((item) => item.toAttachment())
.toList();
}
return [];
}
}
@@ -11,4 +11,12 @@ extension ListAttachmentExtension on List<Attachment> {
}); });
return totalSize; return totalSize;
} }
List<Attachment> get attachmentsWithDispositionAttachment {
return where((element) => element.disposition == ContentDisposition.attachment).toList();
}
List<Attachment> get attachmentWithDispositionInlines {
return where((element) => element.disposition == ContentDisposition.inline).toList();
}
} }
@@ -7,7 +7,8 @@ import 'package:model/model.dart';
class ComposerArguments with EquatableMixin { class ComposerArguments with EquatableMixin {
final EmailActionType emailActionType; final EmailActionType emailActionType;
final PresentationEmail? presentationEmail; final PresentationEmail? presentationEmail;
final EmailContent? emailContent; final List<EmailContent>? emailContents;
final List<Attachment>? attachments;
final Session session; final Session session;
final UserProfile userProfile; final UserProfile userProfile;
final Map<Role, MailboxId> mapMailboxId; final Map<Role, MailboxId> mapMailboxId;
@@ -16,7 +17,8 @@ class ComposerArguments with EquatableMixin {
ComposerArguments({ ComposerArguments({
this.emailActionType = EmailActionType.compose, this.emailActionType = EmailActionType.compose,
this.presentationEmail, this.presentationEmail,
this.emailContent, this.emailContents,
this.attachments,
this.mailboxRole, this.mailboxRole,
required this.session, required this.session,
required this.userProfile, required this.userProfile,
@@ -27,7 +29,8 @@ class ComposerArguments with EquatableMixin {
List<Object?> get props => [ List<Object?> get props => [
emailActionType, emailActionType,
presentationEmail, presentationEmail,
emailContent, emailContents,
attachments,
mailboxRole, mailboxRole,
session, session,
userProfile, userProfile,
@@ -1,29 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
import 'package:model/model.dart';
class MessageContent with EquatableMixin {
final TextFormat textFormat;
final String content;
MessageContent(this.textFormat, this.content);
bool hasImageInlineWithCid() => content.contains('cid:');
String getContentHasInlineAttachment(String baseDownloadUrl, AccountId accountId, List<Attachment> attachments) {
var contentValid = content;
attachments.forEach((attachment) {
if(attachment.cid != null) {
final urlDownloadImage = attachment.getDownloadUrl(baseDownloadUrl, accountId);
contentValid = content.replaceAll('cid:${attachment.cid}', '$urlDownloadImage');
}
});
return contentValid;
}
@override
List<Object?> get props => [textFormat, content];
}
@@ -1,5 +0,0 @@
enum TextFormat {
HTML,
PLAIN
}
@@ -7,3 +7,10 @@ class WebViewLoadingState extends UIState {
@override @override
List<Object?> get props => []; List<Object?> get props => [];
} }
class WebViewLoadCompletedState extends UIState {
WebViewLoadCompletedState();
@override
List<Object?> get props => [];
}
@@ -0,0 +1,145 @@
import 'package:core/core.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
const List<Color> _defaultColors = [Color.fromRGBO(0, 0, 0, 0.1), Color(0x44CCCCCC), Color.fromRGBO(0, 0, 0, 0.1)];
Decoration _radiusBoxDecoration({
required Animation animation,
bool hasCustomColors = false,
AlignmentGeometry beginAlign = Alignment.topLeft,
AlignmentGeometry endAlign = Alignment.bottomRight,
List<Color> colors = _defaultColors,
double radius = 10.0
}) {
return BoxDecoration(
borderRadius: BorderRadius.circular(radius),
shape: BoxShape.rectangle,
gradient: LinearGradient(
begin: beginAlign,
end: endAlign,
colors: hasCustomColors
? colors.map((color) {
return color;
}).toList()
: [
Color.fromRGBO(0, 0, 0, 0.1),
Color(0x32E5E5E5),
Color.fromRGBO(0, 0, 0, 0.1),
],
stops: [animation.value - 2, animation.value, animation.value + 1]));
}
class EmailContentPlaceHolderLoading extends StatefulWidget {
final ResponsiveUtils responsiveUtils;
const EmailContentPlaceHolderLoading({
Key? key,
required this.responsiveUtils,
}) : super(key: key);
@override
_EmailContentPlaceHolderLoadingState createState() => _EmailContentPlaceHolderLoadingState();
}
class _EmailContentPlaceHolderLoadingState extends State<EmailContentPlaceHolderLoading> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(duration: Duration(seconds: 1), vsync: this)..repeat();
_animation = Tween<double>(begin: -2, end: 2)
.animate(CurvedAnimation(curve: Curves.easeInOutSine, parent: _animationController));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget? child) {
return Container(
margin: EdgeInsets.only(top: 20),
padding: EdgeInsets.zero,
color: Colors.transparent,
child: Column(
children: [
Row(children: [
_placeHolderAttachment(context),
SizedBox(width: 16),
_placeHolderAttachment(context),
if (!widget.responsiveUtils.isMobile(context))
SizedBox(width: 16),
if (!widget.responsiveUtils.isMobile(context))
_placeHolderAttachment(context),
if (widget.responsiveUtils.isTablet(context))
SizedBox(width: 16),
if (widget.responsiveUtils.isTablet(context))
_placeHolderAttachment(context),
]),
SizedBox(height: 16),
Container(
margin: EdgeInsets.only(right: 64),
height: 10,
decoration: _radiusBoxDecoration(animation: _animation, radius: 5)),
SizedBox(height: 16),
Container(
height: 10,
decoration: _radiusBoxDecoration(animation: _animation, radius: 5)),
SizedBox(height: 16),
Container(
margin: EdgeInsets.only(right: 128),
height: 10,
decoration: _radiusBoxDecoration(animation: _animation, radius: 5)),
],
),
);
},
);
}
Widget _placeHolderAttachment(BuildContext context) {
double width = MediaQuery.of(context).size.width;
final percentAttachment = widget.responsiveUtils.isMobile(context)
? 0.4
: widget.responsiveUtils.isTablet(context) ? 0.22 : 0.14;
return Container(
height: 55,
width: width * percentAttachment,
margin: EdgeInsets.zero,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16),
decoration: _radiusBoxDecoration(animation: _animation),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 24,
width: 24,
decoration: _radiusBoxDecoration(animation: _animation, radius: 12)),
SizedBox(width: 8),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 5,
decoration: _radiusBoxDecoration(animation: _animation)),
SizedBox(height: 10),
Container(
height: 5,
decoration: _radiusBoxDecoration(animation: _animation))
],
))
]
)
);
}
}
+36 -3
View File
@@ -9,15 +9,13 @@ import 'package:uri/uri.dart';
class Attachment with EquatableMixin { class Attachment with EquatableMixin {
static final String dispositionAttachment = 'attachment';
final PartId? partId; final PartId? partId;
final Id? blobId; final Id? blobId;
final UnsignedInt? size; final UnsignedInt? size;
final String? name; final String? name;
final MediaType? type; final MediaType? type;
final String? cid; final String? cid;
final String? disposition; final ContentDisposition? disposition;
Attachment({ Attachment({
this.partId, this.partId,
@@ -43,3 +41,38 @@ class Attachment with EquatableMixin {
@override @override
List<Object?> get props => [partId, blobId, size, name, type, cid, disposition]; List<Object?> get props => [partId, blobId, size, name, type, cid, disposition];
} }
enum ContentDisposition {
inline,
attachment,
other
}
extension ContentDispositionExtension on ContentDisposition {
String get value {
switch(this) {
case ContentDisposition.inline:
return 'inline';
case ContentDisposition.attachment:
return 'attachment';
case ContentDisposition.other:
return this.toString();
}
}
}
extension DispositionStringExtension on String? {
ContentDisposition? toContentDisposition() {
if (this != null) {
switch(this) {
case 'inline':
return ContentDisposition.inline;
case 'attachment':
return ContentDisposition.attachment;
default:
return ContentDisposition.other;
}
}
return null;
}
}
+20 -21
View File
@@ -1,30 +1,29 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/email_content_type.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; import 'package:model/model.dart';
class EmailContent with EquatableMixin { class EmailContent with EquatableMixin {
final EmailId id; final String content;
final Set<EmailBodyPart>? htmlBody; final EmailContentType type;
final Set<EmailBodyPart>? attachments;
final Map<PartId, EmailBodyValue>? bodyValues;
EmailContent( EmailContent(this.type, this.content);
this.id,
{ bool hasImageInlineWithCid() => content.contains('cid:');
this.htmlBody,
this.attachments, String getContentHasInlineAttachment(String baseDownloadUrl, AccountId accountId, List<Attachment> attachments) {
this.bodyValues var contentValid = content;
} attachments.forEach((attachment) {
); if(attachment.cid != null) {
final urlDownloadImage = attachment.getDownloadUrl(baseDownloadUrl, accountId);
contentValid = content.replaceAll('cid:${attachment.cid}', '$urlDownloadImage');
}
});
return contentValid;
}
@override @override
List<Object?> get props => [ List<Object?> get props => [type, content];
id,
htmlBody,
attachments,
bodyValues
];
} }
+6
View File
@@ -0,0 +1,6 @@
enum EmailContentType {
textHtml,
textPlain,
other
}
@@ -9,5 +9,5 @@ extension EmailBodyPartExtension on EmailBodyPart {
name: name, name: name,
type: type, type: type,
cid: cid, cid: cid,
disposition: disposition); disposition: disposition.toContentDisposition());
} }
+30 -9
View File
@@ -1,6 +1,8 @@
import 'package:http_parser/http_parser.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.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:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:model/email/email_property.dart'; import 'package:model/email/email_property.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
@@ -22,15 +24,6 @@ extension EmailExtension on Email {
return listEmailAddress; return listEmailAddress;
} }
EmailContent toEmailContent() {
return EmailContent(
id,
htmlBody: htmlBody,
attachments: attachments,
bodyValues: bodyValues
);
}
Email updatedEmail({Map<KeyWordIdentifier, bool>? newKeywords}) { Email updatedEmail({Map<KeyWordIdentifier, bool>? newKeywords}) {
return Email( return Email(
id, id,
@@ -86,4 +79,32 @@ extension EmailExtension on Email {
mailboxIds: updatedProperties.contain(EmailProperty.mailboxIds) ? newEmail.mailboxIds : mailboxIds, mailboxIds: updatedProperties.contain(EmailProperty.mailboxIds) ? newEmail.mailboxIds : mailboxIds,
); );
} }
List<EmailContent> get emailContentList {
final newHtmlBody = htmlBody
?.where((emailBody) => emailBody.partId != null && emailBody.type != null)
.toList() ?? <EmailBodyPart>[];
final mapHtmlBody = Map<PartId, MediaType>.fromIterable(
newHtmlBody,
key: (emailBody) => emailBody.partId!,
value: (emailBody) => emailBody.type!,
);
final emailContents = bodyValues?.entries
.map((entries) => EmailContent(mapHtmlBody[entries.key].toEmailContentType(), entries.value.value))
.toList();
return emailContents ?? [];
}
List<Attachment> get allAttachments {
if (attachments != null) {
return attachments!
.where((element) => element.disposition != null)
.map((item) => item.toAttachment())
.toList();
}
return [];
}
} }
@@ -0,0 +1,20 @@
import 'package:http_parser/http_parser.dart';
import 'package:model/email/email_content_type.dart';
extension MediaTypeExtension on MediaType? {
EmailContentType toEmailContentType() {
if (this == null) {
return EmailContentType.textHtml;
} else {
switch(this!.mimeType) {
case 'text/html':
return EmailContentType.textHtml;
case 'text/plain':
return EmailContentType.textPlain;
default:
return EmailContentType.other;
}
}
}
}
+2
View File
@@ -29,6 +29,7 @@ export 'email/move_action.dart';
export 'email/mark_star_action.dart'; export 'email/mark_star_action.dart';
export 'email/email_property.dart'; export 'email/email_property.dart';
export 'email/email_filter.dart'; export 'email/email_filter.dart';
export 'email/email_content_type.dart';
// Extensions // Extensions
export 'extensions/email_address_extension.dart'; export 'extensions/email_address_extension.dart';
@@ -50,6 +51,7 @@ export 'extensions/properties_extension.dart';
export 'extensions/list_mailbox_extension.dart'; export 'extensions/list_mailbox_extension.dart';
export 'extensions/list_extension.dart'; export 'extensions/list_extension.dart';
export 'extensions/list_email_extension.dart'; export 'extensions/list_email_extension.dart';
export 'extensions/media_type_extension.dart';
// Download // Download
export 'download/download_task_id.dart'; export 'download/download_task_id.dart';