diff --git a/assets/images/ic_mailbox_favorite.svg b/assets/images/ic_mailbox_favorite.svg
new file mode 100644
index 000000000..5cca3f986
--- /dev/null
+++ b/assets/images/ic_mailbox_favorite.svg
@@ -0,0 +1,5 @@
+
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 0cfd8d637..f62ec5738 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -44,6 +44,7 @@ class ImagePaths {
String get icMailboxArchived => _getImagePath('ic_mailbox_archived.svg');
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 icFilterSelected => _getImagePath('ic_filter_selected.svg');
String get icFilterMessageAll => _getImagePath('ic_filter_message_all.svg');
String get icFilterMessageAttachments => _getImagePath('ic_filter_message_attachments.svg');
diff --git a/lib/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart b/lib/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart
new file mode 100644
index 000000000..373b96311
--- /dev/null
+++ b/lib/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart
@@ -0,0 +1,48 @@
+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/main/routes/route_navigation.dart';
+
+extension HandleFavoriteTabExtension on BaseMailboxController {
+ void addFavoriteFolderToMailboxList() {
+ PresentationMailbox favoriteFolder = PresentationMailbox.favoriteFolder;
+ if (currentContext != null) {
+ favoriteFolder = favoriteFolder.copyWith(
+ displayName: favoriteFolder.getDisplayName(currentContext!),
+ );
+ }
+
+ _addFavoriteFolderToDefaultMailboxTree(favoriteFolder);
+ _addFavoriteFolderToAllMailboxes(favoriteFolder);
+ }
+
+ void _addFavoriteFolderToDefaultMailboxTree(
+ PresentationMailbox favoriteFolder,
+ ) {
+ final defaultMailboxNode = defaultMailboxTree.value.root;
+ List currentDefaultFolders =
+ defaultMailboxNode.childrenItems ?? [];
+
+ if (currentDefaultFolders.isEmpty) {
+ currentDefaultFolders.add(MailboxNode(favoriteFolder));
+ } else {
+ currentDefaultFolders.insertAfterInbox(MailboxNode(favoriteFolder));
+ }
+
+ defaultMailboxTree.value = MailboxTree(
+ defaultMailboxNode.copyWith(children: currentDefaultFolders),
+ );
+ }
+
+ void _addFavoriteFolderToAllMailboxes(PresentationMailbox favoriteFolder) {
+ final alreadyExists = allMailboxes.any(
+ (mailbox) => mailbox.id == favoriteFolder.id,
+ );
+ if (alreadyExists) return;
+
+ allMailboxes.add(favoriteFolder);
+ }
+}
diff --git a/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart b/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart
index 8338a14ed..8e34e9096 100644
--- a/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart
+++ b/lib/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart
@@ -1,7 +1,24 @@
-
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
+import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart';
extension ListMailboxNodeExtension on List {
List get mailboxIds => map((node) => node.item.id).toList();
-}
\ No newline at end of file
+
+ 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',
+ );
+
+ if (index != -1) {
+ insert(index + 1, newNode);
+ } else {
+ insert(0, newNode);
+ }
+ }
+}
diff --git a/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart
index cd7260b5a..c91f17709 100644
--- a/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart
+++ b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart
@@ -15,6 +15,8 @@ extension PresentationMailboxExtension on PresentationMailbox {
switch(role!.value.toLowerCase()) {
case PresentationMailbox.inboxRole:
return AppLocalizations.of(context).inboxMailboxDisplayName;
+ case PresentationMailbox.favoriteRole:
+ return AppLocalizations.of(context).favoriteMailboxDisplayName;
case PresentationMailbox.archiveRole:
return AppLocalizations.of(context).archiveMailboxDisplayName;
case PresentationMailbox.draftsRole:
@@ -69,6 +71,8 @@ extension PresentationMailboxExtension on PresentationMailbox {
switch(role!.value) {
case PresentationMailbox.inboxRole:
return imagePaths.icMailboxInbox;
+ case PresentationMailbox.favoriteRole:
+ return imagePaths.icMailboxFavorite;
case PresentationMailbox.draftsRole:
return imagePaths.icMailboxDrafts;
case PresentationMailbox.outboxRole:
diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart
index 363499f67..568b0bb9b 100644
--- a/lib/features/mailbox/presentation/mailbox_controller.dart
+++ b/lib/features/mailbox/presentation/mailbox_controller.dart
@@ -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_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';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
@@ -256,12 +257,14 @@ class MailboxController extends BaseMailboxController
viewState.value.fold(
(failure) {
if (failure is GetAllMailboxFailure) {
+ addFavoriteFolderToMailboxList();
mailboxDashBoardController.updateRefreshAllMailboxState(Left(RefreshAllMailboxFailure()));
showRetryToast(failure);
}
},
(success) {
if (success is GetAllMailboxSuccess) {
+ addFavoriteFolderToMailboxList();
mailboxDashBoardController.updateRefreshAllMailboxState(Right(RefreshAllMailboxSuccess()));
_handleCreateDefaultFolderIfMissing(mailboxDashBoardController.mapDefaultMailboxIdByRole);
_handleDataFromNavigationRouter();
diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart
index 271192063..d6f0a8280 100644
--- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart
+++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart
@@ -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_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';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart';
@@ -207,6 +208,20 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa
}
}
+ @override
+ void onDone() {
+ super.onDone();
+ viewState.value.fold((failure) {
+ if (failure is GetAllMailboxFailure) {
+ addFavoriteFolderToMailboxList();
+ }
+ }, (success) {
+ if (success is GetAllMailboxSuccess) {
+ addFavoriteFolderToMailboxList();
+ }
+ });
+ }
+
void _initializeDebounceTimeTextSearchChange() {
_deBouncerTime = Debouncer(
const Duration(milliseconds: 300),
@@ -450,7 +465,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa
}
}
-
void _handleSubAddressingAction(
MailboxId mailboxId,
Map?>? currentRights,
diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb
index 9dd41e721..b05ab4f60 100644
--- a/lib/l10n/intl_messages.arb
+++ b/lib/l10n/intl_messages.arb
@@ -5063,5 +5063,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
+ },
+ "favoriteMailboxDisplayName": "Starred",
+ "@favoriteMailboxDisplayName": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
}
}
\ No newline at end of file
diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart
index 89158e39f..abe6ba09e 100644
--- a/lib/main/localizations/app_localizations.dart
+++ b/lib/main/localizations/app_localizations.dart
@@ -5359,4 +5359,11 @@ class AppLocalizations {
name: 'moveFolderContentToastMessage',
);
}
+
+ String get favoriteMailboxDisplayName {
+ return Intl.message(
+ 'Starred',
+ name: 'favoriteMailboxDisplayName',
+ );
+ }
}
diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart
index 00d225b48..0d486949b 100644
--- a/model/lib/mailbox/presentation_mailbox.dart
+++ b/model/lib/mailbox/presentation_mailbox.dart
@@ -18,8 +18,14 @@ class PresentationMailbox with EquatableMixin {
static const String spamRole = 'spam';
static const String archiveRole = 'archive';
static const String recoveredRole = 'restored messages';
+ static const String favoriteRole = 'favorite';
static final PresentationMailbox unifiedMailbox = PresentationMailbox(MailboxId(Id('unified')));
+ static final PresentationMailbox favoriteFolder = PresentationMailbox(
+ MailboxId(Id(favoriteRole)),
+ name: MailboxName('Starred'),
+ role: Role(favoriteRole),
+ );
static final roleInbox = Role(inboxRole);
static final roleTrash = Role(trashRole);
diff --git a/test/features/mailbox/presentation/extensions/list_mailbox_node_extension_test.dart b/test/features/mailbox/presentation/extensions/list_mailbox_node_extension_test.dart
new file mode 100644
index 000000000..6f0737fae
--- /dev/null
+++ b/test/features/mailbox/presentation/extensions/list_mailbox_node_extension_test.dart
@@ -0,0 +1,97 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:jmap_dart_client/jmap/core/id.dart';
+import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
+import 'package:model/mailbox/presentation_mailbox.dart';
+import 'package:tmail_ui_user/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart';
+import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart';
+
+void main() {
+ group('ListMailboxNodeExtension::insertAfterInbox', () {
+ late List 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);
+ }
+
+ setUp(() {
+ nodes = [
+ buildNode('1', name: 'Sent'),
+ buildNode('2', name: 'Inbox', role: 'inbox'),
+ buildNode('3', name: 'Trash'),
+ ];
+ });
+
+ test('should insert new node right after Inbox (by role)', () {
+ final newNode = buildNode('4', name: 'Draft');
+ nodes.insertAfterInbox(newNode);
+
+ final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
+ expect(ids, ['1', '2', '4', '3']);
+ });
+
+ test('should insert new node right after Inbox (by name when role missing)',
+ () {
+ nodes = [
+ buildNode('1', name: 'Sent'),
+ buildNode('2', name: 'inbox'), // no role
+ buildNode('3', name: 'Trash'),
+ ];
+
+ final newNode = buildNode('4', name: 'Archive');
+ nodes.insertAfterInbox(newNode);
+
+ final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
+ expect(ids, ['1', '2', '4', '3']);
+ });
+
+ test('should insert new node at the beginning when no Inbox found', () {
+ nodes = [
+ buildNode('1', name: 'Sent'),
+ buildNode('2', name: 'Archive'),
+ ];
+
+ final newNode = buildNode('3', name: 'Draft');
+ nodes.insertAfterInbox(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', () {
+ final newNode =
+ buildNode('2', name: 'Inbox', role: 'inbox'); // same id as existing
+ nodes.insertAfterInbox(newNode);
+
+ // The list should remain unchanged
+ final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
+ expect(ids, ['1', '2', '3']);
+ });
+
+ test('should handle case-insensitive inbox name', () {
+ nodes = [
+ buildNode('1', name: 'Sent'),
+ buildNode('2', name: 'INBOX'),
+ ];
+
+ final newNode = buildNode('3', name: 'Draft');
+ nodes.insertAfterInbox(newNode);
+
+ final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
+ expect(ids, ['1', '2', '3']);
+ });
+
+ test('should insert into empty list as the first element', () {
+ nodes = [];
+ final newNode = buildNode('1', name: 'Inbox', role: 'inbox');
+ nodes.insertAfterInbox(newNode);
+
+ final ids = nodes.mailboxIds.map((id) => id.id.value).toList();
+ expect(ids, ['1']);
+ });
+ });
+}