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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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>''';
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user