feat: detect admin/dispute DMs in background notification pipeline#498
feat: detect admin/dispute DMs in background notification pipeline#498AndreaDiazCorreia wants to merge 3 commits intomainfrom
Conversation
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
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/features/notifications/services/background_notification_dm_detection_test.dart (1)
1-134: Consider addingNotificationMessageMapperlayer 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_getLocalizedNotificationTextafter extraction and before the notification is displayed. While the mapper already has entries forAction.sendDmandAction.cooperativeCancelAccepted, adding a test that calls the mapper directly with a concreteSinstance (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
📒 Files selected for processing (6)
lib/features/disputes/notifiers/dispute_chat_notifier.dartlib/features/notifications/services/background_notification_service.dartlib/features/notifications/utils/notification_data_extractor.dartlib/services/mostro_service.dartlib/shared/utils/nostr_utils.darttest/features/notifications/services/background_notification_dm_detection_test.dart
| // 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 checkssendDmandcooperativeCancelAcceptedcases inNotificationDataExtractorcorrectly 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
{"dm": ...}format) in the background notification serviceand display notifications instead of silently failing
NostrUtils.isDmPayload()utility to replace duplicated DM detection logicacross 3 files
sendDmandcooperativeCancelAcceptedcases toNotificationDataExtractorsothey generate proper non-temporary notifications
Context
Admin/dispute chat messages arrive at
tradeKey.publicand get decrypted successfully byunWrap(), butMostroMessage.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
MostroMessagewithAction.sendDmthat flows through the existing notification pipeline.Changes
lib/shared/utils/nostr_utils.dartisDmPayload()static methodlib/features/notifications/services/background_notification_service.dartMostroMessage.fromJsonlib/features/notifications/utils/notification_data_extractor.dartsendDm+cooperativeCancelAcceptedcaseslib/services/mostro_service.dartisDmPayload()lib/features/disputes/notifiers/dispute_chat_notifier.dartisDmPayload()How to test
flutter test test/features/notifications/services/background_notification_dm_detection_test.dart/dm <order_id> <message>)cancellations, timeouts
Summary by CodeRabbit
New Features
Tests