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 bool keepWidthWhileLoading;
|
||||||
final ScrollController? scrollController;
|
final ScrollController? scrollController;
|
||||||
|
final bool enableQuoteToggle;
|
||||||
|
|
||||||
const HtmlContentViewerOnWeb({
|
const HtmlContentViewerOnWeb({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -47,6 +48,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
|
|||||||
this.keepWidthWhileLoading = false,
|
this.keepWidthWhileLoading = false,
|
||||||
this.contentPadding,
|
this.contentPadding,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
|
this.enableQuoteToggle = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -332,14 +334,18 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
|||||||
''';
|
''';
|
||||||
|
|
||||||
final htmlTemplate = HtmlUtils.generateHtmlDocument(
|
final htmlTemplate = HtmlUtils.generateHtmlDocument(
|
||||||
content: content,
|
content: widget.enableQuoteToggle
|
||||||
|
? HtmlUtils.addQuoteToggle(content)
|
||||||
|
: content,
|
||||||
minHeight: minHeight,
|
minHeight: minHeight,
|
||||||
minWidth: _minWidth,
|
minWidth: _minWidth,
|
||||||
styleCSS: HtmlTemplate.tooltipLinkCss,
|
styleCSS: HtmlTemplate.tooltipLinkCss
|
||||||
|
+ (widget.enableQuoteToggle ? HtmlUtils.quoteToggleStyle : ''),
|
||||||
javaScripts: webViewActionScripts
|
javaScripts: webViewActionScripts
|
||||||
+ scriptsDisableZoom
|
+ scriptsDisableZoom
|
||||||
+ HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage
|
+ HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage
|
||||||
+ HtmlInteraction.generateNormalizeImageScript(widget.widthContent),
|
+ HtmlInteraction.generateNormalizeImageScript(widget.widthContent)
|
||||||
|
+ (widget.enableQuoteToggle ? HtmlUtils.quoteToggleScript : ''),
|
||||||
direction: widget.direction,
|
direction: widget.direction,
|
||||||
contentPadding: widget.contentPadding,
|
contentPadding: widget.contentPadding,
|
||||||
useDefaultFont: widget.useDefaultFont,
|
useDefaultFont: widget.useDefaultFont,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
final double minHtmlContentHeight;
|
final double minHtmlContentHeight;
|
||||||
final double offsetHtmlContentHeight;
|
final double offsetHtmlContentHeight;
|
||||||
final bool keepAlive;
|
final bool keepAlive;
|
||||||
|
final bool enableQuoteToggle;
|
||||||
|
|
||||||
final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer;
|
final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer;
|
||||||
final OnMailtoDelegateAction? onMailtoDelegateAction;
|
final OnMailtoDelegateAction? onMailtoDelegateAction;
|
||||||
@@ -49,6 +50,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
this.minHtmlContentHeight = ConstantsUI.htmlContentMinHeight,
|
this.minHtmlContentHeight = ConstantsUI.htmlContentMinHeight,
|
||||||
this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight,
|
this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight,
|
||||||
this.keepAlive = false,
|
this.keepAlive = false,
|
||||||
|
this.enableQuoteToggle = false,
|
||||||
this.keepWidthWhileLoading = false,
|
this.keepWidthWhileLoading = false,
|
||||||
this.contentPadding,
|
this.contentPadding,
|
||||||
this.useDefaultFont = false,
|
this.useDefaultFont = false,
|
||||||
@@ -100,6 +102,9 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
|
|||||||
}
|
}
|
||||||
_customScriptsBuilder = StringBuffer();
|
_customScriptsBuilder = StringBuffer();
|
||||||
_customScriptsBuilder.write(HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage);
|
_customScriptsBuilder.write(HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage);
|
||||||
|
if (widget.enableQuoteToggle) {
|
||||||
|
_customScriptsBuilder.write(HtmlUtils.quoteToggleScript);
|
||||||
|
}
|
||||||
if (widget.initialWidth != null) {
|
if (widget.initialWidth != null) {
|
||||||
_customScriptsBuilder.write(HtmlInteraction.generateNormalizeImageScript(widget.initialWidth!));
|
_customScriptsBuilder.write(HtmlInteraction.generateNormalizeImageScript(widget.initialWidth!));
|
||||||
}
|
}
|
||||||
@@ -122,11 +127,14 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
|
|||||||
void _initialData() {
|
void _initialData() {
|
||||||
_actualHeight = widget.minHtmlContentHeight;
|
_actualHeight = widget.minHtmlContentHeight;
|
||||||
_htmlData = HtmlUtils.generateHtmlDocument(
|
_htmlData = HtmlUtils.generateHtmlDocument(
|
||||||
content: widget.contentHtml,
|
content: widget.enableQuoteToggle
|
||||||
|
? HtmlUtils.addQuoteToggle(widget.contentHtml)
|
||||||
|
: widget.contentHtml,
|
||||||
direction: widget.direction,
|
direction: widget.direction,
|
||||||
javaScripts: _customScriptsBuilder.toString(),
|
javaScripts: _customScriptsBuilder.toString(),
|
||||||
contentPadding: widget.contentPadding,
|
contentPadding: widget.contentPadding,
|
||||||
useDefaultFont: widget.useDefaultFont,
|
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;
|
if (result is! num) return;
|
||||||
|
|
||||||
final double maxContentHeight = result.toDouble();
|
final double maxContentHeight = result.toDouble();
|
||||||
if (maxContentHeight <= _actualHeight) return;
|
|
||||||
|
|
||||||
double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight;
|
double currentHeight = maxContentHeight + widget.offsetHtmlContentHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -579,4 +579,115 @@ class HtmlUtils {
|
|||||||
return false;
|
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,
|
contentPadding: 0,
|
||||||
useDefaultFont: true,
|
useDefaultFont: true,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
|
enableQuoteToggle: isInsideThreadDetailView,
|
||||||
),
|
),
|
||||||
if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame)
|
if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame)
|
||||||
PointerInterceptor(
|
PointerInterceptor(
|
||||||
@@ -447,6 +448,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
onMailtoDelegateAction: controller.openMailToLink,
|
onMailtoDelegateAction: controller.openMailToLink,
|
||||||
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
||||||
keepAlive: isInsideThreadDetailView,
|
keepAlive: isInsideThreadDetailView,
|
||||||
|
enableQuoteToggle: isInsideThreadDetailView,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -484,6 +486,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
useDefaultFont: true,
|
useDefaultFont: true,
|
||||||
onMailtoDelegateAction: controller.openMailToLink,
|
onMailtoDelegateAction: controller.openMailToLink,
|
||||||
keepAlive: isInsideThreadDetailView,
|
keepAlive: isInsideThreadDetailView,
|
||||||
|
enableQuoteToggle: isInsideThreadDetailView,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
|
|||||||
} else {
|
} else {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -224,6 +225,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
|
|||||||
TMailButtonWidget.fromIcon(
|
TMailButtonWidget.fromIcon(
|
||||||
icon: _imagePaths.icChevronDown,
|
icon: _imagePaths.icChevronDown,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
onTapActionCallback: () => setState(() => _isDisplayAll = true),
|
onTapActionCallback: () => setState(() => _isDisplayAll = true),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -290,7 +292,9 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
|
|||||||
prefixEmailAddress: prefixEmailAddress,
|
prefixEmailAddress: prefixEmailAddress,
|
||||||
responsiveUtils: _responsiveUtils,
|
responsiveUtils: _responsiveUtils,
|
||||||
),
|
),
|
||||||
..._buildRecipientsTag(listEmailAddress: listEmailAddress)
|
..._buildRecipientsTag(listEmailAddress: listEmailAddress).map(
|
||||||
|
(child) => Align(alignment: AlignmentDirectional.topStart, child: child),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user