diff --git a/assets/images/ic_collapse_attachment.svg b/assets/images/ic_collapse_attachment.svg
new file mode 100644
index 000000000..66bb2c26f
--- /dev/null
+++ b/assets/images/ic_collapse_attachment.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_download.svg b/assets/images/ic_download.svg
new file mode 100644
index 000000000..3b8cae244
--- /dev/null
+++ b/assets/images/ic_download.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_expand_attachment.svg b/assets/images/ic_expand_attachment.svg
new file mode 100644
index 000000000..e58edcf83
--- /dev/null
+++ b/assets/images/ic_expand_attachment.svg
@@ -0,0 +1,3 @@
+
diff --git a/core/lib/core.dart b/core/lib/core.dart
index d133674b3..5ddd061ca 100644
--- a/core/lib/core.dart
+++ b/core/lib/core.dart
@@ -33,6 +33,7 @@ export 'presentation/views/text/text_form_field_builder.dart';
export 'presentation/views/image/icon_builder.dart';
export 'presentation/views/context_menu/context_menu_action_builder.dart';
export 'presentation/views/context_menu/context_menu_builder.dart';
+export 'presentation/views/dialog/downloading_file_dialog_builder.dart';
// Resources
export 'presentation/resources/assets_paths.dart';
@@ -48,6 +49,8 @@ export 'data/network/config/dynamic_url_interceptors.dart';
export 'data/network/config/endpoint.dart';
export 'data/network/config/service_path.dart';
export 'data/network/dio_client.dart';
+export 'data/network/download/download_client.dart';
+export 'data/network/download/download_manager.dart';
// State
export 'presentation/state/success.dart';
diff --git a/core/lib/data/network/download/download_client.dart b/core/lib/data/network/download/download_client.dart
new file mode 100644
index 000000000..0a3ea69e9
--- /dev/null
+++ b/core/lib/data/network/download/download_client.dart
@@ -0,0 +1,29 @@
+
+import 'dart:io';
+
+import 'package:core/core.dart';
+import 'package:dio/dio.dart';
+
+class DownloadClient {
+
+ final DioClient _dioClient;
+
+ DownloadClient(this._dioClient);
+
+ Future downloadFile(
+ String url,
+ String baseAuth,
+ CancelToken? cancelToken,
+ ) async {
+ final headerParam = _dioClient.getHeaders();
+ headerParam[HttpHeaders.authorizationHeader] = baseAuth;
+ headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader;
+
+ final responseBody = await _dioClient.get(
+ url,
+ options: Options(headers: headerParam, responseType: ResponseType.stream),
+ cancelToken: cancelToken);
+
+ return (responseBody as ResponseBody);
+ }
+}
\ No newline at end of file
diff --git a/core/lib/data/network/download/download_manager.dart b/core/lib/data/network/download/download_manager.dart
new file mode 100644
index 000000000..9fa023768
--- /dev/null
+++ b/core/lib/data/network/download/download_manager.dart
@@ -0,0 +1,76 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:core/core.dart';
+import 'package:core/data/network/download/download_client.dart';
+import 'package:dio/dio.dart';
+
+class DownloadManager {
+ final DownloadClient _downloadClient;
+
+ DownloadManager(this._downloadClient);
+
+ Future downloadFile(
+ String downloadUrl,
+ Future directoryToSave,
+ String filename,
+ String baseAuth,
+ {CancelToken? cancelToken}
+ ) async {
+ final streamController = StreamController();
+
+ try {
+ await Future.wait([
+ _downloadClient.downloadFile(downloadUrl, baseAuth, cancelToken),
+ directoryToSave
+ ]).then((values) {
+ final fileStream = (values[0] as ResponseBody).stream;
+ final tempFilePath = '${(values[1] as Directory).absolute.path}/$filename';
+
+ final file = File(tempFilePath);
+ file.createSync(recursive: true);
+ var randomAccessFile = file.openSync(mode: FileMode.write);
+ late StreamSubscription subscription;
+
+ subscription = fileStream
+ .takeWhile((_) => cancelToken == null || !cancelToken.isCancelled)
+ .listen((data) {
+ subscription.pause();
+ randomAccessFile.writeFrom(data).then((_randomAccessFile) {
+ randomAccessFile = _randomAccessFile;
+ subscription.resume();
+ if (cancelToken != null && cancelToken.isCancelled) {
+ streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message));
+ }
+ }).catchError((error) async {
+ await subscription.cancel();
+ streamController.sink.addError(CommonDownloadFileException(error.toString()));
+ await streamController.close();
+ });
+ }, onDone: () async {
+ await randomAccessFile.close();
+ if (cancelToken != null && cancelToken.isCancelled) {
+ streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message));
+ } else {
+ streamController.sink.add(tempFilePath);
+ }
+ await streamController.close();
+ }, onError: (error) async {
+ await randomAccessFile.close();
+ await file.delete();
+ streamController.sink.addError(CommonDownloadFileException(error.toString()));
+ await streamController.close();
+ });
+ });
+ } catch(exception) {
+ if (cancelToken != null && cancelToken.isCancelled) {
+ streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message));
+ await streamController.close();
+ return streamController.stream.first;
+ } else {
+ throw exception;
+ }
+ }
+ return streamController.stream.first;
+ }
+}
\ 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 47ce8140d..a9722901c 100644
--- a/core/lib/presentation/extensions/color_extension.dart
+++ b/core/lib/presentation/extensions/color_extension.dart
@@ -55,4 +55,5 @@ extension AppColor on Color {
static const toastBackgroundColor = Color(0xFFACAFFF);
static const toastSuccessBackgroundColor = Color(0xFF04AA11);
static const toastErrorBackgroundColor = Color(0xFFFF5858);
+ static const backgroundCountAttachment = Color(0x681C1C1C);
}
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 7eccb38db..5e7019762 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -37,6 +37,8 @@ class ImagePaths {
String get icComposerFileShare => _getImagePath('ic_composer_file_share.svg');
String get icExpandAddress => _getImagePath('ic_expand_address.svg');
String get icSelected => _getImagePath('ic_selected.svg');
+ String get icExpandAttachment => _getImagePath('ic_expand_attachment.svg');
+ String get icDownload => _getImagePath('ic_download.svg');
String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
diff --git a/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart
new file mode 100644
index 000000000..155c9cb18
--- /dev/null
+++ b/core/lib/presentation/views/dialog/downloading_file_dialog_builder.dart
@@ -0,0 +1,71 @@
+
+import 'package:core/core.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+typedef OnCancelDownloadActionClick = void Function();
+
+class DownloadingFileDialogBuilder {
+ Key? _key;
+ String _title = '';
+ String _content = '';
+ String _actionText = '';
+ OnCancelDownloadActionClick? _onCancelDownloadActionClick;
+
+ DownloadingFileDialogBuilder();
+
+ void key(Key key) {
+ _key = key;
+ }
+
+ void title(String title) {
+ _title = title;
+ }
+
+ void content(String content) {
+ _content = content;
+ }
+
+ void actionText(String actionText) {
+ _actionText = actionText;
+ }
+
+ void addCancelDownloadActionClick(OnCancelDownloadActionClick onCancelDownloadActionClick) {
+ _onCancelDownloadActionClick = onCancelDownloadActionClick;
+ }
+
+ Widget build() {
+ return CupertinoAlertDialog(
+ key: _key ?? Key('DownloadingFileBuilder'),
+ title: Text(_title, style: TextStyle(fontSize: 17.0, color: Colors.black)),
+ content: Padding(
+ padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0),
+ child: Center(
+ child: Column(
+ children: [
+ SizedBox(
+ width: 20.0,
+ height: 20.0,
+ child: CupertinoActivityIndicator()),
+ SizedBox(height: 16),
+ Text(
+ _content,
+ style: TextStyle(fontSize: 13.0, color: Colors.black),
+ softWrap: false,
+ maxLines: 1)
+ ],
+ ),
+ )),
+ actions: [
+ TextButton(
+ onPressed: () {
+ if (_onCancelDownloadActionClick != null) {
+ _onCancelDownloadActionClick!();
+ }},
+ child: Text(_actionText, style: TextStyle(fontSize: 17.0, color: AppColor.appColor)),
+ )
+ ],
+ );
+ }
+}
\ 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 3a144dc2b..9a3ccb6a2 100644
--- a/lib/features/composer/presentation/composer_controller.dart
+++ b/lib/features/composer/presentation/composer_controller.dart
@@ -25,7 +25,6 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_inter
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/extensions/email_content_extension.dart';
-import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.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/main/localizations/app_localizations.dart';
diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart
index e01373b93..cdb2d25f5 100644
--- a/lib/features/email/domain/repository/email_repository.dart
+++ b/lib/features/email/domain/repository/email_repository.dart
@@ -1,3 +1,4 @@
+import 'package:dio/dio.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';
@@ -16,4 +17,12 @@ abstract class EmailRepository {
String baseDownloadUrl,
AccountRequest accountRequest
);
+
+ Future exportAttachment(
+ Attachment attachment,
+ AccountId accountId,
+ String baseDownloadUrl,
+ AccountRequest accountRequest,
+ CancelToken cancelToken
+ );
}
\ No newline at end of file
diff --git a/lib/features/email/domain/state/export_attachment_state.dart b/lib/features/email/domain/state/export_attachment_state.dart
new file mode 100644
index 000000000..b90959cd0
--- /dev/null
+++ b/lib/features/email/domain/state/export_attachment_state.dart
@@ -0,0 +1,19 @@
+import 'package:core/core.dart';
+
+class ExportAttachmentSuccess extends UIState {
+ final String filePath;
+
+ ExportAttachmentSuccess(this.filePath);
+
+ @override
+ List