From f86392c854b816c26f3b775abc16072e6a6c4f52 Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 28 May 2025 14:43:37 +0700 Subject: [PATCH] TF-3720 Thread Detail Collapse quote reply Revert "fixup! TF-3720 Thread Detail Collapse quote reply" This reverts commit 2104530977664f892702fba5e5bc6fb3e5768b19. TF-3720 Thread Detail adjust receiver size TF-3720 Thread Detail refactor email receiver alignment --- .../html_content_viewer_on_web_widget.dart | 12 +- .../html_content_viewer_widget.dart | 11 +- core/lib/utils/html/html_utils.dart | 111 ++++++++++++++++++ core/test/utils/html/html_utils_test.dart | 81 +++++++++++++ .../email/presentation/email_view.dart | 3 + .../widgets/email_receiver_widget.dart | 6 +- 6 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 core/test/utils/html/html_utils_test.dart 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 a29f127fe..0ec93703d 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 @@ -33,6 +33,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget { final bool keepWidthWhileLoading; final ScrollController? scrollController; + final bool enableQuoteToggle; const HtmlContentViewerOnWeb({ Key? key, @@ -47,6 +48,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget { this.keepWidthWhileLoading = false, this.contentPadding, this.scrollController, + this.enableQuoteToggle = false, }) : super(key: key); @override @@ -332,14 +334,18 @@ class _HtmlContentViewerOnWebState extends State { '''; final htmlTemplate = HtmlUtils.generateHtmlDocument( - content: content, + content: widget.enableQuoteToggle + ? HtmlUtils.addQuoteToggle(content) + : content, minHeight: minHeight, minWidth: _minWidth, - styleCSS: HtmlTemplate.tooltipLinkCss, + styleCSS: HtmlTemplate.tooltipLinkCss + + (widget.enableQuoteToggle ? HtmlUtils.quoteToggleStyle : ''), javaScripts: webViewActionScripts + scriptsDisableZoom + HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage - + HtmlInteraction.generateNormalizeImageScript(widget.widthContent), + + HtmlInteraction.generateNormalizeImageScript(widget.widthContent) + + (widget.enableQuoteToggle ? HtmlUtils.quoteToggleScript : ''), direction: widget.direction, contentPadding: widget.contentPadding, useDefaultFont: widget.useDefaultFont, 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 a3fe9fcdd..5ccac6cac 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 @@ -33,6 +33,7 @@ class HtmlContentViewer extends StatefulWidget { final double minHtmlContentHeight; final double offsetHtmlContentHeight; final bool keepAlive; + final bool enableQuoteToggle; final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer; final OnMailtoDelegateAction? onMailtoDelegateAction; @@ -49,6 +50,7 @@ class HtmlContentViewer extends StatefulWidget { this.minHtmlContentHeight = ConstantsUI.htmlContentMinHeight, this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight, this.keepAlive = false, + this.enableQuoteToggle = false, this.keepWidthWhileLoading = false, this.contentPadding, this.useDefaultFont = false, @@ -100,6 +102,9 @@ class HtmlContentViewState extends State with AutomaticKeepAl } _customScriptsBuilder = StringBuffer(); _customScriptsBuilder.write(HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage); + if (widget.enableQuoteToggle) { + _customScriptsBuilder.write(HtmlUtils.quoteToggleScript); + } if (widget.initialWidth != null) { _customScriptsBuilder.write(HtmlInteraction.generateNormalizeImageScript(widget.initialWidth!)); } @@ -122,11 +127,14 @@ class HtmlContentViewState extends State with AutomaticKeepAl void _initialData() { _actualHeight = widget.minHtmlContentHeight; _htmlData = HtmlUtils.generateHtmlDocument( - content: widget.contentHtml, + content: widget.enableQuoteToggle + ? HtmlUtils.addQuoteToggle(widget.contentHtml) + : widget.contentHtml, direction: widget.direction, javaScripts: _customScriptsBuilder.toString(), contentPadding: widget.contentPadding, useDefaultFont: widget.useDefaultFont, + styleCSS: widget.enableQuoteToggle ? HtmlUtils.quoteToggleStyle : null ); } @@ -218,7 +226,6 @@ class HtmlContentViewState extends State with AutomaticKeepAl if (result is! num) return; final double maxContentHeight = result.toDouble(); - if (maxContentHeight <= _actualHeight) return; double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight; diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 338559993..801da652c 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -579,4 +579,115 @@ class HtmlUtils { return false; } } + + static String addQuoteToggle(String htmlString) { + final likelyHtml = htmlString.contains(RegExp(r'<[a-zA-Z][^>]*>')) && // Contains a start tag + htmlString.contains(RegExp(r']*>')); // Contains an end tag + + if (!likelyHtml) { + return htmlString; // Not likely HTML, return original + } + + try { + html.DomParser().parseFromString(htmlString, 'text/html'); + } catch (e) { + return htmlString; + } + + final containerElement = '
$htmlString
'; + + final containerDom = html.DomParser().parseFromString(containerElement, 'text/html'); + html.ElementList blockquotes = containerDom.querySelectorAll('.quote-toggle-container > blockquote'); + int currentSearchLevel = 1; + + while (blockquotes.isEmpty) { + // Finish searching at level [currentSearchLevel] + if (currentSearchLevel >= 3) return htmlString; + // No blockquote elements found on first level, try another level + blockquotes = containerDom.querySelectorAll('.quote-toggle-container${' > div' * currentSearchLevel} > blockquote'); + currentSearchLevel++; + } + + final lastBlockquote = blockquotes.last; + + const buttonHtmlContent = ''' + '''; + + // Parse the button HTML content as a fragment + final tempDoc = + html.DomParser().parseFromString(buttonHtmlContent, 'text/html'); + + final buttonElement = tempDoc.querySelector('.quote-toggle-button'); + + // Insert the button before the last blockquote + if (lastBlockquote.parentNode != null && buttonElement != null) { + lastBlockquote.parentNode!.insertBefore(buttonElement, lastBlockquote); + } + + // Return the modified HTML string + return containerDom.documentElement?.outerHtml ?? htmlString; + } + + static String get quoteToggleStyle => ''' + '''; + + static String get quoteToggleScript => ''' + '''; } diff --git a/core/test/utils/html/html_utils_test.dart b/core/test/utils/html/html_utils_test.dart new file mode 100644 index 000000000..909956714 --- /dev/null +++ b/core/test/utils/html/html_utils_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:core/utils/html/html_utils.dart'; +import 'package:universal_html/html.dart' as html; + +void main() { + group('HtmlUtils addQuoteToggle tests', () { + test('Should add toggle button to HTML with single blockquote', () { + const htmlInput = ''' +
+
+

Quoted text

+
+
+ '''; + + final result = HtmlUtils.addQuoteToggle(htmlInput); + + expect(result, contains('quote-toggle-container')); + expect(result, contains('quote-toggle-button')); + }); + + test('Should handle nested blockquotes by modifying deepest level', () { + const htmlInput = ''' +
+
+
+
+

Nested quote

+
+
+
+
+ '''; + + final result = HtmlUtils.addQuoteToggle(htmlInput); + final document = html.DomParser().parseFromString(result, 'text/html'); + + expect( + document.querySelector('.quote-toggle-button')?.nextElementSibling, + document.querySelector('.outer'), + ); + }); + + test('Should return original string when input is not HTML', () { + const plainText = 'This is just plain text without any HTML tags'; + final result = HtmlUtils.addQuoteToggle(plainText); + expect(result, plainText); + }); + + test('Should handle invalid HTML gracefully', () { + const malformedHtml = ''' +
+
+

Unclosed tag +

+
+ '''; + + final result = HtmlUtils.addQuoteToggle(malformedHtml); + expect(result, isNot(equals(malformedHtml))); + expect(result, contains('quote-toggle-button')); + }); + + test('Should preserve existing content when adding toggle', () { + const htmlInput = ''' + + '''; + + final result = HtmlUtils.addQuoteToggle(htmlInput); + final container = html.DivElement()..innerHtml = result; + expect(container.querySelector('.email-body'), isNotNull); + expect(container.querySelector('p')?.text, contains('Hello World')); + expect(container.querySelector('blockquote p')?.text, contains('Previous message')); + }); + }); +} diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index c7bf1a771..9cfb45acc 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -408,6 +408,7 @@ class EmailView extends GetWidget { contentPadding: 0, useDefaultFont: true, scrollController: scrollController, + enableQuoteToggle: isInsideThreadDetailView, ), if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame) PointerInterceptor( @@ -447,6 +448,7 @@ class EmailView extends GetWidget { onMailtoDelegateAction: controller.openMailToLink, onHtmlContentClippedAction: controller.onHtmlContentClippedAction, keepAlive: isInsideThreadDetailView, + enableQuoteToggle: isInsideThreadDetailView, ); }), ), @@ -484,6 +486,7 @@ class EmailView extends GetWidget { useDefaultFont: true, onMailtoDelegateAction: controller.openMailToLink, keepAlive: isInsideThreadDetailView, + enableQuoteToggle: isInsideThreadDetailView, ); }) ); diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index 86c319f21..8571d26b6 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -189,6 +189,7 @@ class _EmailReceiverWidgetState extends State { } else { return Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 40, @@ -224,6 +225,7 @@ class _EmailReceiverWidgetState extends State { TMailButtonWidget.fromIcon( icon: _imagePaths.icChevronDown, backgroundColor: Colors.transparent, + padding: const EdgeInsets.all(3), onTapActionCallback: () => setState(() => _isDisplayAll = true), ) ] @@ -290,7 +292,9 @@ class _EmailReceiverWidgetState extends State { prefixEmailAddress: prefixEmailAddress, responsiveUtils: _responsiveUtils, ), - ..._buildRecipientsTag(listEmailAddress: listEmailAddress) + ..._buildRecipientsTag(listEmailAddress: listEmailAddress).map( + (child) => Align(alignment: AlignmentDirectional.topStart, child: child), + ), ]; }