Skip to content

TF-4385 Fix spam banner show it once per day#4408

Open
dab246 wants to merge 4 commits intomasterfrom
bugfix/tf-4385-fix-spam-banner
Open

TF-4385 Fix spam banner show it once per day#4408
dab246 wants to merge 4 commits intomasterfrom
bugfix/tf-4385-fix-spam-banner

Conversation

@dab246
Copy link
Copy Markdown
Member

@dab246 dab246 commented Mar 25, 2026

Issue

#4385

Resolved

Screen.Recording.2026-03-25.at.15.23.26.mov

Summary by CodeRabbit

  • Bug Fixes
    • Spam report banner cooldown extended from 12 to 24 hours; improved handling when unread spam is zero or when stored dismissal timestamps are abnormal
    • Spam banner updates consolidated to a single refresh path to prevent inconsistent state and ensure correct dismissal tracking
  • Behavior
    • Dismissing the spam banner records the dismissal timestamp more reliably to avoid premature re-showing
  • Tests
    • Added comprehensive tests covering cooldown boundaries, dismissal behavior, clock-skew and error scenarios

@dab246
Copy link
Copy Markdown
Member Author

dab246 commented Mar 25, 2026

@coderabbitai please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

@dab246: Sure, I'll review the changes in this PR right away!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3d452a3-73b0-4782-81c4-d9799194a7aa

📥 Commits

Reviewing files that changed from the base of the PR and between 9c513a5 and d7fdb5d.

📒 Files selected for processing (1)
  • test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart
✅ Files skipped from review due to trivial changes (1)
  • test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart

Walkthrough

This PR changes spam-report timestamp handling from DateTime to epoch milliseconds (int) across datasource, repository, and interactor layers; increases the spam banner cooldown from 12 to 24 hours; renames NotFoundLastTimeDismissedSpamReportException to SpamDismissCooldownActiveException and adds NoUnreadSpamEmailsException; removes MailboxDashBoardController.getSpamReportBanner() in favor of refreshSpamReportBanner(); updates SpamReportController to validate failures before clearing the presentation mailbox; and adds unit tests for the interactor and controller.

Possibly related PRs

Suggested reviewers

  • hoangdat
  • tddang-linagora
🚥 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 title 'TF-4385 Fix spam banner show it once per day' directly summarizes the main objective: fixing the spam banner display logic to show it once per day (24-hour cooldown), which aligns with the substantial refactoring across multiple layers (repository, interactor, controller) to implement millisecond-based timestamp tracking and proper interval validation.
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-4385-fix-spam-banner

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

@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.

Caution

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

⚠️ Outside diff range comments (2)
lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart (2)

20-23: ⚠️ Potential issue | 🟡 Minor

Return type mismatch with abstract contract.

The method deleteLastTimeDismissedSpamReported() returns Future<bool> here, but the abstract SpamReportDataSource declares Future<void>. While Dart covariance allows this to compile, it creates an inconsistency with the contract.

Proposed fix
 `@override`
