Skip to content

TF-4284 Fix refreshing causes "virtual mailboxes" to be hidden#4290

Merged
hoangdat merged 3 commits intomasterfrom
bugfix/tf-4284-refreshing-causes-virtual-mailboxes-to-be-hidden
Apr 10, 2026
Merged

TF-4284 Fix refreshing causes "virtual mailboxes" to be hidden#4290
hoangdat merged 3 commits intomasterfrom
bugfix/tf-4284-refreshing-causes-virtual-mailboxes-to-be-hidden

Conversation

@dab246
Copy link
Copy Markdown
Member

@dab246 dab246 commented Jan 29, 2026

Issue

#4284

Root cause

Because each refresh involves rebuilding the tree before adding the virtual folders (Favorite/Action Required), the mailbox list view is re-rendered.

Solution

Add virtual folders when creating the mailbox tree. Only then trigger the rendering to the UI.

Resolved

Screen.Recording.2026-01-29.at.17.49.38.mov

Summary by CodeRabbit

  • New Features

    • Added a consolidated MailboxCollection model and a single update entry point with optional transformation hooks; AI-driven actions are supported but off by default.
  • Refactor

    • Mailbox tree/list updates moved to an immutable, functional flow with a centralized routine to keep default, personal, team and “all” views synchronized.
    • Favorite and action-required flows now return updated collections instead of mutating state.
  • Tests

    • Tests updated to use the new MailboxCollection shape.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an immutable MailboxCollection model and migrates mailbox tree generation and update flows to use it. Tree builder methods now accept and return MailboxCollection. BaseMailboxController exposes currentMailboxCollection, accepts an optional OnUpdateMailboxCollectionCallback in buildTree/refreshTree, and adds updateMailboxTree to apply a MailboxCollection to controller state. Favorite/action-required folder helpers were converted from mutating void methods to pure functions that return MailboxCollection. Mailbox and search controllers, mailbox creator, and related tests were updated to construct and propagate MailboxCollection; isAINeedsActionEnabled and updateMailboxCollection replaced the previous autoCreateVirtualFolder flow.

