Skip to content

TW-4308 Add labels for several messages part 2#4431

Open
tddang-linagora wants to merge 7 commits intofeature/TW-4308-Add-labels-for-several-messages-part-1from
feature/TW-4308-Add-labels-for-several-messages-part-2
Open

TW-4308 Add labels for several messages part 2#4431
tddang-linagora wants to merge 7 commits intofeature/TW-4308-Add-labels-for-several-messages-part-1from
feature/TW-4308-Add-labels-for-several-messages-part-2

Conversation

@tddang-linagora
Copy link
Copy Markdown
Collaborator

@tddang-linagora tddang-linagora commented Apr 1, 2026

Issue

Demo

Screen.Recording.2026-02-09.at.16.46.27.mov

Summary by CodeRabbit

  • New Features
    • Added bulk label application: users can now select multiple emails and apply labels in a single action.
    • Introduced interactive label selection modal with checkbox support for choosing labels.
    • Added "Label as" action button to email selection interface for quick label management.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97e5bfb9-77e9-41d6-870d-24470fa8104a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/TW-4308-Add-labels-for-several-messages-part-2

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.

Comment on lines +63 to +94
if (session == null) {
emitFailure(
controller: this,
failure: AddListLabelsToListEmailsFailure(
exception: NotFoundSessionException(),
labelDisplays: labelDisplays,
),
);
return;
}

if (accountId == null) {
emitFailure(
controller: this,
failure: AddListLabelsToListEmailsFailure(
exception: NotFoundAccountIdException(),
labelDisplays: labelDisplays,
),
);
return;
}

if (labelKeywords.isEmpty) {
emitFailure(
controller: this,
failure: AddListLabelsToListEmailsFailure(
exception: const LabelKeywordIsNull(),
labelDisplays: labelDisplays,
),
);
return;
}
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.

Avoid boilerplate by using a validate method:

Exception? _validateParams(List<KeyWordIdentifier> keywords) {
  if (_labelController.session == null) return NotFoundSessionException();
  if (_labelController.accountId == null) return NotFoundAccountIdException();
  if (keywords.isEmpty) return const LabelKeywordIsNull();
  return null;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

Comment on lines +105 to +111
if (success is AddListLabelsToListEmailsSuccess) {
toastManager.showMessageSuccess(success);
_pendingOnSync?.call(success.emailIds, success.labelKeywords, shouldRemove: false);
} else if (success is AddListLabelsToListEmailsHasSomeFailure) {
toastManager.showMessageSuccess(success);
_pendingOnSync?.call(success.emailIds, success.labelKeywords, shouldRemove: false);
}
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.

Avoid boilerplate:

  final (emailIds, labelKeywords) = switch (success) {
    AddListLabelsToListEmailsSuccess s => (s.emailIds, s.labelKeywords),
    AddListLabelsToListEmailsHasSomeFailure s => (s.emailIds, s.labelKeywords),
    _ => return,
  };
  toastManager.showMessageSuccess(success);
  _pendingOnSync?.call(emailIds, labelKeywords, shouldRemove: false);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

else if clause deleted.

}

// Store callback before consumeState (stream-based async)
_pendingOnSync = onSync;
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.

Be careful with race condition:

_pendingOnSync is a single field. If the user opens the modal and calls _addLabels twice in a row (double-tap, or two different email lists), the second call will overwrite the _pendingOnSync of the first call. When the stream from the first call resolves, it will call the callback from the second call, resulting in incorrect data.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

_pendingOnSync is deleted.

}

// Store callback before consumeState (stream-based async)
_pendingOnSync = onSync;
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.

Beware of memory leaks.

_pendingOnSync stores a closure that can capture BuildContext, widget state, or large objects from the caller. If the consumeState stream doesn't emit (timeout, uncaught error, dispose early), this closure will be held indefinitely until onClose() is called. If the controller is reused (GetX singleton scope), onClose() may never be called at the right time.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

_pendingOnSync is deleted.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

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: 1

🧹 Nitpick comments (2)
lib/features/labels/presentation/widgets/choose_label_modal.dart (1)

