From 6626223926d2ab0e4ffb94b76eadab8be6819df6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 12 Sep 2023 16:32:07 +0700 Subject: [PATCH] TF-2116 Support show collapse/expand signature for email view (cherry picked from commit 4f0bb84593af28473b34466570b9ecb75c720617) --- .../utils/html_transformer/html_template.dart | 39 +++++ .../html_transformer/html_transform.dart | 6 +- .../utils/html_transformer/html_utils.dart | 86 +++++++++++ .../transform_configuration.dart | 19 ++- core/lib/presentation/utils/icon_utils.dart | 8 + .../html_content_viewer_on_web_widget.dart | 137 ++++++++++++------ .../html_content_viewer_widget.dart | 100 +++++++------ .../presentation/composer_controller.dart | 2 +- 8 files changed, 294 insertions(+), 103 deletions(-) diff --git a/core/lib/presentation/utils/html_transformer/html_template.dart b/core/lib/presentation/utils/html_transformer/html_template.dart index 44e303de1..8b6952ceb 100644 --- a/core/lib/presentation/utils/html_transformer/html_template.dart +++ b/core/lib/presentation/utils/html_transformer/html_template.dart @@ -53,6 +53,45 @@ String generateHtml(String content, { } ''' : ''} ${styleCSS ?? ''} + + .tmail-signature { + text-align: ${direction == TextDirection.rtl ? 'right' : 'left'}; + margin: 16px 0px 16px 0px; + } + + .tmail-signature-button, + .tmail-signature-button * { + box-sizing: border-box; + } + + .tmail-signature-button { + padding: 6px 40px 6px 16px; + border-radius: 4px; + color: #fff; + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.0003 11.8319L5.53383 8.1098C5.18027 7.81516 4.6548 7.86293 4.36016 8.21649C4.06553 8.57006 4.1133 9.09553 4.46686 9.39016L9.46686 13.5568C9.7759 13.8144 10.2248 13.8144 10.5338 13.5568L15.5338 9.39016C15.8874 9.09553 15.9352 8.57006 15.6405 8.21649C15.3459 7.86293 14.8204 7.81516 14.4669 8.1098L10.0003 11.8319Z' fill='%23AEAEC0'/%3E%3C/svg%3E%0A"); + background-repeat: no-repeat; + background-position: right 16px center; + background-color: #FFFFFF; + border-radius: 36px; + border-style: solid; + border-color: var(--m-3-syslight-outline-shadow-outline-variant, #cac4d0); + border-width: 0.5px; + flex-direction: row; + gap: 8px; + align-items: center; + justify-content: flex-start; + flex-shrink: 0; + position: relative; + cursor: pointer; + color: var(--m-3-syslight-tetirary-tertiary, #8c9caf); + text-align: left; + font: var(--m-3-body-large-2, 400 17px/24px "Inter", sans-serif); + } + + .tmail-signature-content { + padding: 12px; + overflow: hidden; + } diff --git a/core/lib/presentation/utils/html_transformer/html_transform.dart b/core/lib/presentation/utils/html_transformer/html_transform.dart index 1d523af59..ce57dcb04 100644 --- a/core/lib/presentation/utils/html_transformer/html_transform.dart +++ b/core/lib/presentation/utils/html_transformer/html_transform.dart @@ -13,10 +13,9 @@ class HtmlTransform { /// Transforms this message to HTML code. Future transformToHtml({ required String htmlContent, + required TransformConfiguration transformConfiguration, Map? mapCidImageDownloadUrl, - TransformConfiguration? transformConfiguration, }) async { - transformConfiguration ??= TransformConfiguration.create(); final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); final document = await transformer.toDocument( message: htmlContent, @@ -28,9 +27,8 @@ class HtmlTransform { /// Transforms this message to Text Plain. String transformToTextPlain({ required String content, - TransformConfiguration? transformConfiguration + required TransformConfiguration transformConfiguration }) { - transformConfiguration ??= TransformConfiguration.create(); final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); final message = transformer.toMessage(content); return message; diff --git a/core/lib/presentation/utils/html_transformer/html_utils.dart b/core/lib/presentation/utils/html_transformer/html_utils.dart index 3b7f1ec4d..ffd33edc4 100644 --- a/core/lib/presentation/utils/html_transformer/html_utils.dart +++ b/core/lib/presentation/utils/html_transformer/html_utils.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/utils/html_transformer/html_event_action.dart'; +import 'package:core/presentation/utils/icon_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; @@ -101,6 +102,83 @@ class HtmlUtils { '''; + static const runScriptsCollapsedExpandedSignature = ''' + const signatureNode = document.querySelector('.tmail-content > .tmail-signature'); + if (signatureNode) { + const signatureContainer = document.createElement('div'); + signatureContainer.setAttribute('class', 'tmail-signature'); + + const signatureContent = document.createElement('div'); + signatureContent.setAttribute('class', 'tmail-signature-content'); + signatureContent.innerHTML = signatureNode.innerHTML; + signatureContent.style.display = 'none'; + + const signatureButton = document.createElement('button'); + signatureButton.setAttribute('class', 'tmail-signature-button'); + signatureButton.textContent = 'Signature'; + signatureButton.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + signatureButton.setAttribute('onclick', 'handleOnClickSignature()'); + + signatureContainer.appendChild(signatureButton); + signatureContainer.appendChild(signatureContent); + + if (signatureNode.outerHTML) { + signatureNode.outerHTML = signatureContainer.outerHTML; + } else { + signatureNode.parentNode.replaceChild(signatureContainer, signatureNode); + } + } + '''; + + static const scriptCollapsedExpandedSignatureOnMobile = ''' + + '''; + static String customCssStyleHtmlEditor({TextDirection direction = TextDirection.ltr}) { if (PlatformInfo.isWeb) { return ''' @@ -108,6 +186,10 @@ class HtmlUtils { .note-editable { direction: ${direction.name}; } + + .note-editable .tmail-signature { + text-align: ${direction == TextDirection.rtl ? 'right' : 'left'}; + } '''; } else if (PlatformInfo.isMobile) { @@ -115,6 +197,10 @@ class HtmlUtils { #editor { direction: ${direction.name}; } + + #editor .tmail-signature { + text-align: ${direction == TextDirection.rtl ? 'right' : 'left'}; + } '''; } else { return ''; diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index af4af62ad..df0576a43 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -34,6 +34,7 @@ class TransformConfiguration { const ReplaceLazyLoadImageTransformer(), if (PlatformInfo.isWeb) const RemoveTooltipLinkTransformer(), + const SignatureTransformer(), ] ); @@ -53,7 +54,22 @@ class TransformConfiguration { const BlockQuotedTransformer(), const BlockCodeTransformer(), const AddTargetBlankInTagATransformer(), - const ImageTransformer(useLoadingAttribute: true) + const ImageTransformer(useLoadingAttribute: true), + const SignatureTransformer(), + ] + ); + + factory TransformConfiguration.forComposeEmail() => TransformConfiguration.create( + customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(), + const SignatureTransformer(), + ], + customTextTransformers: [ + const SanitizeAutolinkHtmlTransformers() ] ); @@ -97,7 +113,6 @@ class TransformConfiguration { static const List standardDomTransformers = [ RemoveScriptTransformer(), - SignatureTransformer(), BlockQuotedTransformer(), BlockCodeTransformer(), AddTargetBlankInTagATransformer(), diff --git a/core/lib/presentation/utils/icon_utils.dart b/core/lib/presentation/utils/icon_utils.dart index 49ea41ccd..e865d8738 100644 --- a/core/lib/presentation/utils/icon_utils.dart +++ b/core/lib/presentation/utils/icon_utils.dart @@ -3,4 +3,12 @@ import 'package:core/utils/platform_info.dart'; class IconUtils { static const double defaultIconSize = PlatformInfo.isWeb ? 20.0 : 24.0; + + static const String chevronUpSVGIconUrlEncoded = ''' + url("data:image/svg+xml,%3Csvg class='chevron-down' width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M14.5352 11.9709C14.8347 12.2276 15.2857 12.193 15.5424 11.8934C15.7991 11.5939 15.7644 11.143 15.4649 10.8863L10.4649 6.60054C10.1974 6.37127 9.8027 6.37127 9.53521 6.60054L4.53521 10.8863C4.23569 11.143 4.201 11.5939 4.45773 11.8934C4.71446 12.193 5.16539 12.2276 5.46491 11.9709L10.0001 8.08364L14.5352 11.9709Z' fill='%23AEAEC0' /%3E%3C/svg%3E") + '''; + + static const String chevronDownSVGIconUrlEncoded = ''' + url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.0003 11.8319L5.53383 8.1098C5.18027 7.81516 4.6548 7.86293 4.36016 8.21649C4.06553 8.57006 4.1133 9.09553 4.46686 9.39016L9.46686 13.5568C9.7759 13.8144 10.2248 13.8144 10.5338 13.5568L15.5338 9.39016C15.8874 9.09553 15.9352 8.57006 15.6405 8.21649C15.3459 7.86293 14.8204 7.81516 14.4669 8.1098L10.0003 11.8319Z' fill='%23AEAEC0'/%3E%3C/svg%3E%0A") + '''; } \ No newline at end of file diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart index d3d98ac12..7f5dbc9c0 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/html_transformer/html_template.dart'; import 'package:core/presentation/utils/html_transformer/html_utils.dart'; +import 'package:core/presentation/utils/icon_utils.dart'; import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/cupertino.dart'; @@ -54,6 +55,7 @@ class _HtmlContentViewerOnWebState extends State { bool _isLoading = true; double minHeight = 100; double minWidth = 300; + final jsonEncoder = const JsonEncoder(); @override void initState() { @@ -101,6 +103,10 @@ class _HtmlContentViewerOnWebState extends State { if (e && e.data && e.data.includes("toIframe:")) { var data = JSON.parse(e.data); if (data["view"].includes("$createdViewId")) { + if (data["type"].includes("showSignature")) { + ${HtmlUtils.runScriptsCollapsedExpandedSignature} + } + if (data["type"].includes("getHeight")) { var height = document.body.scrollHeight; window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: htmlHeight", "height": height}), "*"); @@ -143,6 +149,23 @@ class _HtmlContentViewerOnWebState extends State { return url.protocol === "mailto:"; } + + function handleOnClickSignature() { + console.log("handleOnClickSignature"); + const contentElement = document.querySelector('.tmail-content > .tmail-signature > .tmail-signature-content'); + const buttonElement = document.querySelector('.tmail-content > .tmail-signature > .tmail-signature-button'); + console.log("contentElement: " + contentElement); + console.log("buttonElement: " + buttonElement); + if (contentElement && buttonElement) { + if (contentElement.style.display === 'block') { + contentElement.style.display = 'none'; + buttonElement.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + } else { + contentElement.style.display = 'block'; + buttonElement.style.backgroundImage = `${IconUtils.chevronUpSVGIconUrlEncoded}`; + } + } + } '''; @@ -188,15 +211,9 @@ class _HtmlContentViewerOnWebState extends State { ..style.width = '100%' ..style.height = '100%' ..onLoad.listen((event) async { - final dataGetHeight = {'type': 'toIframe: getHeight', 'view' : createdViewId}; - final dataGetWidth = {'type': 'toIframe: getWidth', 'view' : createdViewId}; - - const jsonEncoder = JsonEncoder(); - final jsonGetHeight = jsonEncoder.convert(dataGetHeight); - final jsonGetWidth = jsonEncoder.convert(dataGetWidth); - - html.window.postMessage(jsonGetHeight, '*'); - html.window.postMessage(jsonGetWidth, '*'); + _sendMessageToWebViewForGetHeight(); + _sendMessageToWebViewForGetWidth(); + _sendMessageToWebViewForShowSignature(); html.window.onMessage.listen((event) { var data = json.decode(event.data); @@ -232,7 +249,6 @@ class _HtmlContentViewerOnWebState extends State { if (data['type'] != null && data['type'].contains('toDart: OpenLink') && data['view'] == createdViewId) { final link = data['url']; if (link != null && mounted) { - log('_HtmlContentViewerOnWebState::_setUpWeb(): OpenLink: $link'); final urlString = link as String; if (urlString.startsWith('mailto:')) { widget.mailtoDelegate?.call(Uri.parse(urlString)); @@ -253,45 +269,76 @@ class _HtmlContentViewerOnWebState extends State { @override Widget build(BuildContext context) { - return Stack( - children: [ - SizedBox( - height: actualHeight, - width: actualWidth, - child: _buildWebView(), - ), - if (_isLoading) Align(alignment: Alignment.topCenter, child: _buildLoadingView()) - ], - ); + return LayoutBuilder(builder: (context, constraint) { + minHeight = math.max(constraint.maxHeight, minHeight); + return Stack( + children: [ + if (_htmlData?.isNotEmpty == false) + const SizedBox.shrink() + else + FutureBuilder( + future: webInit, + builder: (context, snapshot) { + if (snapshot.hasData) { + return SizedBox( + height: actualHeight, + width: actualWidth, + child: HtmlElementView( + key: ValueKey(_htmlData), + viewType: createdViewId, + ), + ); + } else { + return const SizedBox.shrink(); + } + } + ), + if (_isLoading) + const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CupertinoActivityIndicator( + color: AppColor.colorLoading + ) + ) + ) + ) + ], + ); + }); } - Widget _buildLoadingView() { - return const Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - width: 30, - height: 30, - child: CupertinoActivityIndicator(color: AppColor.colorLoading))); + void _sendMessageToWebViewForGetHeight() { + final dataGetHeight = { + 'type': 'toIframe: getHeight', + 'view' : createdViewId + }; + final jsonGetHeight = jsonEncoder.convert(dataGetHeight); + + html.window.postMessage(jsonGetHeight, '*'); } - Widget _buildWebView() { - final htmlData = _htmlData; - if (htmlData == null || htmlData.isEmpty) { - return Container(); - } + void _sendMessageToWebViewForGetWidth() { + final dataGetWidth = { + 'type': 'toIframe: getWidth', + 'view' : createdViewId + }; + final jsonGetWidth = jsonEncoder.convert(dataGetWidth); - return FutureBuilder( - future: webInit, - builder: (context, snapshot) { - if (snapshot.hasData) { - return HtmlElementView( - key: ValueKey(htmlData), - viewType: createdViewId, - ); - } else { - return Container(); - } - } - ); + html.window.postMessage(jsonGetWidth, '*'); + } + + void _sendMessageToWebViewForShowSignature() { + final dataShowSignature = { + 'type': 'toIframe: showSignature', + 'view' : createdViewId + }; + final jsonShowSignature = jsonEncoder.convert(dataShowSignature); + + html.window.postMessage(jsonShowSignature, '*'); } } \ No newline at end of file diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index 56e60c9fb..0d64f577e 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -68,7 +68,7 @@ class _HtmlContentViewState extends State { _htmlData = generateHtml( widget.contentHtml, direction: widget.direction, - javaScripts: HtmlUtils.scriptLazyLoadImage, + javaScripts: HtmlUtils.scriptLazyLoadImage + HtmlUtils.scriptCollapsedExpandedSignatureOnMobile, ); } @@ -81,7 +81,7 @@ class _HtmlContentViewState extends State { _htmlData = generateHtml( widget.contentHtml, direction: widget.direction, - javaScripts: HtmlUtils.scriptLazyLoadImage, + javaScripts: HtmlUtils.scriptLazyLoadImage + HtmlUtils.scriptCollapsedExpandedSignatureOnMobile, ); } } @@ -91,61 +91,58 @@ class _HtmlContentViewState extends State { return LayoutBuilder(builder: (context, constraints) { return Stack( children: [ - SizedBox( - height: actualHeight, - width: constraints.maxWidth, - child: _buildWebView()), + if (_htmlData?.isNotEmpty == false) + const SizedBox.shrink() + else + SizedBox( + height: actualHeight, + width: constraints.maxWidth, + child: InAppWebView( + key: ValueKey(_htmlData), + initialSettings: InAppWebViewSettings( + transparentBackground: true, + ), + onWebViewCreated: (controller) async { + _webViewController = controller; + await controller.loadData(data: _htmlData ?? ''); + widget.onCreated?.call(controller); + }, + onLoadStop: _onLoadStop, + shouldOverrideUrlLoading: _shouldOverrideUrlLoading, + gestureRecognizers: { + Factory(() => LongPressGestureRecognizer()), + if (Platform.isIOS && horizontalGestureActivated) + Factory(() => HorizontalDragGestureRecognizer()), + if (Platform.isAndroid) + Factory(() => ScaleGestureRecognizer()), + }, + onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0) + ), + ), if (_isLoading) - Align( - alignment: Alignment.center, - child: _buildLoadingView() + const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CupertinoActivityIndicator( + color: AppColor.colorLoading + ) + ) + ) ) ], ); }); } - Widget _buildLoadingView() { - return const Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - width: 30, - height: 30, - child: CupertinoActivityIndicator(color: AppColor.colorLoading))); - } - - Widget _buildWebView() { - final htmlData = _htmlData; - if (htmlData == null || htmlData.isEmpty) { - return Container(); - } - return InAppWebView( - key: ValueKey(htmlData), - initialSettings: InAppWebViewSettings( - transparentBackground: true, - ), - onWebViewCreated: (controller) async { - _webViewController = controller; - controller.loadData(data: htmlData); - widget.onCreated?.call(controller); - }, - onLoadStop: _onLoadStop, - shouldOverrideUrlLoading: _shouldOverrideUrlLoading, - gestureRecognizers: { - Factory(() => LongPressGestureRecognizer()), - if (Platform.isIOS && horizontalGestureActivated) - Factory(() => HorizontalDragGestureRecognizer()), - if (Platform.isAndroid) - Factory(() => ScaleGestureRecognizer()), - }, - onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0) - ); - } - void _onLoadStop(InAppWebViewController controller, WebUri? webUri) async { await Future.wait([ _setActualHeightView(), _setActualWidthView(), + _showSignature(), ]); _hideLoadingProgress(); @@ -169,7 +166,6 @@ class _HtmlContentViewState extends State { Future _setActualHeightView() async { final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight'); - log('_HtmlContentViewState::_setActualHeightView(): scrollHeight: $scrollHeight'); if (scrollHeight != null && mounted) { final scrollHeightWithBuffer = scrollHeight + 30.0; if (scrollHeightWithBuffer > minHeight) { @@ -192,12 +188,8 @@ class _HtmlContentViewState extends State { if (result.length == 2) { final scrollWidth = result[0]; final offsetWidth = result[1]; - log('_HtmlContentViewState::_setActualWidthView():scrollWidth: $scrollWidth'); - log('_HtmlContentViewState::_setActualWidthView():offsetWidth: $offsetWidth'); - if (scrollWidth != null && offsetWidth != null && mounted) { final isScrollActivated = scrollWidth.round() == offsetWidth.round(); - log('_HtmlContentViewState::_setActualWidthView():isScrollActivated: $isScrollActivated'); if (isScrollActivated) { setState(() { horizontalGestureActivated = false; @@ -215,6 +207,12 @@ class _HtmlContentViewState extends State { } } + Future _showSignature() async { + await _webViewController.evaluateJavascript( + source: 'showSignature();' + ); + } + void _hideLoadingProgress() { if (mounted && _isLoading) { setState(() { diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 0fec1adf2..7a7e86946 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1253,7 +1253,7 @@ class ComposerController extends BaseController { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration; + TransformConfiguration transformConfiguration = TransformConfiguration.forComposeEmail(); if (isDraftEmail) { transformConfiguration = TransformConfiguration.forDraftsEmail(); } else if (PlatformInfo.isWeb) {