TF-3731 Thread Detail Cache EmailLoaded in memory for web

This commit is contained in:
DatDang
2025-06-26 09:00:52 +07:00
committed by Dat H. Pham
parent 027ab8e0d8
commit 4d9a44df66
7 changed files with 152 additions and 28 deletions
@@ -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 { class GetEmailContentFromCacheSuccess extends UIState {
final String htmlEmailContent; final String htmlEmailContent;
final List<Attachment>? attachments; final List<Attachment>? attachments;
@@ -78,4 +78,13 @@ class UnsubscribeFromThreadAction extends EmailUIAction {
@override @override
List<Object?> get props => [emailId, listUnsubscribe]; List<Object?> get props => [emailId, listUnsubscribe];
}
class CollapseEmailInThreadDetailAction extends EmailUIAction {
CollapseEmailInThreadDetailAction(this.emailId);
final EmailId emailId;
@override
List<Object?> get props => [emailId];
} }
@@ -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/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/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/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/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.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) { } else if (action is UnsubscribeFromThreadAction) {
if (_currentEmailId == null || action.emailId != _currentEmailId) return; if (_currentEmailId == null || action.emailId != _currentEmailId) return;
_handleUnsubscribe(action.listUnsubscribe); _handleUnsubscribe(action.listUnsubscribe);
} else if (action is CollapseEmailInThreadDetailAction) {
if (_currentEmailId == null || action.emailId != _currentEmailId) return;
for (var worker in obxListeners) {
worker.dispose();
}
Get.delete<SingleEmailController>(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() { void _handleOpenEmailDetailedView() {
@@ -600,6 +597,17 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} }
void _getEmailContentAction(EmailId emailId) { 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) { if (session != null && accountId != null) {
try { try {
final baseDownloadUrl = session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); final baseDownloadUrl = session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
@@ -728,6 +736,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
accountId, accountId,
detailedEmail detailedEmail
); );
} else if (currentEmail?.id != null && currentEmailLoaded.value != null) {
_threadDetailController?.cacheEmailLoaded(
currentEmail!.id!,
currentEmailLoaded.value!,
);
} }
final isShowMessageReadReceipt = success.emailCurrent final isShowMessageReadReceipt = success.emailCurrent
@@ -746,7 +759,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_jumpScrollViewToTopOfEmail(); _jumpScrollViewToTopOfEmail();
} }
if (currentEmail?.threadId != null && if (currentEmail?.threadId != null &&
currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id) { currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id &&
success is! GetEmailContentFromThreadCacheSuccess) {
mailboxDashBoardController.dispatchThreadDetailUIAction( mailboxDashBoardController.dispatchThreadDetailUIAction(
LoadThreadDetailAfterSelectedEmailAction( LoadThreadDetailAfterSelectedEmailAction(
currentEmail!.threadId!, currentEmail!.threadId!,
@@ -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: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/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: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'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension ToggleThreadDetailCollapeExpand on ThreadDetailController { extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
@@ -18,22 +14,14 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
emailIdsPresentation[emailId] = presentationEmail.copyWith( emailIdsPresentation[emailId] = presentationEmail.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed, emailInThreadStatus: EmailInThreadStatus.collapsed,
); );
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(emailId),
);
currentExpandedEmailId.value = null; currentExpandedEmailId.value = null;
return; return;
} }
final isInitialized = Get.isRegistered<SingleEmailController>( EmailBindings(currentEmailId: presentationEmail.id).dependencies();
tag: emailId.id.value,
);
if (!isInitialized) {
EmailBindings(currentEmailId: presentationEmail.id).dependencies();
} else {
focusExpandedEmail(emailId);
threadDetailOnEmailActionClick(
presentationEmail,
EmailActionType.markAsRead,
);
}
for (var key in emailIdsPresentation.keys) { for (var key in emailIdsPresentation.keys) {
if (emailIdsPresentation[key] == null) continue; if (emailIdsPresentation[key] == null) continue;
@@ -49,6 +37,9 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
emailIdsPresentation[key] = emailIdsPresentation[key]?.copyWith( emailIdsPresentation[key] = emailIdsPresentation[key]?.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed, emailInThreadStatus: EmailInThreadStatus.collapsed,
); );
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(key),
);
} }
} }
} }
@@ -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;
}
}
@@ -89,6 +89,7 @@ class ThreadDetailController extends BaseController {
IndividualHeaderIdentifier.listPostHeader.value, IndividualHeaderIdentifier.listPostHeader.value,
IndividualHeaderIdentifier.listUnsubscribeHeader.value, IndividualHeaderIdentifier.listUnsubscribeHeader.value,
}); });
final cachedEmailLoaded = <EmailId, EmailLoaded>{};
final mailboxDashBoardController = Get.find<MailboxDashBoardController>(); final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final searchEmailController = Get.find<SearchEmailController>(); final searchEmailController = Get.find<SearchEmailController>();
@@ -221,6 +222,7 @@ class ThreadDetailController extends BaseController {
scrollController = null; scrollController = null;
currentExpandedEmailId.value = null; currentExpandedEmailId.value = null;
currentEmailLoaded.value = null; currentEmailLoaded.value = null;
cachedEmailLoaded.clear();
} }
@override @override
@@ -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<ThreadDetailController>()])
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,
);
}
}
});
});
}