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
This commit is contained in:
DatDang
2025-05-28 14:43:37 +07:00
committed by Dat H. Pham
parent d210a892f2
commit f86392c854
6 changed files with 218 additions and 6 deletions
@@ -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<HtmlContentViewerOnWeb> {
''';
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,
@@ -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<HtmlContentViewer> 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<HtmlContentViewer> 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<HtmlContentViewer> with AutomaticKeepAl
if (result is! num) return;
final double maxContentHeight = result.toDouble();
if (maxContentHeight <= _actualHeight) return;
double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight;
+111
View File
@@ -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 = '<div class="quote-toggle-container" >$htmlString</div>';
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 = '''
<button class="quote-toggle-button collapsed" title="Show trimmed content">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</button>''';
// 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 => '''
<style>
.quote-toggle-button + blockquote {
display: block; /* Default display */
}
.quote-toggle-button.collapsed + blockquote {
display: none;
}
.quote-toggle-button {
display: flex;
align-items: center;
gap: 2px;
background-color: #e8eaed;
padding: 4px 8px;
margin: 8px 0;
border-radius: 9999px;
transition: background-color 0.2s ease-in-out;
border: none;
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE 10+ */
user-select: none; /* Standard syntax */
-webkit-user-drag: none; /* Prevent dragging on WebKit browsers (e.g., Chrome, Safari) */
}
.quote-toggle-button:hover {
background-color: #cdcdcd !important;
}
.dot {
width: 4px;
height: 4px;
background-color: #4b5563;
border-radius: 9999px;
}
</style>''';
static String get quoteToggleScript => '''
<script>
document.addEventListener('DOMContentLoaded', function() {
const buttons = document.querySelectorAll('.quote-toggle-button');
buttons.forEach(button => {
button.onclick = function() {
const blockquote = this.nextElementSibling;
if (blockquote && blockquote.tagName === 'BLOCKQUOTE') {
this.classList.toggle('collapsed');
if (this.classList.contains('collapsed')) {
this.title = 'Show trimmed content';
} else {
this.title = 'Hide expanded content';
}
}
};
});
});
</script>''';
}
+81
View File
@@ -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 = '''
<div>
<blockquote>
<p>Quoted text</p>
</blockquote>
</div>
''';
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 = '''
<div>
<blockquote class="outer">
<div>
<blockquote class="inner">
<p>Nested quote</p>
</blockquote>
</div>
</blockquote>
</div>
''';
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 = '''
<div>
<blockquote>
<p>Unclosed tag
</blockquote>
</div>
''';
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 = '''
<div class="email-body">
<p>Hello World</p>
<blockquote>
<p>Previous message</p>
</blockquote>
</div>
''';
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'));
});
});
}
@@ -408,6 +408,7 @@ class EmailView extends GetWidget<SingleEmailController> {
contentPadding: 0,
useDefaultFont: true,
scrollController: scrollController,
enableQuoteToggle: isInsideThreadDetailView,
),
if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame)
PointerInterceptor(
@@ -447,6 +448,7 @@ class EmailView extends GetWidget<SingleEmailController> {
onMailtoDelegateAction: controller.openMailToLink,
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
keepAlive: isInsideThreadDetailView,
enableQuoteToggle: isInsideThreadDetailView,
);
}),
),
@@ -484,6 +486,7 @@ class EmailView extends GetWidget<SingleEmailController> {
useDefaultFont: true,
onMailtoDelegateAction: controller.openMailToLink,
keepAlive: isInsideThreadDetailView,
enableQuoteToggle: isInsideThreadDetailView,
);
})
);
@@ -189,6 +189,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
} else {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 40,
@@ -224,6 +225,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
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<EmailReceiverWidget> {
prefixEmailAddress: prefixEmailAddress,
responsiveUtils: _responsiveUtils,
),
..._buildRecipientsTag(listEmailAddress: listEmailAddress)
..._buildRecipientsTag(listEmailAddress: listEmailAddress).map(
(child) => Align(alignment: AlignmentDirectional.topStart, child: child),
),
];
}