TF-4141 Add Action required folder to left menu

This commit is contained in:
dab246
2025-12-16 15:24:38 +07:00
committed by Dat H. Pham
parent 61b8c95123
commit 0e49c18f9d
23 changed files with 641 additions and 13 deletions
@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.0001 1.42969C14.7329 1.42969 18.5716 5.26705 18.5716 10.0011C18.5716 14.7352 14.7329 18.5725 10.0001 18.5725C5.26607 18.5725 1.42871 14.7352 1.42871 10.0011C1.42871 5.26705 5.26607 1.42969 10.0001 1.42969ZM10.0001 2.85826C6.05505 2.85826 2.85728 6.05603 2.85728 10.0011C2.85728 13.9462 6.05505 17.144 10.0001 17.144C13.9444 17.144 17.143 13.9457 17.143 10.0011C17.143 6.05652 13.9444 2.85826 10.0001 2.85826ZM9.643 5.71546C10.0375 5.71546 10.3573 6.03526 10.3573 6.42975V10.0752L13.8391 11.4817C14.2049 11.6295 14.3816 12.0458 14.2339 12.4116C14.0861 12.7774 13.6698 12.9541 13.304 12.8063L9.37545 11.2193C9.10546 11.1102 8.92871 10.8482 8.92871 10.557V6.42975C8.92871 6.03526 9.24851 5.71546 9.643 5.71546Z"
fill="#007AFF" />
</svg>

After

Width:  |  Height:  |  Size: 863 B