Suggested reviewers

  • hoangdat
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'TF-4284 Fix refreshing causes "virtual mailboxes" to be hidden' directly and specifically describes the main bug fix addressed in this changeset. It clearly communicates the issue being resolved and matches the commit message and PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/tf-4284-refreshing-causes-virtual-mailboxes-to-be-hidden

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@lib/features/mailbox/presentation/extensions/handle_action_required_tab_extension.dart`:
- Around line 52-64: The helper _addToAllMailboxes mutates the input list by
calling currentAllMailboxes.add(folder); change it to be non-mutating: when the
id is not present, return a new list containing the existing entries plus folder
(e.g., copy currentAllMailboxes and append folder) instead of modifying
currentAllMailboxes in place so callers' original MailboxCollection.allMailboxes
is not altered.

In
`@lib/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart`:
- Around line 55-67: The function _addFavoriteFolderToAllMailboxes currently
mutates the input list allMailboxes by calling .add(favoriteFolder); change it
to return a new list instead to preserve immutability (e.g., create a copy of
allMailboxes and append favoriteFolder or return a new list combining
allMailboxes and favoriteFolder) while keeping the existing early-exit when the
id already exists; update only inside _addFavoriteFolderToAllMailboxes and
preserve parameter names favoriteFolder and allMailboxes.
🧹 Nitpick comments (1)
lib/features/search/mailbox/presentation/search_mailbox_controller.dart (1)

177-183: Consider passing callback to refreshTree for consistency.

The buildTree call (lines 170-173) passes onUpdateMailboxCollectionCallback: updateMailboxCollection, but the refreshTree call here doesn't pass the callback. In MailboxController, the equivalent refreshTree call (line 629-632) does pass the callback. This inconsistency might cause virtual folders not to be added after a refresh in the search mailbox context.

♻️ Proposed fix
     } else if (success is RefreshChangesAllMailboxSuccess) {
       currentMailboxState = success.currentMailboxState;
-      await refreshTree(success.mailboxList);
+      await refreshTree(
+        success.mailboxList,
+        onUpdateMailboxCollectionCallback: updateMailboxCollection,
+      );
       if (currentContext != null) {
         syncAllMailboxWithDisplayName(currentContext!);
       }

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/features/mailbox/presentation/extensions/handle_favorite_tab_extension.dart (1)

36-52: Avoid mutating the existing default tree list.

currentDefaultFolders aliases defaultTree.root.childrenItems. The subsequent insert mutates the original list, undermining the new immutable flow and potentially causing side effects elsewhere. Clone the list before inserting.

🔧 Suggested fix
-    List<MailboxNode> currentDefaultFolders =
-        defaultMailboxNode.childrenItems ?? [];
+    final currentDefaultFolders =
+        List<MailboxNode>.from(defaultMailboxNode.childrenItems ?? []);
🤖 Fix all issues with AI agents
In
`@lib/features/mailbox/presentation/extensions/handle_action_required_tab_extension.dart`:
- Around line 40-49: The _addToDefaultMailboxTree function unconditionally
inserts a virtual Action Required node which can create duplicates; before
calling children.insertAfterStarredOrInbox(MailboxNode(folder)) check the
existing children (root.childrenItems) for an existing MailboxNode representing
the same virtual folder (e.g., compare mailbox id or a distinguishing flag on
PresentationMailbox) and skip insertion (return currentDefaultTree or
root.copyWith(children: children)) if a match is found; use the same identifiers
present in this file (function _addToDefaultMailboxTree, PresentationMailbox,
MailboxTree, MailboxNode, insertAfterStarredOrInbox, root.childrenItems) so the
guard prevents inserting a duplicate node.

In `@lib/features/search/mailbox/presentation/search_mailbox_controller.dart`:
- Around line 224-235: The failure branch is bypassing virtual-folder gating by
calling addActionRequiredFolder directly; instead, construct the
MailboxCollection (using MailboxCollection with
allMailboxes/defaultTree/personalTree/teamTree) and pass it into
updateMailboxCollection so updateMailboxCollection → autoCreateVirtualFolder
applies feature-flag checks (isAINeedsActionEnabled) and also ensures Favorites
get created; replace the direct addActionRequiredFolder call with a call to
updateMailboxCollection(mailboxCollection: MailboxCollection(...),
isRefreshTrigger: false) to unify behavior with the success path.

@github-actions
Copy link
Copy Markdown

This PR has been deployed to https://linagora.github.io/tmail-flutter/4290.

tddang-linagora
tddang-linagora previously approved these changes Apr 6, 2026
Comment on lines 75 to 79
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how we apply the same pattern with other, return a MailboxCollection instead of mutate directly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Signed-off-by: dab246 <tdvu@linagora.com>
@dab246 dab246 force-pushed the bugfix/tf-4284-refreshing-causes-virtual-mailboxes-to-be-hidden branch from 5c22259 to e26bf41 Compare April 9, 2026 06:23
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

tddang-linagora
tddang-linagora previously approved these changes Apr 9, 2026
@dab246 dab246 requested a review from hoangdat April 10, 2026 03:55
@hoangdat
Copy link
Copy Markdown
Member

how about to add these tests to mailbox_tree_builder_test?

  group('MailboxCollection allMailboxes field', () {
    final inbox = PresentationMailbox(
      MailboxId(Id('inbox')),
      name: MailboxName('Inbox'),
      role: Role('inbox'),
    );
    final sent = PresentationMailbox(
      MailboxId(Id('sent')),
      name: MailboxName('Sent'),
      role: Role('sent'),
    );

    test(
        'generateMailboxTreeInUI: returned allMailboxes matches input list',
        () async {
      final input = [inbox, sent];

      final result = await TreeBuilder().generateMailboxTreeInUI(
        allMailboxes: input,
        currentCollection: MailboxCollection.empty(),
      );

      expect(result.allMailboxes.length, equals(input.length));
      expect(
        result.allMailboxes.map((m) => m.id).toList(),
        containsAll(input.map((m) => m.id)),
      );
    });

    test(
        'generateMailboxTreeInUIAfterRefreshChanges: returned allMailboxes matches input list',
        () async {
      final input = [inbox, sent];

      final result =
          await TreeBuilder().generateMailboxTreeInUIAfterRefreshChanges(
        allMailboxes: input,
        currentCollection: MailboxCollection.empty(),
      );

      expect(result.allMailboxes.length, equals(input.length));
      expect(
        result.allMailboxes.map((m) => m.id).toList(),
        containsAll(input.map((m) => m.id)),
      );
    });

    test(
        'generateMailboxTreeInUIAfterRefreshChanges: allMailboxes in result excludes virtual folders from current collection',
        () async {
      final serverMailboxes = [inbox, sent];
      final virtualFavorite = PresentationMailbox.favoriteFolder;

      final previousCollection = MailboxCollection(
        allMailboxes: [inbox, virtualFavorite],
        defaultTree: MailboxTree(MailboxNode.root()),
        personalTree: MailboxTree(MailboxNode.root()),
        teamMailboxTree: MailboxTree(MailboxNode.root()),
      );

      final result =
          await TreeBuilder().generateMailboxTreeInUIAfterRefreshChanges(
        allMailboxes: serverMailboxes,
        currentCollection: previousCollection,
      );

      expect(
        result.allMailboxes.every((m) => !m.isVirtualFolder),
        isTrue,
        reason:
            'Tree builder must not inject virtual folders — they are added by the callback after buildTree/refreshTree',
      );
      expect(result.allMailboxes.length, equals(serverMailboxes.length));
    });
  });

… hidden

Signed-off-by: dab246 <tdvu@linagora.com>
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Improved (4 files improve in Code Health)

Gates Passed
3 Quality Gates Passed

See analysis details in CodeScene

View Improvements
File Code Health Impact Categories Improved
mailbox_controller.dart 6.75 → 6.87 Complex Method
mailbox_tree_builder.dart 9.54 → 10.00 Bumpy Road Ahead, Excess Number of Function Arguments
mailbox_tree_builder_test.dart 9.10 → 9.39 Code Duplication
rule_filter_creator_controller_test.dart 8.74 → 8.76 Large Method

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@dab246
Copy link
Copy Markdown
Member Author

dab246 commented Apr 10, 2026

how about to add these tests to mailbox_tree_builder_test?

Test case 3 has been applied, along with some additional test cases. Test cases 1 and 2 have been skipped because:

Test 1 is technically correct but weak — it only checks no items are lost/duplicated, while the meaningful behavior (selected mailbox → deactivated) is already covered by Test 4. It adds overlap without value.

Test 2 is a tautology — it asserts list == list since the method returns input as-is. It can never fail, so it provides no real safety.

More valuable tests are those ensuring no stale data (virtual folders, deleted mailboxes) leaks after refresh, which are already covered elsewhere.

@hoangdat hoangdat merged commit cb5d436 into master Apr 10, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants