From 4d9a44df664a5329b5cd1fed5d405c059ac6f541 Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 26 Jun 2025 09:00:52 +0700 Subject: [PATCH] TF-3731 Thread Detail Cache EmailLoaded in memory for web --- .../domain/state/get_email_content_state.dart | 9 ++ .../presentation/action/email_ui_action.dart | 9 ++ .../controller/single_email_controller.dart | 36 +++++--- .../toggle_thread_detail_collape_expand.dart | 25 ++---- .../update_cached_list_email_loaded.dart | 15 ++++ .../thread_detail_controller.dart | 2 + .../update_cached_list_email_loaded_test.dart | 84 +++++++++++++++++++ 7 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 lib/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart create mode 100644 test/features/thread_detail/presentation/extension/update_cached_list_email_loaded_test.dart diff --git a/lib/features/email/domain/state/get_email_content_state.dart b/lib/features/email/domain/state/get_email_content_state.dart index 7ca833a30..11d94ef8c 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -27,6 +27,15 @@ class GetEmailContentSuccess extends UIState { ]; } +class GetEmailContentFromThreadCacheSuccess extends GetEmailContentSuccess { + GetEmailContentFromThreadCacheSuccess({ + required super.htmlEmailContent, + super.attachments, + super.inlineImages, + super.emailCurrent, + }); +} + class GetEmailContentFromCacheSuccess extends UIState { final String htmlEmailContent; final List? attachments; diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index 5771a3349..b05711ef3 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -78,4 +78,13 @@ class UnsubscribeFromThreadAction extends EmailUIAction { @override List get props => [emailId, listUnsubscribe]; +} + +class CollapseEmailInThreadDetailAction extends EmailUIAction { + CollapseEmailInThreadDetailAction(this.emailId); + + final EmailId emailId; + + @override + List get props => [emailId]; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 15227e698..991767bd6 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -127,6 +127,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -414,6 +415,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (action is UnsubscribeFromThreadAction) { if (_currentEmailId == null || action.emailId != _currentEmailId) return; _handleUnsubscribe(action.listUnsubscribe); + } else if (action is CollapseEmailInThreadDetailAction) { + if (_currentEmailId == null || action.emailId != _currentEmailId) return; + for (var worker in obxListeners) { + worker.dispose(); + } + Get.delete(tag: _currentEmailId!.id.value); } })); @@ -425,16 +432,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } }); })); - if (_threadDetailController != null) { - obxListeners.add(ever( - _threadDetailController!.currentExpandedEmailId, - (emailId) { - if (emailId == null || emailId != _currentEmailId) return; - - _threadDetailController!.currentEmailLoaded.value = currentEmailLoaded.value; - }, - )); - } } void _handleOpenEmailDetailedView() { @@ -600,6 +597,17 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _getEmailContentAction(EmailId emailId) { + if (_currentEmailId != null && _threadDetailController?.cachedEmailLoaded[_currentEmailId!] != null) { + final emailLoaded = _threadDetailController!.cachedEmailLoaded[_currentEmailId!]!; + consumeState(Stream.value(Right(GetEmailContentFromThreadCacheSuccess( + htmlEmailContent: emailLoaded.htmlContent, + emailCurrent: emailLoaded.emailCurrent, + attachments: emailLoaded.attachments, + inlineImages: emailLoaded.inlineImages, + )))); + return; + } + if (session != null && accountId != null) { try { final baseDownloadUrl = session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); @@ -728,6 +736,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { accountId, detailedEmail ); + } else if (currentEmail?.id != null && currentEmailLoaded.value != null) { + _threadDetailController?.cacheEmailLoaded( + currentEmail!.id!, + currentEmailLoaded.value!, + ); } final isShowMessageReadReceipt = success.emailCurrent @@ -746,7 +759,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _jumpScrollViewToTopOfEmail(); } if (currentEmail?.threadId != null && - currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id) { + currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id && + success is! GetEmailContentFromThreadCacheSuccess) { mailboxDashBoardController.dispatchThreadDetailUIAction( LoadThreadDetailAfterSelectedEmailAction( currentEmail!.threadId!, diff --git a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart index 9ed7cd3b9..9cb5c80fb 100644 --- a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart +++ b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart @@ -1,11 +1,7 @@ -import 'package:get/get.dart'; -import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:model/email/email_in_thread_status.dart'; -import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart'; -import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; extension ToggleThreadDetailCollapeExpand on ThreadDetailController { @@ -18,22 +14,14 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { emailIdsPresentation[emailId] = presentationEmail.copyWith( emailInThreadStatus: EmailInThreadStatus.collapsed, ); + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(emailId), + ); currentExpandedEmailId.value = null; return; } - final isInitialized = Get.isRegistered( - tag: emailId.id.value, - ); - if (!isInitialized) { - EmailBindings(currentEmailId: presentationEmail.id).dependencies(); - } else { - focusExpandedEmail(emailId); - threadDetailOnEmailActionClick( - presentationEmail, - EmailActionType.markAsRead, - ); - } + EmailBindings(currentEmailId: presentationEmail.id).dependencies(); for (var key in emailIdsPresentation.keys) { if (emailIdsPresentation[key] == null) continue; @@ -49,6 +37,9 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { emailIdsPresentation[key] = emailIdsPresentation[key]?.copyWith( emailInThreadStatus: EmailInThreadStatus.collapsed, ); + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(key), + ); } } } diff --git a/lib/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart b/lib/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart new file mode 100644 index 000000000..89925fda8 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart @@ -0,0 +1,15 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +extension UpdateCachedEmailLoaded on ThreadDetailController { + void cacheEmailLoaded(EmailId emailId, EmailLoaded emailLoaded) { + const cacheLimit = 20; + + while (cachedEmailLoaded[emailId] == null && + cachedEmailLoaded.length >= cacheLimit) { + cachedEmailLoaded.remove(cachedEmailLoaded.keys.first); + } + cachedEmailLoaded[emailId] = emailLoaded; + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index 5d042cb67..e8d38bb9d 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -89,6 +89,7 @@ class ThreadDetailController extends BaseController { IndividualHeaderIdentifier.listPostHeader.value, IndividualHeaderIdentifier.listUnsubscribeHeader.value, }); + final cachedEmailLoaded = {}; final mailboxDashBoardController = Get.find(); final searchEmailController = Get.find(); @@ -221,6 +222,7 @@ class ThreadDetailController extends BaseController { scrollController = null; currentExpandedEmailId.value = null; currentEmailLoaded.value = null; + cachedEmailLoaded.clear(); } @override diff --git a/test/features/thread_detail/presentation/extension/update_cached_list_email_loaded_test.dart b/test/features/thread_detail/presentation/extension/update_cached_list_email_loaded_test.dart new file mode 100644 index 000000000..542be9126 --- /dev/null +++ b/test/features/thread_detail/presentation/extension/update_cached_list_email_loaded_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +import 'update_cached_list_email_loaded_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late MockThreadDetailController threadDetailController; + + setUp(() { + threadDetailController = MockThreadDetailController(); + }); + + group('update cached list email loaded test:', () { + test( + 'should update cachedEmailLoaded as expected ' + 'when cachedEmailLoaded contains less than limit (20)', + () { + // arrange + when(threadDetailController.cachedEmailLoaded).thenReturn({ + for (int i = 0; i < 18; i++) EmailId(Id('$i')): EmailLoaded( + htmlContent: '', + attachments: [], + inlineImages: [], + emailCurrent: Email(id: EmailId(Id('$i'))), + ), + }); + final expectedEmailLoaded = EmailLoaded( + htmlContent: '', + attachments: [], + inlineImages: [], + emailCurrent: Email(id: EmailId(Id('19'))), + ); + + // act + threadDetailController.cacheEmailLoaded( + expectedEmailLoaded.emailCurrent!.id!, + expectedEmailLoaded, + ); + + // assert + expect( + expectedEmailLoaded, + threadDetailController.cachedEmailLoaded.values.last, + ); + }); + + test( + 'should update cachedEmailLoaded as expected ' + 'when cachedEmailLoaded contains more than limit (20)', + () { + when(threadDetailController.cachedEmailLoaded).thenReturn({}); + for (var i = 0; i < 25; i++) { + final expectedEmailLoaded = EmailLoaded( + htmlContent: '', + attachments: [], + inlineImages: [], + emailCurrent: Email(id: EmailId(Id('$i'))), + ); + threadDetailController.cacheEmailLoaded( + expectedEmailLoaded.emailCurrent!.id!, + expectedEmailLoaded, + ); + + if (i >= 20) { + expect( + expectedEmailLoaded, + threadDetailController.cachedEmailLoaded.values.last, + ); + expect( + threadDetailController.cachedEmailLoaded[EmailId(Id('${i - 20}'))], + isNull, + ); + } + } + }); + }); +} \ No newline at end of file