169-180: Consider using consistent equality approach for label selection.

The toggle logic uses contains(selectedLabel) at line 172 (which uses object/reference equality) but removes by label.id at line 174. This works correctly because the same Label instances from widget.labels are used throughout, but using id-based comparison for both would be more robust:

♻️ Optional: Use id-based comparison consistently
 void _onToggleLabel(Label selectedLabel) {
   final currentLabels = _selectedLabelStateNotifier.value;

-  if (currentLabels.contains(selectedLabel)) {
+  final isAlreadySelected = currentLabels.any((label) => label.id == selectedLabel.id);
+  if (isAlreadySelected) {
     _selectedLabelStateNotifier.value =
         currentLabels.where((label) => label.id != selectedLabel.id).toList();
   } else {
     _selectedLabelStateNotifier.value = [...currentLabels, selectedLabel];
   }

   _addLabelStateNotifier.value = _selectedLabelStateNotifier.value.isNotEmpty;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/labels/presentation/widgets/choose_label_modal.dart` around
lines 169 - 180, The toggle logic in _onToggleLabel mixes object identity
(contains(selectedLabel)) with id-based removal; make both checks id-based for
robustness by replacing the contains check with an id comparison (e.g., check if
any label in _selectedLabelStateNotifier.value has id == selectedLabel.id) and
when adding ensure you append selectedLabel only if no existing label has the
same id; keep the removal using label.id filtering and update
_addLabelStateNotifier.value as before.
lib/features/search/email/presentation/search_email_bindings.dart (1)

27-30: Register this delegate in one shared scope.

AddListLabelToListEmailsDelegate is now Get.put(...) here and again in lib/features/thread/presentation/thread_bindings.dart, but disposeBindings() still only tears down SearchEmailController. Either hoist the delegate into a shared parent binding or explicitly delete it here so its lifetime is unambiguous.

Also applies to: 53-55

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/search/email/presentation/search_email_bindings.dart` around
lines 27 - 30, The AddListLabelToListEmailsDelegate is being instantiated with
Get.put in multiple places (search_email_bindings and thread_bindings) while
disposeBindings only tears down SearchEmailController, causing ambiguous
lifetime; fix by moving the Get.put(AddListLabelToListEmailsDelegate(...)) into
a shared parent binding (so a single registration serves both scopes) or, if
scoped to search, call Get.delete<AddListLabelToListEmailsDelegate>() in
disposeBindings of SearchEmailController’s bindings to explicitly remove it;
update the binding code that references AddListLabelToListEmailsDelegate,
LabelController, and AddListLabelToListEmailsInteractor accordingly so there is
a single clear owner of the delegate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/thread/presentation/thread_controller.dart`:
- Around line 1316-1318: The current handler for EmailActionType.labelAs clears
the selection by calling cancelSelectEmail() before opening the label chooser,
which loses selection if the modal is dismissed; remove the immediate
cancelSelectEmail() call and rely on openChooseLabelModal(emails) to receive
onCancel: cancelSelectEmail so cleanup happens only after the modal flow
completes (i.e., ensure EmailActionType.labelAs invokes
openChooseLabelModal(emails) without preemptively calling cancelSelectEmail).

---

Nitpick comments:
In `@lib/features/labels/presentation/widgets/choose_label_modal.dart`:
- Around line 169-180: The toggle logic in _onToggleLabel mixes object identity
(contains(selectedLabel)) with id-based removal; make both checks id-based for
robustness by replacing the contains check with an id comparison (e.g., check if
any label in _selectedLabelStateNotifier.value has id == selectedLabel.id) and
when adding ensure you append selectedLabel only if no existing label has the
same id; keep the removal using label.id filtering and update
_addLabelStateNotifier.value as before.

In `@lib/features/search/email/presentation/search_email_bindings.dart`:
- Around line 27-30: The AddListLabelToListEmailsDelegate is being instantiated
with Get.put in multiple places (search_email_bindings and thread_bindings)
while disposeBindings only tears down SearchEmailController, causing ambiguous
lifetime; fix by moving the Get.put(AddListLabelToListEmailsDelegate(...)) into
a shared parent binding (so a single registration serves both scopes) or, if
scoped to search, call Get.delete<AddListLabelToListEmailsDelegate>() in
disposeBindings of SearchEmailController’s bindings to explicitly remove it;
update the binding code that references AddListLabelToListEmailsDelegate,
LabelController, and AddListLabelToListEmailsInteractor accordingly so there is
a single clear owner of the delegate.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5466c47e-7046-4717-aed9-4c304dd9d388

📥 Commits

Reviewing files that changed from the base of the PR and between e78589f and aab8bf7.

📒 Files selected for processing (21)
  • core/lib/presentation/views/button/default_close_button_widget.dart
  • lib/features/email/domain/state/labels/add_list_label_to_list_email_state.dart
  • lib/features/email/domain/usecases/labels/add_list_label_to_list_emails_interactor.dart
  • lib/features/labels/domain/model/add_list_labels_to_list_emails_params.dart
  • lib/features/labels/presentation/delegates/add_list_label_to_list_emails_delegate.dart
  • lib/features/labels/presentation/widgets/choose_label_modal.dart
  • lib/features/mailbox/presentation/widgets/labels/label_list_item.dart
  • lib/features/mailbox_dashboard/presentation/bindings/email_action_interactor_bindings.dart
  • lib/features/mailbox_dashboard/presentation/extensions/labels/handle_logic_label_extension.dart
  • lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart
  • lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart
  • lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart
  • lib/features/search/email/presentation/extension/handle_press_email_selection_action.dart
  • lib/features/search/email/presentation/search_email_bindings.dart
  • lib/features/search/email/presentation/search_email_controller.dart
  • lib/features/search/email/presentation/search_email_view.dart
  • lib/features/thread/presentation/extensions/handle_press_email_selection_action.dart
  • lib/features/thread/presentation/thread_bindings.dart
  • lib/features/thread/presentation/thread_controller.dart
  • lib/features/thread/presentation/thread_view.dart
  • lib/features/thread_detail/presentation/extension/labels/add_label_to_thread_extension.dart

Comment on lines +1316 to +1318
EmailActionType.labelAs: (emails) {
cancelSelectEmail();
openChooseLabelModal(emails);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep the selection until the label modal finishes.

Clearing selection before opening the chooser means a dismissed modal still drops the user's current selection. openChooseLabelModal() already passes onCancel: cancelSelectEmail, so cleanup can happen after the flow completes.

♻️ Suggested change
       EmailActionType.labelAs: (emails) {
-        cancelSelectEmail();
         openChooseLabelModal(emails);
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/thread/presentation/thread_controller.dart` around lines 1316 -
1318, The current handler for EmailActionType.labelAs clears the selection by
calling cancelSelectEmail() before opening the label chooser, which loses
selection if the modal is dismissed; remove the immediate cancelSelectEmail()
call and rely on openChooseLabelModal(emails) to receive onCancel:
cancelSelectEmail so cleanup happens only after the modal flow completes (i.e.,
ensure EmailActionType.labelAs invokes openChooseLabelModal(emails) without
preemptively calling cancelSelectEmail).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • lib/features/thread/presentation/thread_controller.dart

Commit: 1fbdda6ecca121e6ebfaa9c37d951598a24edbe6

The changes have been pushed to the feature/TW-4308-Add-labels-for-several-messages-part-2 branch.

Time taken: 2m 21s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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 (1 files improve in Code Health)

Gates Passed
3 Quality Gates Passed

See analysis details in CodeScene

View Improvements
File Code Health Impact Categories Improved
thread_controller.dart 6.90 → 7.17 Complex Method

Absence of Expected Change Pattern

  • tmail-flutter/lib/features/mailbox/presentation/widgets/labels/label_list_item.dart is usually changed with: tmail-flutter/lib/features/mailbox/presentation/widgets/labels/label_list_view.dart

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.

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.

2 participants