TF-3601 Fix email view gone blank with large html content in iOS
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -19,6 +19,7 @@ typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated);
|
|||||||
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
|
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
|
||||||
typedef OnPreviewEMLDelegateAction = Future<void> Function(Uri? uri);
|
typedef OnPreviewEMLDelegateAction = Future<void> Function(Uri? uri);
|
||||||
typedef OnDownloadAttachmentDelegateAction = Future<void> Function(Uri? uri);
|
typedef OnDownloadAttachmentDelegateAction = Future<void> Function(Uri? uri);
|
||||||
|
typedef OnHtmlContentClippedAction = Function(bool isClipped);
|
||||||
|
|
||||||
class HtmlContentViewer extends StatefulWidget {
|
class HtmlContentViewer extends StatefulWidget {
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
|
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
|
||||||
final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction;
|
final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction;
|
||||||
final OnDownloadAttachmentDelegateAction? onDownloadAttachmentDelegateAction;
|
final OnDownloadAttachmentDelegateAction? onDownloadAttachmentDelegateAction;
|
||||||
|
final OnHtmlContentClippedAction? onHtmlContentClippedAction;
|
||||||
|
|
||||||
const HtmlContentViewer({
|
const HtmlContentViewer({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -48,6 +50,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
this.onScrollHorizontalEnd,
|
this.onScrollHorizontalEnd,
|
||||||
this.onPreviewEMLDelegateAction,
|
this.onPreviewEMLDelegateAction,
|
||||||
this.onDownloadAttachmentDelegateAction,
|
this.onDownloadAttachmentDelegateAction,
|
||||||
|
this.onHtmlContentClippedAction,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -57,6 +60,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
class _HtmlContentViewState extends State<HtmlContentViewer> {
|
class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||||
|
|
||||||
static const double _minHeight = 100.0;
|
static const double _minHeight = 100.0;
|
||||||
|
static const double _iOSHtmlContentMaxHeight = 22000.0;
|
||||||
static const double _offsetHeight = 30.0;
|
static const double _offsetHeight = 30.0;
|
||||||
|
|
||||||
late InAppWebViewController _webViewController;
|
late InAppWebViewController _webViewController;
|
||||||
@@ -184,12 +188,25 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
Size oldContentSize,
|
Size oldContentSize,
|
||||||
Size newContentSize
|
Size newContentSize
|
||||||
) async {
|
) async {
|
||||||
|
if (!mounted || _loadingBarNotifier.value) return;
|
||||||
|
|
||||||
final maxContentHeight = math.max(oldContentSize.height, newContentSize.height);
|
final maxContentHeight = math.max(oldContentSize.height, newContentSize.height);
|
||||||
log('_HtmlContentViewState::_onContentSizeChanged:maxContentHeight: $maxContentHeight');
|
if (maxContentHeight <= _actualHeight) return;
|
||||||
if (maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
|
|
||||||
log('_HtmlContentViewState::_onContentSizeChanged:HEIGHT_UPDATED: $maxContentHeight');
|
double currentHeight = maxContentHeight + _offsetHeight;
|
||||||
|
|
||||||
|
if (PlatformInfo.isIOS) {
|
||||||
|
final isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||||
|
if (isClipped) {
|
||||||
|
widget.onHtmlContentClippedAction?.call(true);
|
||||||
|
}
|
||||||
|
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_actualHeight != currentHeight) {
|
||||||
|
log('_HtmlContentViewState::_onContentSizeChanged: currentHeight = $currentHeight');
|
||||||
setState(() {
|
setState(() {
|
||||||
_actualHeight = maxContentHeight + _offsetHeight;
|
_actualHeight = currentHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,55 +222,84 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onHandleContentSizeChangedEvent(List<dynamic> parameters) async {
|
void _onHandleContentSizeChangedEvent(List<dynamic> parameters) async {
|
||||||
final maxContentHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
|
if (!mounted || _loadingBarNotifier.value) return;
|
||||||
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:maxContentHeight: $maxContentHeight');
|
|
||||||
if (maxContentHeight is num && maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
|
final dynamic result = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
|
||||||
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:HEIGHT_UPDATED: $maxContentHeight');
|
if (result is! num) return;
|
||||||
|
|
||||||
|
final double maxContentHeight = result.toDouble();
|
||||||
|
if (maxContentHeight <= _actualHeight) return;
|
||||||
|
|
||||||
|
double currentHeight = maxContentHeight + _offsetHeight;
|
||||||
|
|
||||||
|
if (PlatformInfo.isIOS) {
|
||||||
|
final bool isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||||
|
if (isClipped) {
|
||||||
|
widget.onHtmlContentClippedAction?.call(true);
|
||||||
|
}
|
||||||
|
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_actualHeight != currentHeight) {
|
||||||
|
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent: currentHeight = $currentHeight');
|
||||||
setState(() {
|
setState(() {
|
||||||
_actualHeight = maxContentHeight + _offsetHeight;
|
_actualHeight = currentHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getActualSizeHtmlViewer() async {
|
Future<void> _getActualSizeHtmlViewer() async {
|
||||||
final listSize = await Future.wait([
|
if (!mounted) return;
|
||||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].scrollWidth'),
|
|
||||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].offsetWidth'),
|
final List<dynamic> listSize = await Future.wait([
|
||||||
_webViewController.evaluateJavascript(source: 'document.body.scrollHeight'),
|
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0]?.scrollWidth'),
|
||||||
|
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0]?.offsetWidth'),
|
||||||
|
_webViewController.evaluateJavascript(source: 'document.body?.scrollHeight'),
|
||||||
]);
|
]);
|
||||||
log('_HtmlContentViewState::_getActualSizeHtmlViewer():listSize: $listSize');
|
|
||||||
Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
|
log('_HtmlContentViewState::_getActualSizeHtmlViewer(): listSize: $listSize');
|
||||||
|
|
||||||
bool isScrollActivated = false;
|
bool isScrollActivated = false;
|
||||||
|
Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
|
||||||
|
|
||||||
if (listSize[0] is num && listSize[1] is num) {
|
final double? scrollWidth = listSize[0] is num ? (listSize[0] as num).toDouble() : null;
|
||||||
final scrollWidth = listSize[0] as num;
|
final double? offsetWidth = listSize[1] is num ? (listSize[1] as num).toDouble() : null;
|
||||||
final offsetWidth = listSize[1] as num;
|
final double? scrollHeight = listSize[2] is num ? (listSize[2] as num).toDouble() : null;
|
||||||
|
|
||||||
|
if (scrollWidth != null && offsetWidth != null) {
|
||||||
isScrollActivated = scrollWidth.round() == offsetWidth.round();
|
isScrollActivated = scrollWidth.round() == offsetWidth.round();
|
||||||
|
|
||||||
if (!isScrollActivated && PlatformInfo.isIOS) {
|
if (!isScrollActivated && PlatformInfo.isIOS) {
|
||||||
newGestureRecognizers = {
|
newGestureRecognizers = {
|
||||||
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)),
|
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)),
|
||||||
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer())
|
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listSize[2] is num) {
|
if (scrollHeight != null && scrollHeight > 0) {
|
||||||
final scrollHeight = listSize[2] as num;
|
double currentHeight = scrollHeight + _offsetHeight;
|
||||||
if (mounted && scrollHeight > 0) {
|
|
||||||
|
if (PlatformInfo.isIOS) {
|
||||||
|
final bool isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||||
|
if (isClipped) {
|
||||||
|
widget.onHtmlContentClippedAction?.call(true);
|
||||||
|
}
|
||||||
|
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_actualHeight != currentHeight || newGestureRecognizers != null) {
|
||||||
|
log('_HtmlContentViewState::_getActualSizeHtmlViewer: currentHeight = $currentHeight');
|
||||||
setState(() {
|
setState(() {
|
||||||
_actualHeight = scrollHeight + _offsetHeight;
|
_actualHeight = currentHeight;
|
||||||
if (newGestureRecognizers != null) {
|
if (newGestureRecognizers != null) {
|
||||||
_gestureRecognizers = newGestureRecognizers;
|
_gestureRecognizers = newGestureRecognizers;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (newGestureRecognizers != null) {
|
||||||
if (mounted && newGestureRecognizers != null) {
|
setState(() {
|
||||||
setState(() {
|
_gestureRecognizers = newGestureRecognizers!;
|
||||||
_gestureRecognizers = newGestureRecognizers!;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isScrollActivated) {
|
if (!isScrollActivated) {
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
final attachmentsViewState = RxMap<Id, Either<Failure, Success>>();
|
final attachmentsViewState = RxMap<Id, Either<Failure, Success>>();
|
||||||
final isEmailContentHidden = RxBool(false);
|
final isEmailContentHidden = RxBool(false);
|
||||||
final currentEmailLoaded = Rxn<EmailLoaded>();
|
final currentEmailLoaded = Rxn<EmailLoaded>();
|
||||||
|
final isEmailContentClipped = RxBool(false);
|
||||||
|
|
||||||
EmailId? _currentEmailId;
|
EmailId? _currentEmailId;
|
||||||
Identity? _identitySelected;
|
Identity? _identitySelected;
|
||||||
@@ -750,6 +751,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
blobCalendarEvent.value = null;
|
blobCalendarEvent.value = null;
|
||||||
emailUnsubscribe.value = null;
|
emailUnsubscribe.value = null;
|
||||||
_identitySelected = null;
|
_identitySelected = null;
|
||||||
|
isEmailContentClipped.value = false;
|
||||||
if (isEmailClosing) {
|
if (isEmailClosing) {
|
||||||
emailLoadedViewState.value = Right(UIState.idle);
|
emailLoadedViewState.value = Right(UIState.idle);
|
||||||
viewState.value = Right(UIState.idle);
|
viewState.value = Right(UIState.idle);
|
||||||
@@ -2520,4 +2522,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onHtmlContentClippedAction(bool isClipped) {
|
||||||
|
log('SingleEmailController::onHtmlContentClippedAction:isClipped = $isClipped');
|
||||||
|
isEmailContentClipped.value = isClipped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
else if (presentationEmail.id == controller.currentEmail?.id)
|
else if (presentationEmail.id == controller.currentEmail?.id)
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (controller.emailContents.value != null) {
|
if (controller.emailContents.value != null) {
|
||||||
final allEmailContents = controller.emailContents.value ?? '';
|
String allEmailContents = controller.emailContents.value ?? '';
|
||||||
|
|
||||||
if (PlatformInfo.isWeb) {
|
if (PlatformInfo.isWeb) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -429,29 +429,68 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (PlatformInfo.isIOS
|
} else if (PlatformInfo.isIOS) {
|
||||||
&& !controller.responsiveUtils.isScreenWithShortestSide(context)) {
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (controller.isEmailContentHidden.isTrue) {
|
if (controller.isEmailContentHidden.isTrue && !controller.responsiveUtils.isScreenWithShortestSide(context)) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
} else {
|
} else {
|
||||||
return Padding(
|
return Column(
|
||||||
padding: const EdgeInsetsDirectional.symmetric(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
vertical: EmailViewStyles.mobileContentVerticalMargin,
|
children: [
|
||||||
horizontal: EmailViewStyles.mobileContentHorizontalMargin
|
Padding(
|
||||||
),
|
padding: const EdgeInsetsDirectional.symmetric(
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
vertical: EmailViewStyles.mobileContentVerticalMargin,
|
||||||
return HtmlContentViewer(
|
horizontal: EmailViewStyles.mobileContentHorizontalMargin,
|
||||||
contentHtml: allEmailContents,
|
),
|
||||||
initialWidth: constraints.maxWidth,
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
direction: AppUtils.getCurrentDirection(context),
|
return HtmlContentViewer(
|
||||||
contentPadding: 0,
|
contentHtml: allEmailContents,
|
||||||
useDefaultFont: true,
|
initialWidth: constraints.maxWidth,
|
||||||
onMailtoDelegateAction: controller.openMailToLink,
|
direction: AppUtils.getCurrentDirection(context),
|
||||||
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
contentPadding: 0,
|
||||||
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
useDefaultFont: true,
|
||||||
);
|
onMailtoDelegateAction: controller.openMailToLink,
|
||||||
})
|
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
||||||
|
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
||||||
|
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
if (controller.isEmailContentClipped.isTrue) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
AppLocalizations.of(context).messageClipped,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: AppColor.steelGray400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TMailButtonWidget.fromText(
|
||||||
|
text: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
||||||
|
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsetsDirectional.only(
|
||||||
|
start: 8,
|
||||||
|
end: 8,
|
||||||
|
bottom: 24,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
onTapActionCallback: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -471,6 +510,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
onMailtoDelegateAction: controller.openMailToLink,
|
onMailtoDelegateAction: controller.openMailToLink,
|
||||||
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
||||||
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
||||||
|
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"@@last_modified": "2025-02-20T19:41:05.121752",
|
"@@last_modified": "2025-03-31T03:40:41.454034",
|
||||||
"initializing_data": "Initializing data...",
|
"initializing_data": "Initializing data...",
|
||||||
"@initializing_data": {
|
"@initializing_data": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -2962,6 +2962,12 @@
|
|||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
|
"errorWhileFetchingSubaddress": "Error while fetching the subaddress",
|
||||||
|
"@errorWhileFetchingSubaddress": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
"connectedToTheInternet": "Connected to the internet",
|
"connectedToTheInternet": "Connected to the internet",
|
||||||
"@connectedToTheInternet": {
|
"@connectedToTheInternet": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -4238,12 +4244,6 @@
|
|||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"messageWarningDialogWhenExpiredOIDCTokenAndReconnection": "Your session expired. We need to take you back to the login page in order to refresh it. You might want to save the email you are currently editing before we do so.",
|
|
||||||
"@messageWarningDialogWhenExpiredOIDCTokenAndReconnection": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders_order": [],
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"replyToList": "Reply to list",
|
"replyToList": "Reply to list",
|
||||||
"@replyToList": {
|
"@replyToList": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -4375,5 +4375,17 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"messageClipped": "[Message clipped]",
|
||||||
|
"@messageClipped": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"viewEntireMessage": "View entire message",
|
||||||
|
"@viewEntireMessage": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4597,4 +4597,18 @@ class AppLocalizations {
|
|||||||
name: 'exitFullscreen',
|
name: 'exitFullscreen',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get messageClipped {
|
||||||
|
return Intl.message(
|
||||||
|
'[Message clipped]',
|
||||||
|
name: 'messageClipped',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get viewEntireMessage {
|
||||||
|
return Intl.message(
|
||||||
|
'View entire message',
|
||||||
|
name: 'viewEntireMessage',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user