-Future<bool> deleteLastTimeDismissedSpamReported() {
+Future<void> deleteLastTimeDismissedSpamReported() {
   throw UnimplementedError();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart`
around lines 20 - 23, The implementation of
deleteLastTimeDismissedSpamReported() currently returns Future<bool> which
mismatches the abstract SpamReportDataSource signature (Future<void>); update
the method signature in hive_spam_report_datasource_impl.dart to return
Future<void> and adjust the method body to not return a boolean (e.g., perform
the deletion and complete/throw on error) so it conforms to
SpamReportDataSource.deleteLastTimeDismissedSpamReported.

47-50: ⚠️ Potential issue | 🟡 Minor

Return type mismatch with abstract contract.

Same issue: storeLastTimeDismissedSpamReported returns Future<bool> but the abstract declares Future<void>.

Proposed fix
 `@override`
-Future<bool> storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) {
+Future<void> storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) {
   throw UnimplementedError();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart`
around lines 47 - 50, The implementation of storeLastTimeDismissedSpamReported
in HiveSpamReportDatasourceImpl has the wrong return type (Future<bool>) and
must match the abstract contract (Future<void>); update the method signature to
Future<void> storeLastTimeDismissedSpamReported(DateTime
lastTimeDismissedSpamReported) and implement the body to persist the timestamp
to Hive (or the appropriate storage used elsewhere in this class) without
returning a value—allow any async operations to complete or throw errors as
needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart`:
- Around line 20-23: The implementation of deleteLastTimeDismissedSpamReported()
currently returns Future<bool> which mismatches the abstract
SpamReportDataSource signature (Future<void>); update the method signature in
hive_spam_report_datasource_impl.dart to return Future<void> and adjust the
method body to not return a boolean (e.g., perform the deletion and
complete/throw on error) so it conforms to
SpamReportDataSource.deleteLastTimeDismissedSpamReported.
- Around line 47-50: The implementation of storeLastTimeDismissedSpamReported in
HiveSpamReportDatasourceImpl has the wrong return type (Future<bool>) and must
match the abstract contract (Future<void>); update the method signature to
Future<void> storeLastTimeDismissedSpamReported(DateTime
lastTimeDismissedSpamReported) and implement the body to persist the timestamp
to Hive (or the appropriate storage used elsewhere in this class) without
returning a value—allow any async operations to complete or throw errors as
needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c12bfab3-18d6-4bc7-b949-58a8d1a499e0

📥 Commits

Reviewing files that changed from the base of the PR and between a8dca32 and cb20a81.

📒 Files selected for processing (11)
  • lib/features/mailbox/presentation/mailbox_controller.dart
  • lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart
  • lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart
  • lib/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart
  • lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart
  • lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart
  • lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart
  • lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart
  • lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart
  • lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
  • lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart
💤 Files with no reviewable changes (2)
  • lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart
  • lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart

@github-actions
Copy link
Copy Markdown

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

Comment on lines +52 to +53
final isIntervalElapsed =
elapsed.inHours > spamReportBannerDisplayIntervalInHours;
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.

Suggested change
final isIntervalElapsed =
elapsed.inHours > spamReportBannerDisplayIntervalInHours;
final isIntervalElapsed =
elapsed.inHours >= spamReportBannerDisplayIntervalInHours;

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


final lastTime = DateTime.fromMillisecondsSinceEpoch(lastTimeDismissedMs);
final now = DateTime.now();
final elapsed = now.difference(lastTime);
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.

because elapsed can be negative (user change timezone, ...), so we should check also
if (elapsed.isNegative) return true;

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


void _validateSpamMailboxChanged(GetSpamMailboxCachedFailure failure) {
if (failure.exception is NoUnreadSpamEmailsException) {
final currentSpamMailbox = presentationSpamMailbox.value;
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.

IMO in this case of NoUnreadSpamEmailsException, presentationSpamMailbox also null?

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.

It might have a null value, but we don't care if it's null.

Copy link
Copy Markdown

@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 (1)
test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart (1)

129-130: Avoid fixed 100ms sleeps in these async tests.

These waits make the suite slower and can still flake under CI scheduling. Prefer a small helper that waits for the mocked stream/interaction to finish so the assertions run deterministically without the repeated timing magic number.

Also applies to: 142-143, 155-156, 170-170, 190-190

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

In
`@test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart`
around lines 129 - 130, Replace the fixed Future.delayed(const
Duration(milliseconds: 100)) sleeps with a deterministic wait helper: implement
a small async helper (e.g., waitForStreamEvent or waitUntilIdle) that awaits the
mocked stream or the specific interaction to complete using a Completer or by
polling a condition (e.g., expecting mockStream.toList()/firstWhere, or checking
a flag set by the mock), then use that helper in place of Future.delayed in the
tests (references: Future.delayed occurrences in
spam_report_controller_test.dart) so assertions run only after the actual async
work completes and the tests stop relying on a magic timing number.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart`:
- Around line 88-139: The tests only check 1h and 25h which are valid for both
the old 12h and new 24h cooldown; update the GetSpamMailboxCachedInteractor
tests to assert the new 24-hour threshold by adding at least one mid-window case
(e.g., msAgo(13) or msAgo(23)) and an exact boundary case msAgo(24), and
optionally a small future timestamp to cover clock-skew; modify the groups that
call interactor.execute(accountId, userName) and the mock
spamReportRepository.getLastTimeDismissedSpamReportedMilliseconds() to include
these new msAgo values and expectations (use same
Right(GetSpamMailboxCachedLoading()), then assert either success via
Right(GetSpamMailboxCachedSuccess(spamMailbox)) or the appropriate exception
like SpamDismissCooldownActiveException or NoUnreadSpamEmailsException).

---

Nitpick comments:
In
`@test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart`:
- Around line 129-130: Replace the fixed Future.delayed(const
Duration(milliseconds: 100)) sleeps with a deterministic wait helper: implement
a small async helper (e.g., waitForStreamEvent or waitUntilIdle) that awaits the
mocked stream or the specific interaction to complete using a Completer or by
polling a condition (e.g., expecting mockStream.toList()/firstWhere, or checking
a flag set by the mock), then use that helper in place of Future.delayed in the
tests (references: Future.delayed occurrences in
spam_report_controller_test.dart) so assertions run only after the actual async
work completes and the tests stop relying on a magic timing number.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1af6eba5-3206-4c62-ae98-d8591b66f90a

📥 Commits

Reviewing files that changed from the base of the PR and between cb20a81 and 7d97bd1.

📒 Files selected for processing (3)
  • lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart
  • test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart
  • test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart (1)

118-171: Consider adding clock-skew tests for completeness.

The interactor has logic for handling future timestamps (negative elapsed time):

if (elapsed.isNegative) {
  if (elapsed.abs() < const Duration(days: 1)) return false;
  return true;
}

This handles cases where device clock was adjusted forward then back. No tests currently cover these paths. Consider adding:

  • Future timestamp < 1 day → should not show banner (cooldown active).
  • Future timestamp >= 1 day → should show banner (likely clock reset).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart`
around lines 118 - 171, Add two tests in
get_spam_mailbox_cached_interactor_test.dart mirroring the existing "cooldown
expired" group that cover future timestamps: call
spamReportRepository.getLastTimeDismissedSpamReportedMilliseconds() with
msAgo(-12) (future < 1 day) and assert interactor.execute(accountId, userName)
emits loading then leftWithException<NoUnreadSpamEmailsException>() (cooldown
active), and call it with msAgo(-25) (future >= 1 day) and assert it emits
loading then Right(GetSpamMailboxCachedSuccess(spamMailbox)) (cooldown expired).
Use the same helper msAgo, the same spamMailbox setups, and the same
expectations structure so tests reference interactor.execute,
spamReportRepository.getLastTimeDismissedSpamReportedMilliseconds, and
GetSpamMailboxCachedSuccess/NoUnreadSpamEmailsException.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart`:
- Around line 118-171: Add two tests in
get_spam_mailbox_cached_interactor_test.dart mirroring the existing "cooldown
expired" group that cover future timestamps: call
spamReportRepository.getLastTimeDismissedSpamReportedMilliseconds() with
msAgo(-12) (future < 1 day) and assert interactor.execute(accountId, userName)
emits loading then leftWithException<NoUnreadSpamEmailsException>() (cooldown
active), and call it with msAgo(-25) (future >= 1 day) and assert it emits
loading then Right(GetSpamMailboxCachedSuccess(spamMailbox)) (cooldown expired).
Use the same helper msAgo, the same spamMailbox setups, and the same
expectations structure so tests reference interactor.execute,
spamReportRepository.getLastTimeDismissedSpamReportedMilliseconds, and
GetSpamMailboxCachedSuccess/NoUnreadSpamEmailsException.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32344017-c052-424e-a98b-89e7973d0233

📥 Commits

Reviewing files that changed from the base of the PR and between 7d97bd1 and 9c513a5.

📒 Files selected for processing (2)
  • test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart
  • test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart
✅ Files skipped from review due to trivial changes (1)
  • test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart

@dab246
Copy link
Copy Markdown
Member Author

dab246 commented Mar 27, 2026

🧹 Nitpick comments (1)

test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart (1)> 118-171: Consider adding clock-skew tests for completeness.

Fixed

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