From 91f3275a54eb231026b46d716b42fa8fd1194a8e Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 14 Jan 2025 02:52:18 +0700 Subject: [PATCH] TF-3416 Handle download attachment in EML previewer on mobile --- core/lib/data/constants/constant.dart | 3 + .../html_content_viewer_widget.dart | 15 ++++- core/lib/utils/preview_eml_file_utils.dart | 2 - .../email_datasource_impl.dart | 23 +------ .../controller/single_email_controller.dart | 14 ++++- .../extensions/attachment_extension.dart | 27 ++++++++ .../email/presentation/utils/email_utils.dart | 24 +++++++ .../email_previewer_dialog_view.dart | 3 + .../email/parsing_attachment_by_uri_test.dart | 63 +++++++++++++++++++ 9 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 test/features/email/parsing_attachment_by_uri_test.dart diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index 93968f5ad..79d26de33 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -10,4 +10,7 @@ class Constant { static const textVCardMimeType = 'text/x-vcard'; static const textPlainMimeType = 'text/plain'; static const emlMimeType = 'message/rfc822'; + static const mailtoScheme = 'mailto'; + static const attachmentScheme = 'attachment'; + static const emlPreviewerScheme = 'eml-previewer'; } \ No newline at end of file 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 00bff2d25..dbf837445 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 @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/html/html_interaction.dart'; import 'package:core/utils/html/html_utils.dart'; import 'package:core/utils/platform_info.dart'; -import 'package:core/utils/preview_eml_file_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -18,6 +18,7 @@ typedef OnScrollHorizontalEndAction = Function(bool leftDirection); typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated); typedef OnMailtoDelegateAction = Future Function(Uri? uri); typedef OnPreviewEMLDelegateAction = Future Function(Uri? uri); +typedef OnDownloadAttachmentDelegateAction = Future Function(Uri? uri); class HtmlContentViewer extends StatefulWidget { @@ -29,6 +30,7 @@ class HtmlContentViewer extends StatefulWidget { final OnMailtoDelegateAction? onMailtoDelegateAction; final OnScrollHorizontalEndAction? onScrollHorizontalEnd; final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction; + final OnDownloadAttachmentDelegateAction? onDownloadAttachmentDelegateAction; const HtmlContentViewer({ Key? key, @@ -39,6 +41,7 @@ class HtmlContentViewer extends StatefulWidget { this.onMailtoDelegateAction, this.onScrollHorizontalEnd, this.onPreviewEMLDelegateAction, + this.onDownloadAttachmentDelegateAction, }) : super(key: key); @override @@ -261,17 +264,23 @@ class _HtmlContentViewState extends State { final requestUri = Uri.parse(url); if (widget.onMailtoDelegateAction != null && - requestUri.isScheme('mailto')) { + requestUri.isScheme(Constant.mailtoScheme)) { await widget.onMailtoDelegateAction?.call(requestUri); return NavigationActionPolicy.CANCEL; } if (widget.onPreviewEMLDelegateAction != null && - requestUri.isScheme(PreviewEmlFileUtils.emlPreviewerScheme)) { + requestUri.isScheme(Constant.emlPreviewerScheme)) { await widget.onPreviewEMLDelegateAction?.call(requestUri); return NavigationActionPolicy.CANCEL; } + if (widget.onDownloadAttachmentDelegateAction != null && + requestUri.isScheme(Constant.attachmentScheme)) { + await widget.onDownloadAttachmentDelegateAction?.call(requestUri); + return NavigationActionPolicy.CANCEL; + } + if (await launcher.canLaunchUrl(Uri.parse(url))) { await launcher.launchUrl( Uri.parse(url), diff --git a/core/lib/utils/preview_eml_file_utils.dart b/core/lib/utils/preview_eml_file_utils.dart index 9a4bafab8..794ecfdbe 100644 --- a/core/lib/utils/preview_eml_file_utils.dart +++ b/core/lib/utils/preview_eml_file_utils.dart @@ -7,8 +7,6 @@ import 'package:html/dom.dart'; import 'package:html/parser.dart'; class PreviewEmlFileUtils { - static const String emlPreviewerScheme = 'eml-previewer'; - Element? _createEmailElement({ required String subjectPrefix, required String fromPrefix, 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 fa9d7c060..93b389ba5 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -42,8 +42,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart'; -import 'package:tmail_ui_user/main/routes/app_routes.dart'; -import 'package:tmail_ui_user/main/routes/route_utils.dart'; class EmailDataSourceImpl extends EmailDataSource { @@ -452,15 +450,11 @@ class EmailDataSourceImpl extends EmailDataSource { final iconBase64Data = await _fileUtils.convertImageAssetToBase64( attachment.getIcon(_imagePaths)); - final link = attachment.isEMLFile - ? _generateEMLAttachmentLink(attachment.blobId?.value) - : null; - final previewAttachment = PreviewAttachment( iconBase64Data: iconBase64Data, name: attachment.name.escapeLtGtHtmlString(), size: filesize(attachment.size?.value), - link: link, + link: attachment.hyperLink, ); listPreviewAttachment.add(previewAttachment); @@ -498,21 +492,6 @@ class EmailDataSourceImpl extends EmailDataSource { }).catchError(_exceptionThrower.throwException); } - String _generateEMLAttachmentLink(String? blobId) { - if (blobId == null) return ''; - - if (PlatformInfo.isWeb) { - return RouteUtils.createUrlWebLocationBar( - AppRoutes.emailEMLPreviewer, - previewId: blobId, - ).toString(); - } else if (PlatformInfo.isMobile) { - return '${PreviewEmlFileUtils.emlPreviewerScheme}://$blobId'; - } else { - return ''; - } - } - Future _transformEmailContent( List emailContents, Map mapCidImageDownloadUrl, diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index d1bb82d91..a3cf64d22 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -2059,6 +2059,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emlPreviewer: emlPreviewer, onMailtoDelegateAction: openMailToLink, onPreviewEMLDelegateAction: (uri) => _openEMLPreviewer(context, uri), + onDownloadAttachmentDelegateAction: (uri) => + _downloadAttachmentInEMLPreview(context, uri), ), ); }, @@ -2069,13 +2071,23 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::_openEMLPreviewer:uri = $uri'); if (uri == null) return; - final blobId = uri.authority; + final blobId = uri.path; log('SingleEmailController::_openEMLPreviewer:blobId = $blobId'); if (blobId.isEmpty) return; previewEMLFileAction(Id(blobId), AppLocalizations.of(context)); } + Future _downloadAttachmentInEMLPreview(BuildContext context, Uri? uri) async { + log('SingleEmailController::_downloadAttachmentInEMLPreview:uri = $uri'); + if (uri == null) return; + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + if (attachment == null) return; + + handleDownloadAttachmentAction(context, attachment); + } + void handleMailToAttendees(CalendarOrganizer? organizer, List? attendees) { final listEmailAddressAttendees = attendees ?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value)) diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index e631581aa..584494c8e 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,6 +1,10 @@ +import 'package:core/data/constants/constant.dart'; import 'package:core/presentation/extensions/media_type_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; extension AttachmentExtension on Attachment { String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; @@ -8,4 +12,27 @@ extension AttachmentExtension on Attachment { bool get isPDFFile => type?.isPDFFile(fileName: name) ?? false; bool get isEMLFile => type?.isEMLFile ?? false; + + String get hyperLink => isEMLFile ? emlLink : attachmentLink; + + String get emlLink { + if (blobId == null) return ''; + + if (PlatformInfo.isWeb) { + return RouteUtils.createUrlWebLocationBar( + AppRoutes.emailEMLPreviewer, + previewId: blobId!.value, + ).toString(); + } else if (PlatformInfo.isMobile) { + return '${Constant.emlPreviewerScheme}:${blobId!.value}'; + } else { + return ''; + } + } + + String get attachmentLink { + if (blobId == null) return ''; + + return '${Constant.attachmentScheme}:${blobId!.value}?name=${name ?? ''}&size=${size?.value ?? ''}&type=${type?.mimeType ?? ''}'; + } } \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 17b8e648b..e4eedf8ef 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -5,11 +5,15 @@ 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:dartz/dartz.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.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/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; @@ -190,4 +194,24 @@ class EmailUtils { final mailtoLinks = extractMailtoLinksFromListPost(listPost); return extractRecipientsFromListMailtoLink(mailtoLinks); } + + static Attachment? parsingAttachmentByUri(Uri uri) { + try { + final blobId = uri.path; + final queryParams = uri.queryParameters; + final name = queryParams['name']; + final size = queryParams['size']; + final type = queryParams['type']; + log('EmailUtils::parsingAttachmentByUri:blobId = $blobId | name = $name | size = $size | type = $type'); + return Attachment( + blobId: Id(blobId), + name: name, + size: size?.isNotEmpty == true ? UnsignedInt(int.parse(size!)) : null, + type: type?.isNotEmpty == true ? MediaType.parse(type!) : null, + ); + } catch (e) { + logError('EmailUtils::parsingAttachmentByUri:Exception = $e:'); + return null; + } + } } \ No newline at end of file diff --git a/lib/features/email_previewer/email_previewer_dialog_view.dart b/lib/features/email_previewer/email_previewer_dialog_view.dart index 85c482fa8..483f59c4c 100644 --- a/lib/features/email_previewer/email_previewer_dialog_view.dart +++ b/lib/features/email_previewer/email_previewer_dialog_view.dart @@ -10,12 +10,14 @@ class EmailPreviewerDialogView extends StatelessWidget { final EMLPreviewer emlPreviewer; final OnMailtoDelegateAction onMailtoDelegateAction; final OnPreviewEMLDelegateAction onPreviewEMLDelegateAction; + final OnDownloadAttachmentDelegateAction onDownloadAttachmentDelegateAction; const EmailPreviewerDialogView({ super.key, required this.emlPreviewer, required this.onMailtoDelegateAction, required this.onPreviewEMLDelegateAction, + required this.onDownloadAttachmentDelegateAction, }); @override @@ -29,6 +31,7 @@ class EmailPreviewerDialogView extends StatelessWidget { direction: AppUtils.getCurrentDirection(context), onMailtoDelegateAction: onMailtoDelegateAction, onPreviewEMLDelegateAction: onPreviewEMLDelegateAction, + onDownloadAttachmentDelegateAction: onDownloadAttachmentDelegateAction, ), ), ); diff --git a/test/features/email/parsing_attachment_by_uri_test.dart b/test/features/email/parsing_attachment_by_uri_test.dart new file mode 100644 index 000000000..c87a31882 --- /dev/null +++ b/test/features/email/parsing_attachment_by_uri_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +void main() { + group('parsingAttachmentByUri', () { + test('should parse a valid URI and return an Attachment object', () { + final uri = Uri.parse( + 'attachment:blobId?name=testfile.txt&size=1024&type=text/plain', + ); + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + + expect(attachment, isNotNull); + expect(attachment!.blobId?.value, 'blobId'); + expect(attachment.name, 'testfile.txt'); + expect(attachment.size?.value, 1024); + expect(attachment.type?.toString(), 'text/plain'); + }); + + test('should return null for an invalid URI', () { + final uri = Uri.parse('https://example.com'); + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + + expect(attachment, isNull); + }); + + test('should handle missing optional parameters gracefully', () { + final uri = Uri.parse('attachment:blobId?name=testfile.txt'); + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + + expect(attachment, isNotNull); + expect(attachment!.blobId?.value, 'blobId'); + expect(attachment.name, 'testfile.txt'); + expect(attachment.size, isNull); + expect(attachment.type, isNull); + }); + + test('should handle invalid size parameter gracefully', () { + final uri = Uri.parse( + 'attachment:blobId?name=testfile.txt&size=invalid', + ); + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + + expect(attachment, isNull); + }); + + test('should handle empty type parameter gracefully', () { + final uri = Uri.parse( + 'attachment:blobId?name=testfile.txt&type=', + ); + + final attachment = EmailUtils.parsingAttachmentByUri(uri); + + expect(attachment, isNotNull); + expect(attachment!.blobId?.value, 'blobId'); + expect(attachment.name, 'testfile.txt'); + expect(attachment.type, isNull); + }); + }); +}