diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ff2873629..ec63a3110 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.linagora.android.tmail"> + @@ -17,7 +18,7 @@ + + + + + + Team Mail + Started + In Progress + Canceled + Failed + Completed + Paused + \ No newline at end of file diff --git a/lib/features/email/presentation/email_bindings.dart b/lib/features/email/presentation/email_bindings.dart index 574067c74..6b156840c 100644 --- a/lib/features/email/presentation/email_bindings.dart +++ b/lib/features/email/presentation/email_bindings.dart @@ -1,12 +1,17 @@ +import 'package:core/core.dart'; import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/email_controller.dart'; +import 'package:tmail_ui_user/features/login/data/repository/credential_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; class EmailBindings extends Bindings { @override @@ -20,5 +25,14 @@ class EmailBindings extends Bindings { Get.put(EmailController( Get.find(), Get.find())); + Get.lazyPut(() => CredentialRepositoryImpl(Get.find())); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => DownloadAttachmentsInteractor( + Get.find(), + Get.find())); + Get.put(EmailController( + Get.find(), + Get.find(), + Get.find())); } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 8218e602c..d4bf7a3d0 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -1,16 +1,21 @@ +import 'dart:io'; + import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_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/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.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/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -21,11 +26,18 @@ class EmailController extends BaseController { final GetEmailContentInteractor _getEmailContentInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; + final DownloadAttachmentsInteractor _downloadAttachmentsInteractor; + final DeviceManager _deviceManager; final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; EmailContent? emailContent; EmailController(this._getEmailContentInteractor, this._markAsEmailReadInteractor); + EmailController( + this._getEmailContentInteractor, + this._downloadAttachmentsInteractor, + this._deviceManager + ); @override void onReady() { @@ -102,6 +114,46 @@ class EmailController extends BaseController { backToThreadView(); } + void downloadAttachments(BuildContext context, List attachments) async { + final needRequestPermission = await _deviceManager.isNeedRequestStoragePermissionOnAndroid(); + + if (Platform.isAndroid && needRequestPermission) { + final status = await Permission.storage.status; + switch (status) { + case PermissionStatus.granted: + _downloadAttachmentsAction(context, attachments); + break; + case PermissionStatus.permanentlyDenied: + _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); + break; + default: { + final requested = await Permission.storage.request(); + switch (requested) { + case PermissionStatus.granted: + _downloadAttachmentsAction(context, attachments); + break; + default: + _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); + break; + } + } + } + } else { + _downloadAttachmentsAction(context, attachments); + } + } + + void _downloadAttachmentsAction(BuildContext context, List attachments) async { + final accountId = mailboxDashBoardController.accountId.value; + if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); + await _downloadAttachmentsInteractor.execute(attachments, accountId, baseDownloadUrl) + .then((result) => result.fold( + (failure) => AppLocalizations.of(context).attachment_download_failed, + (success) => AppLocalizations.of(context).attachment_download_successfully)); + } + } + bool canComposeEmail() => mailboxDashBoardController.sessionCurrent != null && mailboxDashBoardController.userProfile.value != null && mailboxDashBoardController.mapMailboxId.containsKey(PresentationMailbox.roleOutbox) diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 0b0fce7e8..63b87603e 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -141,9 +141,8 @@ class EmailView extends GetWidget { .onOpenExpandAddressReceiverActionClick(() => emailController.toggleDisplayEmailAddressAction(expandMode: ExpandMode.EXPAND)) .build()), _buildLoadingView(), - SizedBox(height: 16), - _buildListAttachments(context), _buildListMessageContent(), + _buildListAttachments(context), ], ) ); @@ -161,14 +160,18 @@ class EmailView extends GetWidget { key: Key('list_attachment'), primary: false, shrinkWrap: true, - padding: EdgeInsets.only(bottom: 16), + padding: EdgeInsets.only(top: 16), itemCount: attachments.length, gridDelegate: SliverGridDelegateFixedHeight( height: 60, crossAxisCount: responsiveUtils.isMobile(context) ? 2 : 4, crossAxisSpacing: 16.0, mainAxisSpacing: 8.0), - itemBuilder: (context, index) => AttachmentFileTileBuilder(imagePaths, attachments[index]).build()) + itemBuilder: (context, index) => + (AttachmentFileTileBuilder(imagePaths, attachments[index]) + ..onDownloadAttachmentFileActionClick((attachment) => + emailController.downloadAttachments(context, [attachment]))) + .build()) : SizedBox.shrink(); } else { return SizedBox.shrink(); @@ -191,6 +194,7 @@ class EmailView extends GetWidget { ? ListView.builder( primary: false, shrinkWrap: true, + padding: EdgeInsets.only(top: 16), key: Key('list_message_content'), itemCount: messageContents.length, itemBuilder: (context, index) => diff --git a/lib/features/email/presentation/extensions/email_body_part_extension.dart b/lib/features/email/presentation/extensions/email_body_part_extension.dart index 591ad61a5..0adda96d1 100644 --- a/lib/features/email/presentation/extensions/email_body_part_extension.dart +++ b/lib/features/email/presentation/extensions/email_body_part_extension.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart'; +import 'package:model/model.dart'; extension EmailBodyPartExtension on EmailBodyPart { - AttachmentFile toAttachmentFile() => AttachmentFile(partId, blobId, size, name, type, cid); + Attachment toAttachment() => Attachment(partId, blobId, size, name, type, cid); } diff --git a/lib/features/email/presentation/extensions/email_content_extension.dart b/lib/features/email/presentation/extensions/email_content_extension.dart index c60e55a80..68214dd02 100644 --- a/lib/features/email/presentation/extensions/email_content_extension.dart +++ b/lib/features/email/presentation/extensions/email_content_extension.dart @@ -1,7 +1,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/email_content.dart'; import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.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'; import 'package:tmail_ui_user/features/email/presentation/extensions/email_body_part_extension.dart'; @@ -27,21 +27,21 @@ extension EmailContentExtension on EmailContent { return listMessageContent; } - List getListAttachment() { + List getListAttachment() { if (attachments != null) { return attachments! .where((element) => (element.disposition != null && element.disposition != 'inline')) - .map((item) => item.toAttachmentFile()) + .map((item) => item.toAttachment()) .toList(); } return []; } - List getListAttachmentInline() { + List getListAttachmentInline() { if (attachments != null) { return attachments! .where((element) => element.disposition == 'inline') - .map((item) => item.toAttachmentFile()) + .map((item) => item.toAttachment()) .toList(); } return []; diff --git a/lib/features/email/presentation/model/attachment_file.dart b/lib/features/email/presentation/model/attachment_file.dart deleted file mode 100644 index c394da3ed..000000000 --- a/lib/features/email/presentation/model/attachment_file.dart +++ /dev/null @@ -1,28 +0,0 @@ - -import 'package:equatable/equatable.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; - -class AttachmentFile with EquatableMixin { - - final PartId? partId; - final Id? blobId; - final UnsignedInt? size; - final String? name; - final MediaType? type; - final String? cid; - - AttachmentFile( - this.partId, - this.blobId, - this.size, - this.name, - this.type, - this.cid - ); - - @override - List get props => [partId, blobId, size, name, type, cid]; -} \ No newline at end of file diff --git a/lib/features/email/presentation/model/message_content.dart b/lib/features/email/presentation/model/message_content.dart index 957cae630..831fe5948 100644 --- a/lib/features/email/presentation/model/message_content.dart +++ b/lib/features/email/presentation/model/message_content.dart @@ -1,8 +1,6 @@ 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:tmail_ui_user/features/email/presentation/model/attachment_file.dart'; import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart'; import 'package:model/model.dart'; @@ -15,16 +13,11 @@ class MessageContent with EquatableMixin { bool hasImageInlineWithCid() => content.contains('cid:'); - String getContentHasInlineAttachment(Session session, AccountId accountId, List attachmentFiles) { + String getContentHasInlineAttachment(String baseDownloadUrl, AccountId accountId, List attachments) { var contentValid = content; - attachmentFiles.forEach((attachment) { + attachments.forEach((attachment) { if(attachment.cid != null) { - final urlDownloadImage = session.getDownloadUrl( - '${accountId.id.value}', - '${attachment.blobId?.value}', - '${attachment.name}', - '${attachment.type?.mimeType}'); - + final urlDownloadImage = attachment.getDownloadUrl(baseDownloadUrl, accountId); contentValid = content.replaceAll('cid:${attachment.cid}', '$urlDownloadImage'); } }); diff --git a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart b/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart index 2d560465b..934c55e83 100644 --- a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart @@ -4,23 +4,21 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart'; +import 'package:model/model.dart'; -typedef OnOpenAttachmentFileActionClick = void Function(); +typedef OnDownloadAttachmentFileActionClick = void Function(Attachment attachment); class AttachmentFileTileBuilder { final ImagePaths _imagePaths; - final AttachmentFile _attachmentFile; + final Attachment _attachment; - OnOpenAttachmentFileActionClick? _onOpenAttachmentFileActionClick; + OnDownloadAttachmentFileActionClick? _onDownloadAttachmentFileActionClick; - AttachmentFileTileBuilder(this._imagePaths, this._attachmentFile); + AttachmentFileTileBuilder(this._imagePaths, this._attachment); - AttachmentFileTileBuilder onOpenAttachmentFileActionClick( - OnOpenAttachmentFileActionClick onOpenAttachmentFileActionClick) { - _onOpenAttachmentFileActionClick = onOpenAttachmentFileActionClick; - return this; + void onDownloadAttachmentFileActionClick(OnDownloadAttachmentFileActionClick onDownloadAttachmentFileActionClick) { + _onDownloadAttachmentFileActionClick = onDownloadAttachmentFileActionClick; } Widget build() { @@ -38,26 +36,29 @@ class AttachmentFileTileBuilder { child: MediaQuery( data: MediaQueryData(padding: EdgeInsets.zero), child: ListTile( + contentPadding: EdgeInsets.zero, + focusColor: AppColor.primaryColor, + hoverColor: AppColor.primaryColor, onTap: () { - if (_onOpenAttachmentFileActionClick != null) { - _onOpenAttachmentFileActionClick!(); + if (_onDownloadAttachmentFileActionClick != null) { + _onDownloadAttachmentFileActionClick!(_attachment); }}, leading: Transform( - transform: Matrix4.translationValues(0.0, 2.0, 0.0), + transform: Matrix4.translationValues(14.0, 2.0, 0.0), child: SvgPicture.asset(_imagePaths.icAttachmentFile, width: 24, height: 24, fit: BoxFit.fill)), title: Transform( - transform: Matrix4.translationValues(-18.0, -8.0, 0.0), + transform: Matrix4.translationValues(-8.0, -8.0, 0.0), child: Text( - _attachmentFile.name ?? '', + _attachment.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: AppColor.attachmentFileNameColor, fontWeight: FontWeight.w500), )), - subtitle: _attachmentFile.size != null && _attachmentFile.size?.value != 0 + subtitle: _attachment.size != null && _attachment.size?.value != 0 ? Transform( - transform: Matrix4.translationValues(-18.0, -8.0, 0.0), + transform: Matrix4.translationValues(-8.0, -8.0, 0.0), child: Text( - filesize(_attachmentFile.size?.value), + filesize(_attachment.size?.value), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: AppColor.attachmentFileSizeColor))) diff --git a/lib/features/email/presentation/widgets/message_content_tile_builder.dart b/lib/features/email/presentation/widgets/message_content_tile_builder.dart index 0fee68fa1..e7d9d9f2a 100644 --- a/lib/features/email/presentation/widgets/message_content_tile_builder.dart +++ b/lib/features/email/presentation/widgets/message_content_tile_builder.dart @@ -5,14 +5,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.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'; class MessageContentTileBuilder { final MessageContent messageContent; - final List attachmentInlines; + final List attachmentInlines; final Session? session; final AccountId? accountId; final HtmlMessagePurifier htmlMessagePurifier; @@ -51,7 +51,7 @@ class MessageContentTileBuilder { String getHtmlMessageText() { final message = (attachmentInlines.isNotEmpty && session != null && accountId != null && messageContent.hasImageInlineWithCid()) - ? '${messageContent.getContentHasInlineAttachment(session!, accountId!, attachmentInlines)}' + ? '${messageContent.getContentHasInlineAttachment(session!.getDownloadUrl(), accountId!, attachmentInlines)}' : '${messageContent.content}'; final trustAsHtml = htmlMessagePurifier.purifyHtmlMessage( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 11a5a7b2a..33aef1d28 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -269,5 +269,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "attachment_download_failed": "Attachment download failed", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachment_download_successfully": "Attachment downloaded successfully", + "@attachment_download_successfully": { + "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 11a5a7b2a..33aef1d28 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -269,5 +269,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "attachment_download_failed": "Attachment download failed", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachment_download_successfully": "Attachment downloaded successfully", + "@attachment_download_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 11a5a7b2a..b8a6f634a 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2021-09-15T13:25:41.117609", + "@@last_modified": "2021-09-15T23:44:29.035668", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -269,5 +269,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "attachment_download_failed": "Attachment download failed", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachment_download_successfully": "Attachment downloaded successfully", + "@attachment_download_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 11a5a7b2a..33aef1d28 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -269,5 +269,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "attachment_download_failed": "Attachment download failed", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachment_download_successfully": "Attachment downloaded successfully", + "@attachment_download_successfully": { + "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 11a5a7b2a..33aef1d28 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -269,5 +269,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "attachment_download_failed": "Attachment download failed", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachment_download_successfully": "Attachment downloaded successfully", + "@attachment_download_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 6171d4ca2..97d5ddc5a 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:core/presentation/utils/app_toast.dart'; +import 'package:device_info/device_info.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -13,6 +14,7 @@ class CoreBindings extends Bindings { _bindingKeyboardManager(); _bindingValidator(); _bindingToast(); + _bindingDeviceManager(); } void _bindingAppImagePaths() { @@ -38,4 +40,9 @@ class CoreBindings extends Bindings { void _bindingToast() { Get.put(AppToast()); } + + void _bindingDeviceManager() { + Get.put(DeviceInfoPlugin()); + Get.put(DeviceManager(Get.find())); + } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 79ce07ce3..2aab4d733 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -300,5 +300,19 @@ class AppLocalizations { name: 'an_error_occurred', ); } + + String get attachment_download_failed { + return Intl.message( + 'Attachment download failed', + name: 'attachment_download_failed', + ); + } + + String get attachment_download_successfully { + return Intl.message( + 'Attachment downloaded successfully', + name: 'attachment_download_successfully', + ); + } }