TF-3267 Implement HTML attachment preview
This commit is contained in:
@@ -13,4 +13,5 @@ class Constant {
|
||||
static const mailtoScheme = 'mailto';
|
||||
static const attachmentScheme = 'attachment';
|
||||
static const emlPreviewerScheme = 'eml-previewer';
|
||||
static const htmlExtension = '.html';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
class UnsupportedCharsetException implements Exception {
|
||||
const UnsupportedCharsetException();
|
||||
}
|
||||
|
||||
class NullCharsetException implements Exception {
|
||||
const NullCharsetException();
|
||||
}
|
||||
@@ -47,4 +47,9 @@ extension MediaTypeExtension on MediaType {
|
||||
fileName?.endsWith(Constant.pdfExtension) == true);
|
||||
|
||||
bool get isEMLFile => mimeType == Constant.emlMimeType;
|
||||
|
||||
bool isHTMLFile({required String? fileName}) =>
|
||||
mimeType == Constant.textHtmlMimeType ||
|
||||
(mimeType == Constant.octetStreamMimeType &&
|
||||
fileName?.endsWith(Constant.htmlExtension) == true);
|
||||
}
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ class StandardizeHtmlSanitizingTransformers extends TextTransformer {
|
||||
'style',
|
||||
'body',
|
||||
'section',
|
||||
'nav',
|
||||
'main',
|
||||
'footer',
|
||||
];
|
||||
|
||||
const StandardizeHtmlSanitizingTransformers();
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
typedef OnClickHyperLinkAction = Function(Uri?);
|
||||
typedef OnMailtoClicked = void Function(Uri? uri);
|
||||
|
||||
class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
|
||||
@@ -21,13 +22,15 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
final TextDirection? direction;
|
||||
|
||||
/// Handler for mailto: links
|
||||
final Function(Uri?)? mailtoDelegate;
|
||||
final OnMailtoClicked? mailtoDelegate;
|
||||
|
||||
final OnClickHyperLinkAction? onClickHyperLinkAction;
|
||||
|
||||
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
|
||||
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
|
||||
final bool allowResizeToDocumentSize;
|
||||
|
||||
|
||||
final bool keepWidthWhileLoading;
|
||||
|
||||
const HtmlContentViewerOnWeb({
|
||||
Key? key,
|
||||
required this.contentHtml,
|
||||
@@ -37,6 +40,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
this.mailtoDelegate,
|
||||
this.direction,
|
||||
this.onClickHyperLinkAction,
|
||||
this.keepWidthWhileLoading = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -99,7 +103,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
||||
}
|
||||
}
|
||||
|
||||
if (data['type'] != null && data['type'].contains('toDart: htmlWidth')) {
|
||||
if (data['type'] != null && data['type'].contains('toDart: htmlWidth') && !widget.keepWidthWhileLoading) {
|
||||
final docWidth = data['width'] ?? _actualWidth;
|
||||
if (docWidth != null && mounted) {
|
||||
if (docWidth > _minWidth && widget.allowResizeToDocumentSize) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'app_logger.dart';
|
||||
|
||||
import 'package:core/domain/exceptions/string_exception.dart';
|
||||
|
||||
class StringConvert {
|
||||
static const String separatorPattern = r'[ ,;]+';
|
||||
|
||||
@@ -38,4 +41,24 @@ class StringConvert {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static String decodeFromBytes(
|
||||
Uint8List bytes, {
|
||||
required String? charset,
|
||||
bool isHtml = false,
|
||||
}) {
|
||||
if (isHtml) {
|
||||
return utf8.decode(bytes);
|
||||
} else if (charset == null) {
|
||||
throw const NullCharsetException();
|
||||
} else if (charset.toLowerCase().contains('utf-8')) {
|
||||
return utf8.decode(bytes);
|
||||
} else if (charset.toLowerCase().contains('latin')) {
|
||||
return latin1.decode(bytes);
|
||||
} else if (charset.toLowerCase().contains('ascii')) {
|
||||
return ascii.decode(bytes);
|
||||
} else {
|
||||
throw const UnsupportedCharsetException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,5 +143,35 @@ void main() {
|
||||
|
||||
expect(result, equals('<img src="cid:email123">'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist nav tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<nav href="javascript:alert(1)"></nav>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<nav></nav>'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist main tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<main href="javascript:alert(1)"></main>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<main></main>'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist footer tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<footer href="javascript:alert(1)"></footer>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<footer></footer>'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/domain/exceptions/string_exception.dart';
|
||||
import 'package:core/utils/string_convert.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -159,4 +160,91 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('string convert test:', () {
|
||||
const testText = 'Hello';
|
||||
test(
|
||||
'should use utf8 decoder '
|
||||
'when isHtml is true',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: null, isHtml: true);
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use utf8 decoder '
|
||||
'when charset contains utf-8',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'utf-8');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use latin1 decoder '
|
||||
'when charset contains latin-1',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = latin1.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'latin-1');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use ascii decoder '
|
||||
'when charset contains ascii',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = ascii.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'ascii');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should throw NullCharsetException '
|
||||
'when charset is null',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// assert
|
||||
expect(
|
||||
() => StringConvert.decodeFromBytes(bytes, charset: null),
|
||||
throwsA(isA<NullCharsetException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'should throw UnsupportedCharsetException '
|
||||
'when charset is unsupported',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// assert
|
||||
expect(
|
||||
() => StringConvert.decodeFromBytes(bytes, charset: 'unsupported'),
|
||||
throwsA(isA<UnsupportedCharsetException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,4 +520,14 @@ class EmailRepositoryImpl extends EmailRepository {
|
||||
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored) {
|
||||
return emailDataSource[DataSourceType.session]!.getPreviewEMLContentInMemory(keyStored);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sanitizeHtmlContent(
|
||||
String htmlContent,
|
||||
TransformConfiguration configuration
|
||||
) {
|
||||
return _htmlDataSource.transformHtmlEmailContent(
|
||||
htmlContent,
|
||||
configuration);
|
||||
}
|
||||
}
|
||||
@@ -195,4 +195,8 @@ abstract class EmailRepository {
|
||||
Future<void> storePreviewEMLContentToSessionStorage(EMLPreviewer emlPreviewer);
|
||||
|
||||
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored);
|
||||
|
||||
Future<String> sanitizeHtmlContent(
|
||||
String htmlContent,
|
||||
TransformConfiguration configuration);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:model/email/attachment.dart';
|
||||
|
||||
class GettingHtmlContentFromAttachment extends LoadingState {
|
||||
GettingHtmlContentFromAttachment({required this.attachment});
|
||||
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [attachment];
|
||||
}
|
||||
|
||||
class GetHtmlContentFromAttachmentSuccess extends UIState {
|
||||
GetHtmlContentFromAttachmentSuccess({
|
||||
required this.sanitizedHtmlContent,
|
||||
required this.htmlAttachmentTitle,
|
||||
required this.attachment,
|
||||
});
|
||||
|
||||
final String sanitizedHtmlContent;
|
||||
final String htmlAttachmentTitle;
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
sanitizedHtmlContent,
|
||||
htmlAttachmentTitle,
|
||||
attachment,
|
||||
];
|
||||
}
|
||||
|
||||
class GetHtmlContentFromAttachmentFailure extends FeatureFailure {
|
||||
GetHtmlContentFromAttachmentFailure({super.exception, required this.attachment});
|
||||
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [attachment];
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/string_convert.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
import 'package:model/download/download_task_id.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/domain/state/get_html_content_from_attachment_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
|
||||
|
||||
class GetHtmlContentFromAttachmentInteractor {
|
||||
GetHtmlContentFromAttachmentInteractor(this._downloadAttachmentForWebInteractor);
|
||||
|
||||
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
|
||||
|
||||
Stream<Either<Failure, Success>> execute(
|
||||
AccountId accountId,
|
||||
Attachment attachment,
|
||||
DownloadTaskId taskId,
|
||||
String baseDownloadUrl,
|
||||
TransformConfiguration transformConfiguration,
|
||||
) async* {
|
||||
final onReceiveController = StreamController<Either<Failure, Success>>();
|
||||
try {
|
||||
yield Right(GettingHtmlContentFromAttachment(attachment: attachment));
|
||||
final downloadState = await _downloadAttachmentForWebInteractor.execute(
|
||||
taskId,
|
||||
attachment,
|
||||
accountId,
|
||||
baseDownloadUrl,
|
||||
onReceiveController,
|
||||
).last;
|
||||
|
||||
Either<Failure, Success>? sanitizeState;
|
||||
await downloadState.fold(
|
||||
(failure) {
|
||||
sanitizeState = Left(GetHtmlContentFromAttachmentFailure(
|
||||
exception: failure is FeatureFailure ? failure.exception : null,
|
||||
attachment: attachment,
|
||||
));
|
||||
},
|
||||
(success) async {
|
||||
if (success is! DownloadAttachmentForWebSuccess) {
|
||||
sanitizeState = Right(GettingHtmlContentFromAttachment(attachment: attachment));
|
||||
} else {
|
||||
final htmlContent = StringConvert.decodeFromBytes(
|
||||
success.bytes,
|
||||
charset: success.attachment.charset,
|
||||
isHtml: true,
|
||||
);
|
||||
sanitizeState = await _sanitizeHtmlContent(
|
||||
htmlContent,
|
||||
transformConfiguration,
|
||||
attachment,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onReceiveController.close();
|
||||
if (sanitizeState != null) {
|
||||
yield sanitizeState!;
|
||||
} else {
|
||||
yield Left(GetHtmlContentFromAttachmentFailure(
|
||||
exception: null,
|
||||
attachment: attachment,
|
||||
));
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
logError('GetHtmlContentFromAttachmentInteractor:exception: $e');
|
||||
onReceiveController.close();
|
||||
yield Left(GetHtmlContentFromAttachmentFailure(
|
||||
exception: e,
|
||||
attachment: attachment,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<Failure, Success>?> _sanitizeHtmlContent(
|
||||
String htmlContent,
|
||||
TransformConfiguration transformConfiguration,
|
||||
Attachment attachment,
|
||||
) async {
|
||||
try {
|
||||
final sanitizedHtmlContent = await _downloadAttachmentForWebInteractor
|
||||
.emailRepository
|
||||
.sanitizeHtmlContent(
|
||||
htmlContent,
|
||||
transformConfiguration,
|
||||
);
|
||||
return Right(GetHtmlContentFromAttachmentSuccess(
|
||||
sanitizedHtmlContent: sanitizedHtmlContent,
|
||||
htmlAttachmentTitle: attachment.generateFileName(),
|
||||
attachment: attachment,
|
||||
));
|
||||
} catch (e) {
|
||||
return Left(GetHtmlContentFromAttachmentFailure(
|
||||
exception: e,
|
||||
attachment: attachment,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart';
|
||||
@@ -78,6 +79,7 @@ class EmailBindings extends BaseBindings {
|
||||
Get.find<StoreEventAttendanceStatusInteractor>(),
|
||||
Get.find<ParseEmailByBlobIdInteractor>(),
|
||||
Get.find<PreviewEmailFromEmlFileInteractor>(),
|
||||
Get.find<GetHtmlContentFromAttachmentInteractor>(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -165,6 +167,7 @@ class EmailBindings extends BaseBindings {
|
||||
Get.lazyPut(() => ParseEmailByBlobIdInteractor(Get.find<EmailRepository>()));
|
||||
Get.lazyPut(() => PreviewEmailFromEmlFileInteractor(Get.find<EmailRepository>()));
|
||||
IdentityInteractorsBindings().dependencies();
|
||||
Get.lazyPut(() => GetHtmlContentFromAttachmentInteractor(Get.find<DownloadAttachmentForWebInteractor>()));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_
|
||||
import 'package:core/presentation/utils/html_transformer/text/new_line_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
|
||||
import 'package:core/utils/html/html_utils.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -58,6 +59,7 @@ import 'package:tmail_ui_user/features/email/domain/state/download_attachment_fo
|
||||
import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart';
|
||||
@@ -82,6 +84,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_even
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/send_receipt_to_sender_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
|
||||
@@ -99,6 +102,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_lis
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachment_previewer.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart';
|
||||
import 'package:tmail_ui_user/features/email_previewer/email_previewer_dialog_view.dart';
|
||||
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
|
||||
@@ -149,6 +153,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor;
|
||||
final ParseEmailByBlobIdInteractor _parseEmailByBlobIdInteractor;
|
||||
final PreviewEmailFromEmlFileInteractor _previewEmailFromEmlFileInteractor;
|
||||
final GetHtmlContentFromAttachmentInteractor _getHtmlContentFromAttachmentInteractor;
|
||||
|
||||
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
|
||||
SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor;
|
||||
@@ -204,6 +209,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
this._storeEventAttendanceStatusInteractor,
|
||||
this._parseEmailByBlobIdInteractor,
|
||||
this._previewEmailFromEmlFileInteractor,
|
||||
this._getHtmlContentFromAttachmentInteractor,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -263,6 +269,19 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
_handleParseEmailByBlobIdSuccess(success);
|
||||
} else if (success is PreviewEmailFromEmlFileSuccess) {
|
||||
_handlePreviewEmailFromEMLFileSuccess(success);
|
||||
} else if (success is GetHtmlContentFromAttachmentSuccess) {
|
||||
_updateAttachmentsViewState(success.attachment.blobId, Right(success));
|
||||
Get.dialog(HtmlAttachmentPreviewer(
|
||||
title: success.htmlAttachmentTitle,
|
||||
htmlContent: success.sanitizedHtmlContent,
|
||||
mailToClicked: openMailToLink,
|
||||
downloadAttachmentClicked: () {
|
||||
downloadAttachmentForWeb(success.attachment);
|
||||
},
|
||||
responsiveUtils: responsiveUtils,
|
||||
));
|
||||
} else if (success is GettingHtmlContentFromAttachment) {
|
||||
_updateAttachmentsViewState(success.attachment.blobId, Right(success));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +309,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
_handleParseEmailByBlobIdFailure(failure);
|
||||
} else if (failure is PreviewEmailFromEmlFileFailure) {
|
||||
_handlePreviewEmailFromEMLFileFailure(failure);
|
||||
} else if (failure is GetHtmlContentFromAttachmentFailure) {
|
||||
_handleGetHtmlContentFromAttachmentFailure(failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,7 +1176,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar,
|
||||
});
|
||||
mailboxDashBoardController.setSelectedEmail(newEmail);
|
||||
|
||||
|
||||
final emailId = newEmail?.id;
|
||||
if (emailId == null) return;
|
||||
mailboxDashBoardController.updateEmailFlagByEmailIds(
|
||||
@@ -1932,11 +1953,61 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
previewPDFFileAction(context, attachment);
|
||||
} else if (attachment.isEMLFile) {
|
||||
previewEMLFileAction(attachment.blobId, AppLocalizations.of(context));
|
||||
} else {
|
||||
} else if (attachment.isHTMLFile) {
|
||||
final attachmentEvaluation = evaluateAttachment(attachment);
|
||||
if (!attachmentEvaluation.canDownloadAttachment) {
|
||||
consumeState(Stream.value(Left(GetHtmlContentFromAttachmentFailure(
|
||||
exception: null,
|
||||
attachment: attachment,
|
||||
))));
|
||||
return;
|
||||
}
|
||||
|
||||
consumeState(_getHtmlContentFromAttachmentInteractor.execute(
|
||||
attachmentEvaluation.accountId!,
|
||||
attachment,
|
||||
DownloadTaskId(attachmentEvaluation.blobId!.value),
|
||||
attachmentEvaluation.downloadUrl!,
|
||||
TransformConfiguration.create(
|
||||
customDomTransformers: [SanitizeHyperLinkTagInHtmlTransformer()],
|
||||
customTextTransformers: [const StandardizeHtmlSanitizingTransformers()],
|
||||
),
|
||||
));
|
||||
}else {
|
||||
handleDownloadAttachmentAction(context, attachment);
|
||||
}
|
||||
}
|
||||
|
||||
({
|
||||
bool canDownloadAttachment,
|
||||
AccountId? accountId,
|
||||
String? downloadUrl,
|
||||
Id? blobId,
|
||||
}) evaluateAttachment(
|
||||
Attachment attachment,
|
||||
) {
|
||||
final accountId = mailboxDashBoardController.accountId.value;
|
||||
final downloadUrl = mailboxDashBoardController.sessionCurrent
|
||||
?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
|
||||
final blobId = attachment.blobId;
|
||||
|
||||
if (accountId == null || downloadUrl == null || blobId == null) {
|
||||
return (
|
||||
canDownloadAttachment: false,
|
||||
accountId: accountId,
|
||||
downloadUrl: downloadUrl,
|
||||
blobId: blobId,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
canDownloadAttachment: true,
|
||||
accountId: accountId,
|
||||
downloadUrl: downloadUrl,
|
||||
blobId: blobId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> previewPDFFileAction(BuildContext context, Attachment attachment) async {
|
||||
if (accountId == null || session == null) {
|
||||
appToast.showToastErrorMessage(
|
||||
@@ -2121,4 +2192,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
ComposerArguments.fromMailtoUri(listEmailAddress: listEmailAddressMailTo)
|
||||
);
|
||||
}
|
||||
|
||||
void _handleGetHtmlContentFromAttachmentFailure(GetHtmlContentFromAttachmentFailure failure) {
|
||||
_updateAttachmentsViewState(failure.attachment.blobId, Left(failure));
|
||||
if (currentOverlayContext != null && currentContext != null) {
|
||||
appToast.showToastErrorMessage(
|
||||
currentOverlayContext!,
|
||||
AppLocalizations.of(currentContext!).thisHtmlAttachmentCannotBePreviewed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,6 @@ extension AttachmentExtension on Attachment {
|
||||
}
|
||||
|
||||
DownloadTaskId get downloadTaskId => DownloadTaskId(blobId!.value);
|
||||
|
||||
bool get isHTMLFile => type?.isPDFFile(fileName: name) ?? false;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ 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/domain/state/get_html_content_from_attachment_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';
|
||||
import 'package:tmail_ui_user/main/error/capability_validator.dart';
|
||||
@@ -64,10 +65,12 @@ class EmailUtils {
|
||||
static bool checkingIfAttachmentActionIsEnabled(Either<Failure, Success>? state) {
|
||||
return state?.fold(
|
||||
(failure) {
|
||||
return failure is DownloadAttachmentForWebFailure;
|
||||
return failure is DownloadAttachmentForWebFailure
|
||||
|| failure is GetHtmlContentFromAttachmentFailure;
|
||||
},
|
||||
(success) {
|
||||
return success is DownloadAttachmentForWebSuccess
|
||||
|| success is GetHtmlContentFromAttachmentSuccess
|
||||
|| success is IdleDownloadAttachmentForWeb;
|
||||
}) ?? false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||
import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
|
||||
import 'package:core/presentation/views/responsive/responsive_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_attachment_viewer.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
||||
|
||||
class HtmlAttachmentPreviewer extends StatelessWidget {
|
||||
const HtmlAttachmentPreviewer({
|
||||
super.key,
|
||||
required this.htmlContent,
|
||||
required this.title,
|
||||
required this.mailToClicked,
|
||||
required this.downloadAttachmentClicked,
|
||||
required this.responsiveUtils,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String htmlContent;
|
||||
final OnMailtoClicked mailToClicked;
|
||||
final VoidCallback downloadAttachmentClicked;
|
||||
final ResponsiveUtils responsiveUtils;
|
||||
|
||||
static const double _verticalMargin = 16;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TopBarAttachmentViewer(
|
||||
title: title,
|
||||
closeAction: popBack,
|
||||
downloadAction: downloadAttachmentClicked,
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: GestureDetector(
|
||||
onTap: popBack,
|
||||
child: PointerInterceptor(
|
||||
child: ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: _verticalMargin),
|
||||
color: Colors.white,
|
||||
child: ResponsiveWidget(
|
||||
responsiveUtils: responsiveUtils,
|
||||
desktop: _buildHtmlViewerWith(
|
||||
context,
|
||||
width: constraints.maxWidth * 0.4,
|
||||
height: constraints.maxHeight - _verticalMargin * 2
|
||||
),
|
||||
tablet: _buildHtmlViewerWith(
|
||||
context,
|
||||
width: constraints.maxWidth * 0.8,
|
||||
height: constraints.maxHeight - _verticalMargin * 2
|
||||
),
|
||||
mobile: _buildHtmlViewerWith(
|
||||
context,
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight - _verticalMargin * 2
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
HtmlContentViewerOnWeb _buildHtmlViewerWith(
|
||||
BuildContext context, {
|
||||
required double width,
|
||||
required double height,
|
||||
}) {
|
||||
return HtmlContentViewerOnWeb(
|
||||
contentHtml: htmlContent,
|
||||
widthContent: width,
|
||||
heightContent: height,
|
||||
direction: AppUtils.getCurrentDirection(context),
|
||||
mailtoDelegate: (uri) {
|
||||
popBack();
|
||||
mailToClicked(uri);
|
||||
},
|
||||
keepWidthWhileLoading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import 'package:tmail_ui_user/features/email/domain/exceptions/download_attachme
|
||||
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_attachment_viewer.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
|
||||
@@ -221,8 +221,8 @@ class _PDFViewerState extends State<PDFViewer> {
|
||||
if (deviceInfo.hasData && deviceInfo.data is WebBrowserInfo) {
|
||||
browserName = (deviceInfo.data as WebBrowserInfo).browserName;
|
||||
}
|
||||
return TopBarPDFViewer(
|
||||
attachment: widget.attachment,
|
||||
return TopBarAttachmentViewer(
|
||||
title: widget.attachment.generateFileName(),
|
||||
downloadAction: () => widget.downloadAction?.call(
|
||||
viewState.bytes,
|
||||
widget.attachment.generateFileName()
|
||||
@@ -235,8 +235,8 @@ class _PDFViewerState extends State<PDFViewer> {
|
||||
}
|
||||
return child ?? const SizedBox.shrink();
|
||||
},
|
||||
child: TopBarPDFViewer(
|
||||
attachment: widget.attachment,
|
||||
child: TopBarAttachmentViewer(
|
||||
title: widget.attachment.generateFileName(),
|
||||
closeAction: () {
|
||||
_downloadAttachmentCancelToken?.cancel();
|
||||
Navigator.maybeOf(context)?.pop();
|
||||
|
||||
+5
-6
@@ -1,16 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:model/email/attachment.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
class TopBarPDFViewer extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
class TopBarAttachmentViewer extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback? downloadAction;
|
||||
final VoidCallback? printAction;
|
||||
final VoidCallback? closeAction;
|
||||
|
||||
const TopBarPDFViewer({
|
||||
const TopBarAttachmentViewer({
|
||||
super.key,
|
||||
required this.attachment,
|
||||
required this.title,
|
||||
this.downloadAction,
|
||||
this.printAction,
|
||||
this.closeAction,
|
||||
@@ -40,7 +39,7 @@ class TopBarPDFViewer extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
attachment.generateFileName(),
|
||||
title,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
@@ -4119,5 +4119,17 @@
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"saveAndRefresh": "Save & refresh",
|
||||
"@saveAndRefresh": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"thisHtmlAttachmentCannotBePreviewed": "This html attachment cannot be previewed",
|
||||
"@thisHtmlAttachmentCannotBePreviewed": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
@@ -4327,4 +4327,18 @@ class AppLocalizations {
|
||||
name: 'editAsNewEmail',
|
||||
);
|
||||
}
|
||||
|
||||
String get saveAndRefresh {
|
||||
return Intl.message(
|
||||
'Save & refresh',
|
||||
name: 'saveAndRefresh',
|
||||
);
|
||||
}
|
||||
|
||||
String get thisHtmlAttachmentCannotBePreviewed {
|
||||
return Intl.message(
|
||||
'This html attachment cannot be previewed',
|
||||
name: 'thisHtmlAttachmentCannotBePreviewed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_accept_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
|
||||
@@ -107,6 +108,7 @@ const fallbackGenerators = {
|
||||
MockSpec<ToastManager>(),
|
||||
MockSpec<CalendarEventDataSource>(),
|
||||
MockSpec<DioClient>(),
|
||||
MockSpec<GetHtmlContentFromAttachmentInteractor>(),
|
||||
])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -143,6 +145,7 @@ void main() {
|
||||
final printUtils = MockPrintUtils();
|
||||
final applicationManager = MockApplicationManager();
|
||||
final mockToastManager = MockToastManager();
|
||||
final getHtmlContentFromAttachmentInteractor = MockGetHtmlContentFromAttachmentInteractor();
|
||||
|
||||
late SingleEmailController singleEmailController;
|
||||
|
||||
@@ -199,6 +202,7 @@ void main() {
|
||||
storeEventAttendanceStatusInteractor,
|
||||
parseEmailByBlobIdInteractor,
|
||||
previewEmailFromEmlFileInteractor,
|
||||
getHtmlContentFromAttachmentInteractor,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user