TF-105 [BUG] Fix long line should be wrapped

This commit is contained in:
dab246
2021-10-12 16:51:30 +07:00
committed by Dat H. Pham
parent cb2c889fe2
commit dd5e8e4519
22 changed files with 243 additions and 286 deletions
@@ -1,24 +0,0 @@
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/dom.dart';
class SetStyleBlockQuoteTransformer extends DomTransformer {
const SetStyleBlockQuoteTransformer();
@override
void process(Document document, String message, TransformConfiguration configuration) {
final quoteElements = document.getElementsByTagName('blockquote');
for (final quote in quoteElements) {
final style = quote.attributes['style'];
if (style == null) {
quote.attributes['style'] = '''
margin: 0px 8px 0px 8px;
padding: 8px 16px 8px 16px;
border-left:5px solid #eee;
''';
}
}
}
}
@@ -12,23 +12,12 @@ class ImageTransformer extends DomTransformer {
final imageElements = document.getElementsByTagName('img'); final imageElements = document.getElementsByTagName('img');
for (final imageElement in imageElements) { for (final imageElement in imageElements) {
final src = imageElement.attributes['src']; final src = imageElement.attributes['src'];
if (src != null) { if (src != null && src.startsWith('http:')) {
if (src.startsWith('http')) { // always at least enforce HTTPS images:
if (configuration.blockExternalImages) { final url = src.substring('http:'.length);
imageElement.attributes.remove('src'); imageElement.attributes['src'] = 'https:$url';
} else if (src.startsWith('http:')) {
// always at least enforce HTTPS images:
final url = src.substring('http:'.length);
imageElement.attributes['src'] = 'https:$url';
}
}
}
final style = imageElement.attributes['style'];
if (style == null) {
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
} else {
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;' + style;
} }
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
} }
} }
} }
@@ -1,36 +0,0 @@
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/dom.dart';
class SetStyleTableTransformer extends DomTransformer {
const SetStyleTableTransformer();
@override
void process(Document document, String message, TransformConfiguration configuration) {
final tableElements = document.getElementsByTagName('table');
for (final table in tableElements) {
final style = table.attributes['style'];
if (style == null) {
table.attributes['style'] = 'width: 100%;max-width: 100%;border:1px solid #f0f0f0;border-collapse: collapse;border-spacing: 2px;';
}
}
final tdElements = document.getElementsByTagName('td');
for (final tdTag in tdElements) {
final style = tdTag.attributes['style'];
if (style == null) {
tdTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
}
}
final thElements = document.getElementsByTagName('th');
for (final thTag in thElements) {
final style = thTag.attributes['style'];
if (style == null) {
thTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
}
}
}
}
@@ -4,39 +4,22 @@ import 'package:html/dom.dart';
class HtmlTransform { class HtmlTransform {
final String _message; final String _contentHtml;
HtmlTransform(this._message); HtmlTransform(this._contentHtml);
/// Transforms this message to HTML code. /// Transforms this message to HTML code.
/// /// Optionally specify the [transformConfiguration] to control all aspects of the transformation
/// Set [blockExternalImages] to `true` in case external images should be blocked. /// - in that case other parameters are ignored.
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored. String transformToHtml({TransformConfiguration? transformConfiguration}) {
String transformToHtml({ final document = transformToDocument(transformConfiguration: transformConfiguration);
bool? blockExternalImages,
TransformConfiguration? transformConfiguration,
}) {
final document = transformToDocument(
blockExternalImages: blockExternalImages,
transformConfiguration: transformConfiguration,
);
return document.outerHtml; return document.outerHtml;
} }
/// Transforms this message to Document. /// Transforms this message to Document.
/// Document transformToDocument({TransformConfiguration? transformConfiguration}) {
/// Set [blockExternalImages] to `true` in case external images should be blocked. transformConfiguration ??= TransformConfiguration.create();
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored.
Document transformToDocument({
bool? blockExternalImages,
int? maxImageWidth,
TransformConfiguration? transformConfiguration,
}) {
transformConfiguration ??= TransformConfiguration.create(
blockExternalImages: blockExternalImages,
maxImageWidth: maxImageWidth,
);
final transformer = MessageContentTransformer(transformConfiguration); final transformer = MessageContentTransformer(transformConfiguration);
return transformer.toDocument(_message); return transformer.toDocument(_contentHtml);
} }
} }
@@ -1,11 +1,9 @@
import 'package:core/presentation/utils/html_transformer/dom/blockquote_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/link_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/link_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/meta_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/meta_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/table_transformers.dart';
import 'package:core/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/linebreak_text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/text/linebreak_text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/links_text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/text/links_text_transformer.dart';
@@ -14,12 +12,6 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d
/// Contains the configuration for all transformations. /// Contains the configuration for all transformations.
class TransformConfiguration { class TransformConfiguration {
/// Should external images be blocked?
final bool blockExternalImages;
/// The maximum width for embedded images. It make sense to limit this to reduce the generated HTML size.
final int? maxImageWidth;
/// The list of DOM transformers being used /// The list of DOM transformers being used
final List<DomTransformer> domTransformers; final List<DomTransformer> domTransformers;
@@ -30,16 +22,12 @@ class TransformConfiguration {
/// ///
/// Compare [create] to have an easier to use building function /// Compare [create] to have an easier to use building function
const TransformConfiguration( const TransformConfiguration(
this.blockExternalImages,
this.maxImageWidth,
this.domTransformers, this.domTransformers,
this.textTransformers this.textTransformers
); );
/// Provides easy access to a standard configuration that does not block external images. /// Provides easy access to a standard configuration that does not block external images.
static const TransformConfiguration standardConfiguration = TransformConfiguration( static const TransformConfiguration standardConfiguration = TransformConfiguration(
false,
standardMaxImageWidth,
standardDomTransformers, standardDomTransformers,
standardTextTransformers standardTextTransformers
); );
@@ -61,8 +49,6 @@ class TransformConfiguration {
: standardTextTransformers; : standardTextTransformers;
maxImageWidth ??= standardMaxImageWidth; maxImageWidth ??= standardMaxImageWidth;
return TransformConfiguration( return TransformConfiguration(
blockExternalImages ?? false,
maxImageWidth,
domTransformers, domTransformers,
textTransformers textTransformers
); );
@@ -75,8 +61,6 @@ class TransformConfiguration {
RemoveScriptTransformer(), RemoveScriptTransformer(),
ImageTransformer(), ImageTransformer(),
EnsureRelationNoReferrerTransformer(), EnsureRelationNoReferrerTransformer(),
SetStyleBlockQuoteTransformer(),
SetStyleTableTransformer(),
]; ];
static const List<TextTransformer> standardTextTransformers = [ static const List<TextTransformer> standardTextTransformers = [
@@ -1,55 +1,31 @@
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';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart' as launcher; import 'package:url_launcher/url_launcher.dart' as launcher;
class _HtmlGenerationArguments {
final String message;
final bool blockExternalImages;
const _HtmlGenerationArguments(
this.message,
this.blockExternalImages,
);
}
class _HtmlGenerationResult {
final String? html;
final String? errorDetails;
const _HtmlGenerationResult.success(this.html) : errorDetails = null;
const _HtmlGenerationResult.error(this.errorDetails) : this.html = null;
}
class HtmlContentViewer extends StatefulWidget { class HtmlContentViewer extends StatefulWidget {
final String message; final String contentHtml;
final bool blockExternalImages; final int minHeight;
/// Is notified about any errors that might occur
final void Function(Object? exception, StackTrace? stackTrace)? onError;
/// Register this callback if you want a reference to the [InAppWebViewController]. /// Register this callback if you want a reference to the [InAppWebViewController].
final void Function(InAppWebViewController controller)? onWebViewCreated; final void Function(InAppWebViewController controller)? onCreated;
final void Function(InAppWebViewController controller, Uri? uri)? onWebViewLoadStart; final void Function(InAppWebViewController controller, Uri? uri)? onLoadStart;
final void Function(InAppWebViewController controller, Uri? uri)? onWebViewLoadStop; final void Function(InAppWebViewController controller, Uri? uri)? onLoadStop;
/// Handler for mailto: links /// Handler for mailto: links
final Future Function(Uri mailto)? mailtoDelegate; final Future Function(Uri mailto)? mailtoDelegate;
const HtmlContentViewer({ const HtmlContentViewer({
Key? key, Key? key,
required this.message, required this.contentHtml,
this.blockExternalImages = false, this.minHeight = 100,
this.onError, this.onCreated,
this.onWebViewCreated, this.onLoadStart,
this.onWebViewLoadStart, this.onLoadStop,
this.onWebViewLoadStop,
this.mailtoDelegate, this.mailtoDelegate,
}) : super(key: key); }) : super(key: key);
@@ -59,123 +35,126 @@ class HtmlContentViewer extends StatefulWidget {
class _HtmlContentViewState extends State<HtmlContentViewer> { class _HtmlContentViewState extends State<HtmlContentViewer> {
String? _htmlData; double? _documentHeight = 1.0;
bool? _wereExternalImagesBlocked; double? _documentWidth = 1.0;
bool _isGenerating = false; String? _initialPageContent;
double? _webViewContentHeight = 1.0; late InAppWebViewController _webViewController;
@override @override
void initState() { void initState() {
_generateHtml(widget.blockExternalImages);
super.initState(); super.initState();
_initialPageContent = _generateHtmlDocument(widget.contentHtml);
} }
void _generateHtml(bool blockExternalImages) async { String _generateHtmlDocument(String content) {
_wereExternalImagesBlocked = blockExternalImages; final htmlTemplate = '''
_isGenerating = true; <!DOCTYPE html>
final args = _HtmlGenerationArguments( <html>
widget.message, <head>
blockExternalImages, <meta name="viewport" content="width=device-width, initial-scale=1.0">
); <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
final result = await compute(_generateHtmlImpl, args); <style>
_htmlData = result.html; #editor {
if (_htmlData == null) { outline: 0px solid transparent;
final onError = widget.onError; min-height: ${widget.minHeight}px;
if (onError != null) { min-width: 300px;
onError(result.errorDetails, null); color: #182952;
} font-family: verdana;
} }
if (mounted) { blockquote {
setState(() { font: normal helvetica, verdana;
_isGenerating = false; margin-top: 10px;
}); margin-bottom: 10px;
} margin-left: 20px;
} padding-left: 15px;
border-left: 5px solid #eee;
}
table {
width: 100%;
max-width: 100%;
}
td {
padding: 13px;
margin: 0px;
}
th {
padding: 13px;
margin: 0px;
}
</style>
<script>
var documentHeight;
static _HtmlGenerationResult _generateHtmlImpl(_HtmlGenerationArguments args) { function onLoaded() {
try { documentHeight = document.body.scrollHeight;
final htmlTransform = HtmlTransform(args.message); document.execCommand("styleWithCSS", false, true);
final html = htmlTransform.transformToHtml( }
blockExternalImages: args.blockExternalImages, </script>
); </head>
return _HtmlGenerationResult.success(html); <body onload="onLoaded();">
} catch (e, s) { <div id="editor">$content</div>
String errorDetails = e.toString() + '\n\n' + s.toString(); </body>
return _HtmlGenerationResult.error(errorDetails); </html>
} ''';
return htmlTemplate;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isGenerating) {
return SizedBox.shrink();
}
if (widget.blockExternalImages != _wereExternalImagesBlocked) {
_generateHtml(widget.blockExternalImages);
}
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
_documentWidth = size.width - 30;
return SizedBox( return SizedBox(
height: _webViewContentHeight, height: _documentHeight,
width: size.width, width: _documentWidth,
child: Padding( child: _buildWebView(),
padding: EdgeInsets.only(top: 16),
child: _buildWebView(),
),
); );
} }
Widget _buildWebView() { Widget _buildWebView() {
if (_htmlData == null) { if (_initialPageContent == null || _initialPageContent?.isEmpty == true) {
return Container(); return Container();
} }
return InAppWebView( return InAppWebView(
key: ValueKey(_htmlData), key: ValueKey(_initialPageContent),
initialData: InAppWebViewInitialData(data: _htmlData!), initialData: InAppWebViewInitialData(data: _initialPageContent!),
initialOptions: InAppWebViewGroupOptions( initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions( crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true, useShouldOverrideUrlLoading: true,
verticalScrollBarEnabled: false, verticalScrollBarEnabled: false,
disableVerticalScroll: true, disableVerticalScroll: false,
transparentBackground: true, disableHorizontalScroll: false,
supportZoom: true,
), ),
android: AndroidInAppWebViewOptions( android: AndroidInAppWebViewOptions(
useWideViewPort: false,
loadWithOverviewMode: true,
useHybridComposition: true, useHybridComposition: true,
), ),
ios: IOSInAppWebViewOptions( ios: IOSInAppWebViewOptions(
allowsInlineMediaPlayback: true, enableViewportScale: false,
enableViewportScale: true,
allowsLinkPreview: false allowsLinkPreview: false
) ),
), ),
onLoadStart: (controller, uri) { onLoadStart: (controller, uri) {
if (widget.onWebViewLoadStart != null) { if (widget.onLoadStart != null) {
widget.onWebViewLoadStart!(controller, uri); widget.onLoadStart!(controller, uri);
} }
}, },
onWebViewCreated: widget.onWebViewCreated, onWebViewCreated: _onWebViewCreated,
onLoadStop: (controller, uri) async { onLoadStop: (controller, uri) async {
var scrollHeight = (await controller.evaluateJavascript(source: 'document.body.scrollHeight')); final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
if (scrollHeight != null) { if ((scrollHeight != null) && mounted && (scrollHeight + 30.0 > widget.minHeight)) {
final scrollWidth = (await controller.evaluateJavascript(source: 'document.body.scrollWidth'));
final size = MediaQuery.of(context).size;
final containerWidth = size.width - 60.0;
if (scrollWidth > containerWidth) {
var scale = (containerWidth / scrollWidth);
if (scale < 0.2) {
scale = 0.2;
}
await controller.zoomBy(zoomFactor: scale, iosAnimated: true);
scrollHeight = (scrollHeight * scale).ceil();
}
setState(() { setState(() {
_webViewContentHeight = double.tryParse('${scrollHeight + 24}'); _documentHeight = (scrollHeight + 30.0);
}); });
} }
if (widget.onLoadStop != null) {
if (widget.onWebViewLoadStop != null) { widget.onLoadStop!(controller, uri);
widget.onWebViewLoadStop!(controller, uri); }
},
onScrollChanged: (controller, x, y) {
if (y != 0) {
controller.scrollTo(x: 0, y: 0);
} }
}, },
shouldOverrideUrlLoading: _shouldOverrideUrlLoading, shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
@@ -185,6 +164,13 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
); );
} }
void _onWebViewCreated(InAppWebViewController controller) {
_webViewController = controller;
if (widget.onCreated != null) {
widget.onCreated!(_webViewController);
}
}
Future<NavigationActionPolicy> _shouldOverrideUrlLoading( Future<NavigationActionPolicy> _shouldOverrideUrlLoading(
InAppWebViewController controller, InAppWebViewController controller,
NavigationAction request NavigationAction request
@@ -28,7 +28,6 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_addre
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
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/model/composer_arguments.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/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';
@@ -215,18 +214,11 @@ class ComposerController extends BaseController {
final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag()); final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag());
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)
.addBlockTag('p', attribute: 'style=\"font-size:14px;font-style:italic;color:#182952;\"') .addBlockTag('p', attribute: 'style=\"font-size:14px;font-style:italic;color:#182952;\"')
: ''; : '';
var trustAsHtml = ''; final trustAsHtml = composerArguments.value?.emailContent?.content ?? '';
if (composerArguments.value != null) {
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>';
@@ -256,7 +248,7 @@ class ComposerController extends BaseController {
EmailBodyPart( EmailBodyPart(
partId: generatePartId, partId: generatePartId,
blobId: generateBlobId, blobId: generateBlobId,
type: MediaType.parse(EmailConstants.HTML_TEXT) type: MediaType.parse('text/html')
)}, )},
bodyValues: { bodyValues: {
generatePartId: EmailBodyValue(emailBodyText, false, false) generatePartId: EmailBodyValue(emailBodyText, false, false)
@@ -174,17 +174,23 @@ class ComposerView extends GetWidget<ComposerController> {
Widget _buildComposerEditor(BuildContext context) { Widget _buildComposerEditor(BuildContext context) {
return Obx(() { return Obx(() {
if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) { if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) {
return PackagedHtmlEditor( return HtmlEditor(
key: Key('composer_editor'), key: Key('composer_editor'),
minHeight: 400, minHeight: 400,
supportZoom: true,
disableHorizontalScroll: false,
disableVerticalScroll: false,
onCreated: (editorApi) => controller.htmlEditorApi = editorApi, onCreated: (editorApi) => controller.htmlEditorApi = editorApi,
); );
} else { } else {
final message = controller.getContentEmail(); final message = controller.getContentEmail();
return message.isNotEmpty return message.isNotEmpty
? PackagedHtmlEditor( ? HtmlEditor(
key: Key('composer_editor'), key: Key('composer_editor'),
minHeight: 400, minHeight: 400,
supportZoom: true,
disableHorizontalScroll: false,
disableVerticalScroll: false,
onCreated: (editorApi) => controller.htmlEditorApi = editorApi, onCreated: (editorApi) => controller.htmlEditorApi = editorApi,
initialContent: message, initialContent: message,
) )
@@ -0,0 +1,6 @@
import 'package:model/model.dart';
abstract class HtmlDataSource {
Future<String> transformToHtml(EmailContent emailContent);
}
@@ -0,0 +1,19 @@
import 'package:model/email/email_content.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
class HtmlDataSourceImpl extends HtmlDataSource {
final HtmlAnalyzer _htmlAnalyzer;
HtmlDataSourceImpl(this._htmlAnalyzer);
@override
Future<String> transformToHtml(EmailContent emailContent) {
return Future.sync(() async {
return await _htmlAnalyzer.transformToHtml(emailContent);
}).catchError((error) {
throw error;
});
}
}
@@ -0,0 +1,17 @@
import 'package:core/presentation/utils/html_transformer/html_transform.dart';
import 'package:model/email/email_content_type.dart';
import 'package:model/model.dart';
class HtmlAnalyzer {
Future<String> transformToHtml(EmailContent emailContent) async {
switch(emailContent.type) {
case EmailContentType.textHtml:
final htmlTransform = HtmlTransform(emailContent.content);
return htmlTransform.transformToHtml();
default:
return emailContent.content;
}
}
}
@@ -5,14 +5,16 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_request.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_request.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
class EmailRepositoryImpl extends EmailRepository { class EmailRepositoryImpl extends EmailRepository {
final EmailDataSource emailDataSource; final EmailDataSource emailDataSource;
final HtmlDataSource _htmlDataSource;
EmailRepositoryImpl(this.emailDataSource); EmailRepositoryImpl(this.emailDataSource, this._htmlDataSource);
@override @override
Future<Email> getEmailContent(AccountId accountId, EmailId emailId) { Future<Email> getEmailContent(AccountId accountId, EmailId emailId) {
@@ -68,4 +70,9 @@ class EmailRepositoryImpl extends EmailRepository {
) { ) {
return emailDataSource.markAsStar(accountId, emails, markStarAction); return emailDataSource.markAsStar(accountId, emails, markStarAction);
} }
@override
Future<String> transformEmailContentToHtml(EmailContent emailContent) {
return _htmlDataSource.transformToHtml(emailContent);
}
} }
@@ -34,4 +34,6 @@ abstract class EmailRepository {
List<Email> emails, List<Email> emails,
MarkStarAction markStarAction MarkStarAction markStarAction
); );
Future<String> transformEmailContentToHtml(EmailContent emailContent);
} }
@@ -2,13 +2,13 @@ import 'package:core/core.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
class GetEmailContentSuccess extends UIState { class GetEmailContentSuccess extends UIState {
final List<EmailContent> emailContents; final EmailContent emailContent;
final List<Attachment> attachments; final List<Attachment> attachments;
GetEmailContentSuccess(this.emailContents, this.attachments); GetEmailContentSuccess(this.emailContent, this.attachments);
@override @override
List<Object?> get props => [emailContents, attachments]; List<Object?> get props => [emailContent, attachments];
} }
class GetEmailContentFailure extends FeatureFailure { class GetEmailContentFailure extends FeatureFailure {
@@ -15,9 +15,20 @@ 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.emailContentList, if (email.emailContentList.isNotEmpty) {
email.allAttachments)); final emailContent = email.emailContentList.first;
if (emailContent.type == EmailContentType.textHtml) {
final contentHtml = await emailRepository.transformEmailContentToHtml(emailContent);
yield Right<Failure, Success>(GetEmailContentSuccess(
EmailContent(emailContent.type, contentHtml),
email.allAttachments));
} else {
yield Right<Failure, Success>(GetEmailContentSuccess(emailContent, email.allAttachments));
}
} else {
yield Left(GetEmailContentFailure(null));
}
} catch (e) { } catch (e) {
yield Left(GetEmailContentFailure(e)); yield Left(GetEmailContentFailure(e));
} }
@@ -1,5 +0,0 @@
class EmailConstants {
static const HTML_TEXT = 'text/html';
static const PLAIN_TEXT = 'text/plain';
}
@@ -3,7 +3,10 @@ import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
import 'package:tmail_ui_user/features/email/data/network/email_api.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/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/repository/email_repository.dart';
@@ -22,7 +25,11 @@ class EmailBindings extends Bindings {
void dependencies() { void dependencies() {
Get.lazyPut(() => EmailDataSourceImpl(Get.find<EmailAPI>())); Get.lazyPut(() => EmailDataSourceImpl(Get.find<EmailAPI>()));
Get.lazyPut<EmailDataSource>(() => Get.find<EmailDataSourceImpl>()); Get.lazyPut<EmailDataSource>(() => Get.find<EmailDataSourceImpl>());
Get.lazyPut(() => EmailRepositoryImpl(Get.find<EmailDataSource>())); Get.lazyPut(() => HtmlDataSourceImpl(Get.find<HtmlAnalyzer>()));
Get.lazyPut<HtmlDataSource>(() => Get.find<HtmlDataSourceImpl>());
Get.lazyPut(() => EmailRepositoryImpl(
Get.find<EmailDataSource>(),
Get.find<HtmlDataSource>()));
Get.lazyPut<EmailRepository>(() => Get.find<EmailRepositoryImpl>()); Get.lazyPut<EmailRepository>(() => Get.find<EmailRepositoryImpl>());
Get.put(SendEmailInteractor(Get.find<EmailRepository>())); Get.put(SendEmailInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => GetEmailContentInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => GetEmailContentInteractor(Get.find<EmailRepository>()));
@@ -49,7 +49,7 @@ 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 emailContents = <EmailContent>[].obs; final emailContent = Rxn<EmailContent>();
final attachments = <Attachment>[].obs; final attachments = <Attachment>[].obs;
EmailId? _currentEmailId; EmailId? _currentEmailId;
@@ -129,9 +129,9 @@ class EmailController extends BaseController {
} }
void _getEmailContentSuccess(GetEmailContentSuccess success) { void _getEmailContentSuccess(GetEmailContentSuccess success) {
emailContents.value = success.emailContents; emailContent.value = success.emailContent;
attachments.value = success.attachments; attachments.value = success.attachments;
if (emailContents.isNotEmpty) { if (emailContent.value != null && emailContent.value!.type == EmailContentType.textHtml) {
dispatchState(Right(WebViewLoadingState())); dispatchState(Right(WebViewLoadingState()));
} }
} }
@@ -139,7 +139,7 @@ class EmailController extends BaseController {
void _clearEmailContent() { void _clearEmailContent() {
toggleDisplayEmailAddressAction(expandMode: ExpandMode.COLLAPSE); toggleDisplayEmailAddressAction(expandMode: ExpandMode.COLLAPSE);
attachmentsExpandMode.value = ExpandMode.COLLAPSE; attachmentsExpandMode.value = ExpandMode.COLLAPSE;
emailContents.clear(); emailContent.value = null;
attachments.clear(); attachments.clear();
} }
@@ -347,24 +347,17 @@ 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!,
emailContents: emailContents, emailContent: emailContent.value,
attachments: attachments, attachments: attachments,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role, mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role,
session: mailboxDashBoardController.sessionCurrent!, session: mailboxDashBoardController.sessionCurrent!,
+22 -11
View File
@@ -27,11 +27,6 @@ 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,
@@ -279,11 +274,27 @@ class EmailView extends GetView {
} }
Widget _buildEmailContent() { Widget _buildEmailContent() {
return Obx(() => emailController.emailContents.isNotEmpty return Obx(() {
? HtmlContentViewer( if (emailController.emailContent.value != null) {
message: emailController.emailContents.first.content, switch(emailController.emailContent.value!.type) {
onWebViewLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())), case EmailContentType.textHtml:
mailtoDelegate: (uri) async {}) return HtmlContentViewer(
: SizedBox.shrink()); contentHtml: emailController.emailContent.value!.content,
onLoadStart: (webController, uri) => emailController.dispatchState(Right(WebViewLoadingState())),
onLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())),
mailtoDelegate: (uri) async {});
case EmailContentType.textPlain:
return Padding(
padding: EdgeInsets.only(top: 16),
child: Text(
emailController.emailContent.value!.content,
style: TextStyle(fontSize: 14, color: AppColor.nameUserColor)));
case EmailContentType.other:
return SizedBox.shrink();
}
} else {
return SizedBox.shrink();
}
});
} }
} }
@@ -7,7 +7,7 @@ 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 List<EmailContent>? emailContents; final EmailContent? emailContent;
final List<Attachment>? attachments; final List<Attachment>? attachments;
final Session session; final Session session;
final UserProfile userProfile; final UserProfile userProfile;
@@ -17,7 +17,7 @@ class ComposerArguments with EquatableMixin {
ComposerArguments({ ComposerArguments({
this.emailActionType = EmailActionType.compose, this.emailActionType = EmailActionType.compose,
this.presentationEmail, this.presentationEmail,
this.emailContents, this.emailContent,
this.attachments, this.attachments,
this.mailboxRole, this.mailboxRole,
required this.session, required this.session,
@@ -29,7 +29,7 @@ class ComposerArguments with EquatableMixin {
List<Object?> get props => [ List<Object?> get props => [
emailActionType, emailActionType,
presentationEmail, presentationEmail,
emailContents, emailContent,
attachments, attachments,
mailboxRole, mailboxRole,
session, session,
@@ -4,7 +4,10 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; import 'package:tmail_ui_user/features/caching/state_cache_client.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
import 'package:tmail_ui_user/features/email/data/network/email_api.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/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/repository/email_repository.dart';
@@ -48,7 +51,11 @@ class ThreadBindings extends Bindings {
Get.lazyPut(() => ScrollController()); Get.lazyPut(() => ScrollController());
Get.lazyPut(() => EmailDataSourceImpl(Get.find<EmailAPI>())); Get.lazyPut(() => EmailDataSourceImpl(Get.find<EmailAPI>()));
Get.lazyPut<EmailDataSource>(() => Get.find<EmailDataSourceImpl>()); Get.lazyPut<EmailDataSource>(() => Get.find<EmailDataSourceImpl>());
Get.lazyPut(() => EmailRepositoryImpl(Get.find<EmailDataSource>())); Get.lazyPut(() => HtmlDataSourceImpl(Get.find<HtmlAnalyzer>()));
Get.lazyPut<HtmlDataSource>(() => Get.find<HtmlDataSourceImpl>());
Get.lazyPut(() => EmailRepositoryImpl(
Get.find<EmailDataSource>(),
Get.find<HtmlDataSource>()));
Get.lazyPut<EmailRepository>(() => Get.find<EmailRepositoryImpl>()); Get.lazyPut<EmailRepository>(() => Get.find<EmailRepositoryImpl>());
Get.lazyPut(() => MarkAsEmailReadInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => MarkAsEmailReadInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => MarkAsMultipleEmailReadInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => MarkAsMultipleEmailReadInteractor(Get.find<EmailRepository>()));
@@ -3,6 +3,7 @@ import 'package:core/presentation/utils/app_toast.dart';
import 'package:device_info/device_info.dart'; import 'package:device_info/device_info.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
class CoreBindings extends Bindings { class CoreBindings extends Bindings {
@@ -35,6 +36,7 @@ class CoreBindings extends Bindings {
void _bindingValidator() { void _bindingValidator() {
Get.put(HtmlMessagePurifier()); Get.put(HtmlMessagePurifier());
Get.put(HtmlAnalyzer());
} }
void _bindingToast() { void _bindingToast() {