Skip to content

feat: detect admin/dispute DMs in background notification pipeline#498

Draft
AndreaDiazCorreia wants to merge 3 commits intomainfrom
feat/admin-chat-background-notifications
Draft

feat: detect admin/dispute DMs in background notification pipeline#498
AndreaDiazCorreia wants to merge 3 commits intomainfrom
feat/admin-chat-background-notifications

Conversation

@AndreaDiazCorreia
Copy link
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Feb 24, 2026

  • Detect admin/dispute DM messages ({"dm": ...} format) in the background notification service
    and display notifications instead of silently failing
  • Extract shared NostrUtils.isDmPayload() utility to replace duplicated DM detection logic
    across 3 files
  • Add explicit sendDm and cooperativeCancelAccepted cases to NotificationDataExtractor so
    they generate proper non-temporary notifications

Context

Admin/dispute chat messages arrive at tradeKey.public and get decrypted successfully by
unWrap(), but MostroMessage.fromJson() fails because the inner format is [{"dm": {...}}]
instead of the standard Mostro message format. This caused background notifications for admin DMs
to be silently dropped.

This PR (Phase 1 of the chat notifications plan) fixes this by
detecting the DM format before JSON parsing and constructing a synthetic MostroMessage with
Action.sendDm that flows through the existing notification pipeline.

Changes

File Change
lib/shared/utils/nostr_utils.dart New isDmPayload() static method
lib/features/notifications/services/background_notification_service.dart DM detection before
MostroMessage.fromJson
lib/features/notifications/utils/notification_data_extractor.dart sendDm +
cooperativeCancelAccepted cases
lib/services/mostro_service.dart Refactored to use shared isDmPayload()
lib/features/disputes/notifiers/dispute_chat_notifier.dart Refactored to use shared
isDmPayload()

How to test

  1. Unit tests: flutter test test/features/notifications/services/background_notification_dm_detection_test.dart
  2. Manual — background notification flow:
    • Open a dispute on an active order
    • Have an admin send a DM through the Mostro CLI (/dm <order_id> <message>)
    • Put the app in background before the message arrives
    • Verify a local notification appears (generic "new message" text, no content exposed)
  3. Manual — foreground still works:
    • Keep the app open on the dispute chat screen
    • Have admin send a DM — message should appear in the chat as before
    • Verify no duplicate notification is shown
  4. Regression — standard Mostro events:
    • Create/take orders and verify background notifications still work for order updates,
      cancellations, timeouts

Summary by CodeRabbit

  • New Features

    • Improved background notification system to better detect and process direct messages and dispute chat messages through centralized detection logic.
    • Added support for recognizing sendDm and cooperativeCancelAccepted notification actions with proper payload extraction.
  • Tests

    • Added comprehensive test coverage for direct message notification detection, including payload validation, message construction, and notification data extraction.

Add detection for admin/dispute DM messages in background service by checking
for {"dm": {...}} format before standard MostroMessage parsing. Return synthetic
MostroMessage with sendDm action to trigger notification flow.

Part of chat notifications implementation (Phase 1: Admin DM background notifications).
…load

Extract duplicate DM format detection logic (`item is Map && item.containsKey('dm')`)
into shared `NostrUtils.isDmPayload()` method. Replace inline checks in
DisputeChatNotifier, BackgroundNotificationService, and MostroService with calls
to the new utility.

Update tests to exercise NostrUtils.isDmPayload directly instead of testing
detection logic in isolation. Add edge case coverage for non-Map types.
…xtractor

Add explicit case handlers for Action.sendDm and Action.cooperativeCancelAccepted
in NotificationDataExtractor to ensure they generate non-temporary notifications.
Both actions require no payload extraction (empty values map).

Expand test coverage to validate three layers of the admin/dispute DM notification
pipeline: NostrUtils.isDmPayload detection, MostroMessage construction with sendDm
action, and NotificationDataExtractor
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Important

Review skipped

Draft detected.

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.

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:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/admin-chat-background-notifications

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
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 (1)
test/features/notifications/services/background_notification_dm_detection_test.dart (1)

1-134: Consider adding NotificationMessageMapper layer tests to validate the full background notification pipeline.

The three pipeline layers tested here (detection → construction → extraction) are covered, but the fourth layer — NotificationMessageMapper.getLocalizedTitleWithInstance / getLocalizedMessageWithInstance — executes in _getLocalizedNotificationText after extraction and before the notification is displayed. While the mapper already has entries for Action.sendDm and Action.cooperativeCancelAccepted, adding a test that calls the mapper directly with a concrete S instance (e.g. SEn()) for these actions would confirm that localization keys resolve correctly and close this coverage gap.

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

