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'[a-zA-Z][^>]*>')); // 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 = '''
+
+ ''';
+
+ 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 = '''
+
+ ''';
+
+ 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 = '''
+
+ ''';
+
+ 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 = '''
+
+
Hello World
+
+ Previous message
+
+
+ ''';
+
+ 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),
+ ),
];
}