@@ -45,6 +45,7 @@ class ImagePaths {
String get icMailboxSpam => _getImagePath('ic_mailbox_spam.svg');
String get icMailboxTrash => _getImagePath('ic_mailbox_trash.svg');
String get icMailboxFavorite => _getImagePath('ic_mailbox_favorite.svg');
String get icMailboxActionRequired => _getImagePath('ic_mailbox_action_required.svg');
String get icFilterSelected => _getImagePath('ic_filter_selected.svg');
String get icFilterMessageAll => _getImagePath('ic_filter_message_all.svg');
String get icFilterMessageAttachments => _getImagePath('ic_filter_message_attachments.svg');
@@ -0,0 +1,91 @@
import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_ai_action_extension.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleActionRequiredTabExtension on BaseMailboxController {
void setUpActionRequiredFolder(
MailboxDashBoardController dashboardController,
) {
if (!dashboardController.isAiCapabilitySupported) return;
dashboardController.actionRequiredFolderController.getCountEmails(
session: dashboardController.sessionCurrent,
accountId: dashboardController.accountId.value,
);
}
void onActionRequiredFolderCountChanged(int count) {
if (count > 0) {
_addActionRequiredFolder(count);
} else {
_removeActionRequiredFolder();
}
}
void _addActionRequiredFolder(int count) {
final folder = _buildActionRequiredFolder(count);
_addToDefaultMailboxTree(folder);
_addToAllMailboxes(folder);
}
PresentationMailbox _buildActionRequiredFolder(int count) {
final base = PresentationMailbox.actionRequiredFolder;
return base.copyWith(
displayName:
currentContext != null ? base.getDisplayName(currentContext!) : null,
totalEmails: TotalEmails(UnsignedInt(count)),
unreadEmails: UnreadEmails(UnsignedInt(count)),
);
}
void _addToDefaultMailboxTree(PresentationMailbox folder) {
final root = defaultMailboxTree.value.root;
final children = List<MailboxNode>.from(root.childrenItems ?? []);
children.insertAfterStarredOrInbox(MailboxNode(folder));
defaultMailboxTree.value = MailboxTree(
root.copyWith(children: children),
);
}
void _addToAllMailboxes(PresentationMailbox folder) {
if (_allMailboxesContains(folder.id)) return;
allMailboxes.add(folder);
}
void _removeActionRequiredFolder() {
final folder = PresentationMailbox.actionRequiredFolder;
_removeFromDefaultMailboxTree(folder.id);
_removeFromAllMailboxes(folder.id);
}
void _removeFromDefaultMailboxTree(MailboxId folderId) {
final root = defaultMailboxTree.value.root;
final children = List<MailboxNode>.from(root.childrenItems ?? [])
..removeWhere((node) => node.item.id == folderId);
defaultMailboxTree.value = MailboxTree(
root.copyWith(children: children),
);
}
void _removeFromAllMailboxes(MailboxId folderId) {
allMailboxes.removeWhere((mailbox) => mailbox.id == folderId);
}
bool _allMailboxesContains(MailboxId id) {
return allMailboxes.any((mailbox) => mailbox.id == id);
}
}
@@ -5,20 +5,60 @@ import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.d
extension ListMailboxNodeExtension on List<MailboxNode> {
List<MailboxId> get mailboxIds => map((node) => node.item.id).toList();
/// Insert [newNode] after Inbox if present, otherwise at the beginning.
void insertAfterInbox(MailboxNode newNode) {
final alreadyExists = any((node) => node.item.id == newNode.item.id);
if (alreadyExists) return;
final index = indexWhere(
(node) =>
node.item.role?.value == PresentationMailbox.inboxRole ||
node.item.name?.name.toLowerCase() == 'inbox',
insertAfterByPriority(
newNode,
[_isInbox],
);
}
if (index != -1) {
insert(index + 1, newNode);
} else {
insert(0, newNode);
/// Insert [newNode] after Starred if present,
/// otherwise after Inbox, otherwise at the beginning.
void insertAfterStarredOrInbox(MailboxNode newNode) {
insertAfterByPriority(
newNode,
[
_isStarred,
_isInbox,
],
);
}
/// Insert [newNode] after the first mailbox matching [priorities].
/// If none match, inserts at the beginning.
void insertAfterByPriority(
MailboxNode newNode,
List<bool Function(MailboxNode)> priorities,
) {
if (_containsMailbox(newNode)) return;
for (final predicate in priorities) {
final index = indexWhere(predicate);
if (index != -1) {
insert(index + 1, newNode);
return;
}
}
insert(0, newNode);
}
bool _isInbox(MailboxNode node) {
return node.item.role?.value == PresentationMailbox.inboxRole ||
_equalsIgnoreCase(node.item.name?.name, 'inbox');
}
bool _isStarred(MailboxNode node) {
return node.item.role?.value == PresentationMailbox.favoriteRole ||
_equalsIgnoreCase(node.item.name?.name, 'starred');
}
bool _containsMailbox(MailboxNode node) {
return any((e) => e.item.id == node.item.id);
}
bool _equalsIgnoreCase(String? value, String expected) {
return value != null && value.toLowerCase() == expected;
}
}
@@ -17,6 +17,8 @@ extension PresentationMailboxExtension on PresentationMailbox {
return AppLocalizations.of(context).inboxMailboxDisplayName;
case PresentationMailbox.favoriteRole:
return AppLocalizations.of(context).favoriteMailboxDisplayName;
case PresentationMailbox.actionRequiredRole:
return AppLocalizations.of(context).actionRequiredMailboxDisplayName;
case PresentationMailbox.archiveRole:
return AppLocalizations.of(context).archiveMailboxDisplayName;
case PresentationMailbox.draftsRole:
@@ -44,6 +46,10 @@ extension PresentationMailboxExtension on PresentationMailbox {
switch(role!.value.toLowerCase()) {
case PresentationMailbox.inboxRole:
return appLocalizations.inboxMailboxDisplayName;
case PresentationMailbox.favoriteRole:
return appLocalizations.favoriteMailboxDisplayName;
case PresentationMailbox.actionRequiredRole:
return appLocalizations.actionRequiredMailboxDisplayName;
case PresentationMailbox.archiveRole:
return appLocalizations.archiveMailboxDisplayName;
case PresentationMailbox.draftsRole:
@@ -73,6 +79,8 @@ extension PresentationMailboxExtension on PresentationMailbox {
return imagePaths.icMailboxInbox;
case PresentationMailbox.favoriteRole:
return imagePaths.icMailboxFavorite;
case PresentationMailbox.actionRequiredRole:
return imagePaths.icMailboxActionRequired;
case PresentationMailbox.draftsRole:
return imagePaths.icMailboxDrafts;
case PresentationMailbox.outboxRole:
@@ -69,6 +69,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_int
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_action_required_tab_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart';
@@ -264,6 +265,7 @@ class MailboxController extends BaseMailboxController
(failure) {
if (failure is GetAllMailboxFailure) {
addFavoriteFolderToMailboxList();
setUpActionRequiredFolder(mailboxDashBoardController);
mailboxDashBoardController.updateRefreshAllMailboxState(Left(RefreshAllMailboxFailure()));
showRetryToast(failure);
}
@@ -271,6 +273,7 @@ class MailboxController extends BaseMailboxController
(success) {
if (success is GetAllMailboxSuccess) {
addFavoriteFolderToMailboxList();
setUpActionRequiredFolder(mailboxDashBoardController);
mailboxDashBoardController.updateRefreshAllMailboxState(Right(RefreshAllMailboxSuccess()));
_handleCreateDefaultFolderIfMissing(mailboxDashBoardController.mapDefaultMailboxIdByRole);
_handleDataFromNavigationRouter();
@@ -280,6 +283,7 @@ class MailboxController extends BaseMailboxController
}
} else if (success is CreateDefaultMailboxAllSuccess) {
addFavoriteFolderToMailboxList();
setUpActionRequiredFolder(mailboxDashBoardController);
}
});
}
@@ -434,6 +438,11 @@ class MailboxController extends BaseMailboxController
);
}
});
ever(
mailboxDashBoardController.actionRequiredFolderController.actionRequiredFolderCount,
onActionRequiredFolderCountChanged,
);
}
void _handleMarkEmailsAsReadOrUnread({
@@ -0,0 +1,79 @@
import 'package:core/data/model/source_type/data_source_type.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
import 'package:tmail_ui_user/features/caching/caching_manager.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart';
import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart';
import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart';
import 'package:tmail_ui_user/features/thread/data/datasource_impl/local_thread_datasource_impl.dart';
import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart';
import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart';
import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart';
import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart';
import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_impl.dart';
import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/get_count_unread_emails_in_folder_interactor.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
class ActionRequiredInteractorBindings extends InteractorsBindings {
@override
void bindingsDataSource() {
Get.lazyPut<ThreadDataSource>(() => Get.find<ThreadDataSourceImpl>());
Get.lazyPut<StateDataSource>(() => Get.find<StateDataSourceImpl>());
}
@override
void bindingsDataSourceImpl() {
Get.lazyPut(
() => ThreadDataSourceImpl(
Get.find<ThreadAPI>(),
Get.find<ThreadIsolateWorker>(),
Get.find<RemoteExceptionThrower>(),
),
);
Get.lazyPut(
() => LocalThreadDataSourceImpl(
Get.find<EmailCacheManager>(),
Get.find<CachingManager>(),
Get.find<CacheExceptionThrower>(),
),
);
Get.lazyPut(
() => StateDataSourceImpl(
Get.find<StateCacheManager>(),
Get.find<IOSSharingManager>(),
Get.find<CacheExceptionThrower>(),
),
);
}
@override
void bindingsInteractor() {
Get.lazyPut(
() => GetCountUnreadEmailsInFolderInteractor(
Get.find<ThreadRepository>(),
),
);
}
@override
void bindingsRepository() {
Get.lazyPut<ThreadRepository>(() => Get.find<ThreadRepositoryImpl>());
}
@override
void bindingsRepositoryImpl() {
Get.lazyPut(
() => ThreadRepositoryImpl(
{
DataSourceType.network: Get.find<ThreadDataSource>(),
DataSourceType.local: Get.find<LocalThreadDataSourceImpl>()
},
Get.find<StateDataSource>(),
),
);
}
}
@@ -106,6 +106,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_e
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_spam_report_state_interactor.dart';
import 'package:tmail_ui_user/features/download/presentation/bindings/download_interactor_bindings.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/action_required_folder_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart';
import 'package:tmail_ui_user/features/download/presentation/controllers/download_controller.dart';
@@ -209,6 +210,8 @@ class MailboxDashBoardBindings extends BaseBindings {
Get.lazyPut(() => ComposerManager());
Get.put(ActionRequiredFolderController());
Get.put(MailboxDashBoardController(
Get.find<MoveToMailboxInteractor>(),
Get.find<DeleteEmailPermanentlyInteractor>(),
@@ -0,0 +1,82 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/action_required_interactor_bindings.dart';
import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart';
import 'package:tmail_ui_user/features/thread/domain/state/get_count_emails_in_folder_state.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/get_count_unread_emails_in_folder_interactor.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class ActionRequiredFolderController extends BaseController {
final actionRequiredFolderCount = RxInt(0);
GetCountUnreadEmailsInFolderInteractor?
_getCountUnreadEmailsInFolderInteractor;
void injectBinding() {
ActionRequiredInteractorBindings().dependencies();
_getCountUnreadEmailsInFolderInteractor =
getBinding<GetCountUnreadEmailsInFolderInteractor>();
}
Future<void> getCountEmails({
required Session? session,
required AccountId? accountId,
}) async {
if (_getCountUnreadEmailsInFolderInteractor == null) {
consumeState(Stream.value(
Left(GetCountUnreadEmailsInFolderFailure(InteractorIsNullException())),
));
return;
}
if (session == null) {
consumeState(Stream.value(
Left(GetCountUnreadEmailsInFolderFailure(NotFoundSessionException())),
));
return;
}
if (accountId == null) {
consumeState(Stream.value(
Left(GetCountUnreadEmailsInFolderFailure(NotFoundAccountIdException())),
));
return;
}
consumeState(_getCountUnreadEmailsInFolderInteractor!.execute(
session: session,
accountId: accountId,
));
}
@override
void handleSuccessViewState(Success success) {
if (success is GetCountUnreadEmailsInFolderSuccess) {
actionRequiredFolderCount.value = success.count;
} else {
super.handleSuccessViewState(success);
}
}
@override
void handleFailureViewState(Failure failure) {
if (failure is GetCountUnreadEmailsInFolderFailure) {
actionRequiredFolderCount.value = 0;
} else {
super.handleFailureViewState(failure);
}
}
@override
void onClose() {
_getCountUnreadEmailsInFolderInteractor = null;
super.onClose();
}
}
@@ -117,6 +117,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_te
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_email_sort_order_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/download_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/action_required_folder_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search;
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart';
@@ -124,6 +125,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/cleanup_recent_search_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_action_type_for_email_selection.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_ai_action_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_clear_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_create_new_rule_filter.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_preferences_setting_extension.dart';
@@ -237,6 +239,8 @@ class MailboxDashBoardController extends ReloadableController
final getStoredOidcConfigurationInteractor =
Get.find<GetStoredOidcConfigurationInteractor>();
final getTokenOIDCInteractor = Get.find<GetTokenOIDCInteractor>();
final actionRequiredFolderController =
Get.find<ActionRequiredFolderController>();
final MoveToMailboxInteractor _moveToMailboxInteractor;
final DeleteEmailPermanentlyInteractor _deleteEmailPermanentlyInteractor;
@@ -874,6 +878,9 @@ class MailboxDashBoardController extends ReloadableController
if (PlatformInfo.isMobile) {
injectFCMBindings(session, currentAccountId);
}
if (isAiCapabilitySupported) {
actionRequiredFolderController.injectBinding();
}
_getVacationResponse();
spamReportController.getSpamReportStateAction();
@@ -58,6 +58,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_int
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_action_required_tab_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
@@ -253,6 +254,12 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa
);
}
});
ever(
dashboardController
.actionRequiredFolderController.actionRequiredFolderCount,
onActionRequiredFolderCountChanged,
);
}
void _registerInputFocusListener() {
@@ -16,6 +16,7 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart';
@@ -523,4 +524,22 @@ class ThreadRepositoryImpl extends ThreadRepository {
);
yield emailResponse;
}
@override
Future<int> getCountUnreadEmailsInFolder({
required Session session,
required AccountId accountId,
}) async {
final networkDataSource = mapDataSource[DataSourceType.network]!;
final emailResponse = await networkDataSource.getAllEmail(
session,
accountId,
filter: EmailFilterCondition(
hasKeyword: KeyWordIdentifierExtension.needActionMail.value,
notKeyword: KeyWordIdentifier.emailSeen.value,
),
properties: Properties({EmailProperty.id}),
);
return emailResponse.emailList?.length ?? 0;
}
}
@@ -1,2 +1,3 @@
class NotFoundEmailsDeletedException implements Exception {}
class NotFoundEmailsDeletedException implements Exception {}
class InteractorIsNullException implements Exception {}
@@ -93,4 +93,9 @@ abstract class ThreadRepository {
);
Future<void> clearEmailCacheAndStateCache(AccountId accountId, Session session);
Future<int> getCountUnreadEmailsInFolder({
required Session session,
required AccountId accountId,
});
}
@@ -0,0 +1,18 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
class GettingCountUnreadEmailsInFolder extends LoadingState {}
class GetCountUnreadEmailsInFolderSuccess extends UIState {
final int count;
GetCountUnreadEmailsInFolderSuccess({required this.count});
@override
List<Object> get props => [count];
}
class GetCountUnreadEmailsInFolderFailure extends FeatureFailure {
GetCountUnreadEmailsInFolderFailure(dynamic exception)
: super(exception: exception);
}
@@ -0,0 +1,29 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart';
import 'package:tmail_ui_user/features/thread/domain/state/get_count_emails_in_folder_state.dart';
class GetCountUnreadEmailsInFolderInteractor {
final ThreadRepository _threadRepository;
GetCountUnreadEmailsInFolderInteractor(this._threadRepository);
Stream<Either<Failure, Success>> execute({
required Session session,
required AccountId accountId,
}) async* {
try {
yield Right(GettingCountUnreadEmailsInFolder());
final count = await _threadRepository.getCountUnreadEmailsInFolder(
session: session,
accountId: accountId,
);
yield Right(GetCountUnreadEmailsInFolderSuccess(count: count));
} catch (e) {
yield Left(GetCountUnreadEmailsInFolderFailure(e));
}
}
}
+7 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-16T11:52:06.406969",
"@@last_modified": "2025-12-16T15:21:10.099014",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -5141,5 +5141,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"actionRequiredMailboxDisplayName": "Action required",
"@actionRequiredMailboxDisplayName": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -5464,4 +5464,11 @@ class AppLocalizations {
name: 'actionRequired',
);
}
String get actionRequiredMailboxDisplayName {
return Intl.message(
'Action required',
name: 'actionRequiredMailboxDisplayName',
);
}
}
@@ -19,6 +19,7 @@ class PresentationMailbox with EquatableMixin {
static const String archiveRole = 'archive';
static const String recoveredRole = 'restored messages';
static const String favoriteRole = 'favorite';
static const String actionRequiredRole = 'need-action';
static final PresentationMailbox unifiedMailbox = PresentationMailbox(MailboxId(Id('unified')));
static final PresentationMailbox favoriteFolder = PresentationMailbox(
@@ -26,6 +27,11 @@ class PresentationMailbox with EquatableMixin {
name: MailboxName('Starred'),
role: roleFavorite,
);
static final PresentationMailbox actionRequiredFolder = PresentationMailbox(
MailboxId(Id(actionRequiredRole)),
name: MailboxName('Action required'),
role: roleActionRequired,
);
static final roleInbox = Role(inboxRole);
static final roleTrash = Role(trashRole);
@@ -38,6 +44,7 @@ class PresentationMailbox with EquatableMixin {
static final roleArchive = Role(archiveRole);
static final roleRecovered = Role(recoveredRole);
static final roleFavorite = Role(favoriteRole);
static final roleActionRequired = Role(actionRequiredRole);
final MailboxId id;
final MailboxName? name;
@@ -94,4 +94,196 @@ void main() {
expect(ids, ['1']);
});
});
group('ListMailboxNodeExtension::insertAfterStarredOrInbox', () {
late List<MailboxNode> nodes;
MailboxNode buildNode(String id, {String? name, String? role}) {
final mailbox = PresentationMailbox(
MailboxId(Id(id)),
name: name != null ? MailboxName(name) : null,
role: role != null ? Role(role) : null,
);
return MailboxNode(mailbox);
}
test('should insert after Starred when Starred exists (by role)', () {
nodes = [
buildNode('1', name: 'Inbox', role: 'inbox'),
buildNode('2', name: 'Starred', role: 'favorite'),
buildNode('3', name: 'Trash'),
];
final newNode = buildNode('4', name: 'Draft');
nodes.insertAfterStarredOrInbox(newNode);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2', '4', '3']);
});
test(
'should insert after Starred when role missing but name matches (case-insensitive)',
() {
nodes = [
buildNode('1', name: 'Inbox'),
buildNode('2', name: 'STARRED'), // no role
buildNode('3', name: 'Trash'),
];
final newNode = buildNode('4', name: 'Draft');
nodes.insertAfterStarredOrInbox(newNode);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2', '4', '3']);
});
test('should insert after Inbox when Starred not found', () {
nodes = [
buildNode('1', name: 'Sent'),
buildNode('2', name: 'Inbox', role: 'inbox'),
buildNode('3', name: 'Trash'),
];
final newNode = buildNode('4', name: 'Archive');
nodes.insertAfterStarredOrInbox(newNode);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2', '4', '3']);
});
test('should insert at beginning when neither Starred nor Inbox found', () {
nodes = [
buildNode('1', name: 'Sent'),
buildNode('2', name: 'Trash'),
];
final newNode = buildNode('3', name: 'Draft');
nodes.insertAfterStarredOrInbox(newNode);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['3', '1', '2']);
});
test('should not insert if node with same id already exists', () {
nodes = [
buildNode('1', name: 'Inbox', role: 'inbox'),
buildNode('2', name: 'Starred', role: 'favorite'),
];
final newNode =
buildNode('2', name: 'Starred', role: 'favorite'); // duplicate id
nodes.insertAfterStarredOrInbox(newNode);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2']);
});
});
group('ListMailboxNodeExtension::insertAfterByPriority', () {
late List<MailboxNode> nodes;
MailboxNode buildNode(String id, {String? name, String? role}) {
final mailbox = PresentationMailbox(
MailboxId(Id(id)),
name: name != null ? MailboxName(name) : null,
role: role != null ? Role(role) : null,
);
return MailboxNode(mailbox);
}
bool isInbox(MailboxNode node) =>
node.item.role?.value == PresentationMailbox.inboxRole ||
node.item.name?.name.toLowerCase() == 'inbox';
bool isStarred(MailboxNode node) =>
node.item.role?.value == PresentationMailbox.favoriteRole ||
node.item.name?.name.toLowerCase() == 'starred';
bool isSent(MailboxNode node) =>
node.item.role?.value == PresentationMailbox.sentRole ||
node.item.name?.name.toLowerCase() == 'sent';
test('should insert after first matched priority', () {
nodes = [
buildNode('1', name: 'Inbox', role: 'inbox'),
buildNode('2', name: 'Starred', role: 'favorite'),
buildNode('3', name: 'Sent', role: 'sent'),
];
final newNode = buildNode('4', name: 'Draft');
nodes.insertAfterByPriority(
newNode,
[isStarred, isInbox, isSent],
);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2', '4', '3']);
});
test('should fallback to second priority when first not found', () {
nodes = [
buildNode('1', name: 'Inbox', role: 'inbox'),
buildNode('2', name: 'Trash'),
];
final newNode = buildNode('3', name: 'Draft');
nodes.insertAfterByPriority(
newNode,
[isStarred, isInbox],
);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '3', '2']);
});
test('should insert at beginning when no priority matched', () {
nodes = [
buildNode('1', name: 'Trash'),
buildNode('2', name: 'Archive'),
];
final newNode = buildNode('3', name: 'Draft');
nodes.insertAfterByPriority(
newNode,
[isStarred, isInbox, isSent],
);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['3', '1', '2']);
});
test('should not insert when node with same id already exists', () {
nodes = [
buildNode('1', name: 'Inbox', role: 'inbox'),
buildNode('2', name: 'Sent', role: 'sent'),
];
final newNode = buildNode('2', name: 'Sent', role: 'sent');
nodes.insertAfterByPriority(
newNode,
[isInbox, isSent],
);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1', '2']);
});
test('should insert into empty list as first element', () {
nodes = [];
final newNode = buildNode('1', name: 'Inbox', role: 'inbox');
nodes.insertAfterByPriority(
newNode,
[isInbox],
);
final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
expect(ids, ['1']);
});
});
}
@@ -57,6 +57,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_in
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/action_required_folder_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
@@ -169,6 +170,7 @@ const fallbackGenerators = {
MockSpec<SubaddressingInteractor>(),
MockSpec<CreateDefaultMailboxInteractor>(),
MockSpec<MoveFolderContentInteractor>(),
MockSpec<ActionRequiredFolderController>(fallbackGenerators: fallbackGenerators),
MockSpec<TreeBuilder>(),
MockSpec<VerifyNameInteractor>(),
MockSpec<GetAllMailboxInteractor>(),
@@ -245,6 +247,7 @@ void main() {
final appGridDashboardController = MockAppGridDashboardController();
final spamReportController = MockSpamReportController();
final networkConnectionController = MockNetworkConnectionController();
final actionRequiredFolderController = MockActionRequiredFolderController();
// mock search controller direct dependencies
final quickSearchEmailInteractor = MockQuickSearchEmailInteractor();
@@ -327,6 +330,7 @@ void main() {
Get.put<AppGridDashboardController>(appGridDashboardController);
Get.put<SpamReportController>(spamReportController);
Get.put<NetworkConnectionController>(networkConnectionController);
Get.put<ActionRequiredFolderController>(actionRequiredFolderController);
Get.put<CachingManager>(cachingManager);
Get.put<LanguageCacheManager>(languageCacheManager);
Get.put<AuthorizationInterceptors>(authorizationInterceptors);
@@ -54,6 +54,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_in
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subaddressing_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/action_required_folder_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
@@ -170,6 +171,7 @@ const fallbackGenerators = {
MockSpec<SubaddressingInteractor>(),
MockSpec<CreateDefaultMailboxInteractor>(),
MockSpec<MoveFolderContentInteractor>(),
MockSpec<ActionRequiredFolderController>(fallbackGenerators: fallbackGenerators),
MockSpec<TreeBuilder>(),
MockSpec<VerifyNameInteractor>(),
MockSpec<GetAllMailboxInteractor>(),
@@ -236,6 +238,7 @@ void main() {
final appGridDashboardController = MockAppGridDashboardController();
final spamReportController = MockSpamReportController();
final networkConnectionController = MockNetworkConnectionController();
final actionRequiredFolderController = MockActionRequiredFolderController();
final quickSearchEmailInteractor = MockQuickSearchEmailInteractor();
final saveRecentSearchInteractor = MockSaveRecentSearchInteractor();
@@ -323,6 +326,7 @@ void main() {
Get.put<AppGridDashboardController>(appGridDashboardController);
Get.put<SpamReportController>(spamReportController);
Get.put<NetworkConnectionController>(networkConnectionController);
Get.put<ActionRequiredFolderController>(actionRequiredFolderController);
Get.put<CachingManager>(cachingManager);
Get.put<LanguageCacheManager>(languageCacheManager);
Get.put<AuthorizationInterceptors>(authorizationInterceptors);
@@ -54,6 +54,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_email_sort_order_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/download_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/action_required_folder_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart';
import 'package:tmail_ui_user/features/download/presentation/controllers/download_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
@@ -140,6 +141,7 @@ const fallbackGenerators = {
MockSpec<AppGridDashboardController>(fallbackGenerators: fallbackGenerators),
MockSpec<SpamReportController>(fallbackGenerators: fallbackGenerators),
MockSpec<NetworkConnectionController>(fallbackGenerators: fallbackGenerators),
MockSpec<ActionRequiredFolderController>(fallbackGenerators: fallbackGenerators),
MockSpec<RemoveEmailDraftsInteractor>(),
MockSpec<MoveToMailboxInteractor>(),
MockSpec<MarkAsStarEmailInteractor>(),
@@ -207,6 +209,7 @@ void main() {
final MockAppGridDashboardController appGridDashboardController = MockAppGridDashboardController();
final MockSpamReportController spamReportController = MockSpamReportController();
final MockNetworkConnectionController networkConnectionController = MockNetworkConnectionController();
final MockActionRequiredFolderController actionRequiredFolderController = MockActionRequiredFolderController();
final composerManager = MockComposerManager();
late MailboxDashBoardController mailboxDashboardController;
@@ -353,6 +356,7 @@ void main() {
Get.put<AppGridDashboardController>(appGridDashboardController);
Get.put<SpamReportController>(spamReportController);
Get.put<NetworkConnectionController>(networkConnectionController);
Get.put<ActionRequiredFolderController>(actionRequiredFolderController);
Get.put<GetSessionInteractor>(getSessionInteractor);
Get.put<GetAuthenticatedAccountInteractor>(getAuthenticatedAccountInteractor);
Get.put<UpdateAccountCacheInteractor>(updateAccountCacheInteractor);