In
`@test/features/notifications/services/background_notification_dm_detection_test.dart`
around lines 1 - 134, Add tests that exercise
NotificationMessageMapper.getLocalizedTitleWithInstance and
getLocalizedMessageWithInstance for the actions covered (Action.sendDm and
Action.cooperativeCancelAccepted) using a concrete localization instance (e.g.,
SEn) to ensure localization keys resolve; create test cases that build a
NotificationData (or MostroMessage -> extract via NotificationDataExtractor) and
then call NotificationMessageMapper.getLocalizedTitleWithInstance(SEn()) and
getLocalizedMessageWithInstance(SEn()) asserting non-empty/expected strings,
mirroring existing test patterns in this file and referencing
NotificationMessageMapper, getLocalizedTitleWithInstance,
getLocalizedMessageWithInstance, and SEn to locate where to add the new tests.
🤖 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/notifications/services/background_notification_service.dart`:
- Around line 175-183: The code constructs a MostroMessage using
matchingSession.orderId which can be null and results in a null notification
payload; add an explicit guard: check matchingSession.orderId before building
the MostroMessage in background_notification_service.dart (the block that
returns MostroMessage for NostrUtils.isDmPayload firstItem), and if orderId is
null either early-return/skip creating the DM notification or set a non-null
fallback (e.g., "unlinked-session") for MostroMessage.id so
flutterLocalNotificationsPlugin.show never receives a null payload; ensure the
chosen approach is clearly documented in the conditional.

---

Nitpick comments:
In
`@test/features/notifications/services/background_notification_dm_detection_test.dart`:
- Around line 1-134: Add tests that exercise
NotificationMessageMapper.getLocalizedTitleWithInstance and
getLocalizedMessageWithInstance for the actions covered (Action.sendDm and
Action.cooperativeCancelAccepted) using a concrete localization instance (e.g.,
SEn) to ensure localization keys resolve; create test cases that build a
NotificationData (or MostroMessage -> extract via NotificationDataExtractor) and
then call NotificationMessageMapper.getLocalizedTitleWithInstance(SEn()) and
getLocalizedMessageWithInstance(SEn()) asserting non-empty/expected strings,
mirroring existing test patterns in this file and referencing
NotificationMessageMapper, getLocalizedTitleWithInstance,
getLocalizedMessageWithInstance, and SEn to locate where to add the new tests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 149010c and a072019.

📒 Files selected for processing (6)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
  • lib/features/notifications/services/background_notification_service.dart
  • lib/features/notifications/utils/notification_data_extractor.dart
  • lib/services/mostro_service.dart
  • lib/shared/utils/nostr_utils.dart
  • test/features/notifications/services/background_notification_dm_detection_test.dart

Comment on lines +175 to +183
// Detect admin/dispute DM format: [{"dm": {"action": "send-dm", ...}}]
final firstItem = result[0];
if (NostrUtils.isDmPayload(firstItem)) {
return MostroMessage(
action: mostro_action.Action.sendDm,
id: matchingSession.orderId,
timestamp: event.createdAt?.millisecondsSinceEpoch,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

matchingSession.orderId null produces a null notification payload.

matchingSession.orderId is String?. If it is null (unlinked session), MostroMessage.id is null, so payload: mostroMessage.id passed to flutterLocalNotificationsPlugin.show is also null. Tapping the notification navigates to /notifications rather than /trade_detail/$orderId.

In practice dispute sessions are always linked to an order, so this is low-risk, but an explicit guard here makes the intent clear.

🛡️ Proposed defensive guard
     if (NostrUtils.isDmPayload(firstItem)) {
+      if (matchingSession.orderId == null) {
+        logger.w('DM payload detected but session has no orderId — notification will lack deep-link');
+      }
       return MostroMessage(
         action: mostro_action.Action.sendDm,
         id: matchingSession.orderId,
         timestamp: event.createdAt?.millisecondsSinceEpoch,
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/notifications/services/background_notification_service.dart`
around lines 175 - 183, The code constructs a MostroMessage using
matchingSession.orderId which can be null and results in a null notification
payload; add an explicit guard: check matchingSession.orderId before building
the MostroMessage in background_notification_service.dart (the block that
returns MostroMessage for NostrUtils.isDmPayload firstItem), and if orderId is
null either early-return/skip creating the DM notification or set a non-null
fallback (e.g., "unlinked-session") for MostroMessage.id so
flutterLocalNotificationsPlugin.show never receives a null payload; ensure the
chosen approach is clearly documented in the conditional.

Copy link
Contributor

@mostronatorcoder mostronatorcoder 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 looks solid — clean extraction of isDmPayload(), good test coverage, and the synthetic MostroMessage approach for DM detection is the right call.

However, this PR has merge conflicts with the base branch (mergeable_state: dirty). The conflict likely comes from recent changes in dispute_chat_notifier.dart (PR #501 refactored the same file significantly).

Please rebase/merge from main to resolve conflicts, then this should be good to go.

Code review notes (all positive):

  • NostrUtils.isDmPayload() is a clean DRY improvement over the 3 inline checks
  • sendDm and cooperativeCancelAccepted cases in NotificationDataExtractor correctly marked as non-temporary
  • Tests cover detection, construction, and extraction layers thoroughly
  • The early return before MostroMessage.fromJson() in background service is the right place to intercept

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.

1 participant