Skip to content

Add P2P chat background notifications (Phase 2)#529

Open
AndreaDiazCorreia wants to merge 10 commits intomainfrom
feat/p2p-chat-background-notifications
Open

Add P2P chat background notifications (Phase 2)#529
AndreaDiazCorreia wants to merge 10 commits intomainfrom
feat/p2p-chat-background-notifications

Conversation

@AndreaDiazCorreia
Copy link
Copy Markdown
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Mar 13, 2026

Summary

  • Extract tradeKey event handling into _handleTradeKeyEvent() for clarity
  • Add sharedKey matching in _decryptAndProcessEvent() so P2P chat messages arriving in background are detected
  • Add _handleP2PChatEvent() that decrypts via p2pUnwrap(sharedKey) and triggers a generic "New message" notification
  • Reuses existing localization keys and notification pipeline from Phase 1 (admin DM notifications)

How it works

Previously, _decryptAndProcessEvent() only matched events by tradeKey.public, so P2P chat messages (which arrive at sharedKey.public) were silently dropped. Now, when no tradeKey match is found, sessions are checked by sharedKey.public. Matched events are decrypted with p2pUnwrap and produce the same generic chat notification used for admin DMs.

Test plan

  • Open an active trade with P2P chat between two users
  • Send the app to background (do NOT kill it)
  • From the counterparty, send a P2P chat message
  • Verify a local notification appears with "New message" title and generic body
  • Tap the notification and verify it navigates to the trade detail screen
  • Verify Mostro protocol notifications (order updates, cancellations, timeouts) still work as before
  • Verify admin/dispute chat background notifications still work as before
  • Run flutter analyze — zero issues expected

Summary by CodeRabbit

  • New Features

    • Background notifications now support peer-to-peer chat messages and route to trade detail when applicable.
  • Improvements

    • Notification processing tries multiple decryption paths for more reliable matching.
    • Distinguishes chat-message vs admin DM for correct title/message mapping.
    • Suppresses duplicate/self-notifications and logs missing order IDs safely.
    • Foreground app state prevents background notification retries while active.
  • Tests

    • Added coverage for P2P chat notification routing and mapping.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 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

Reworks background notification decryption: early-return when recipient is null, route events by matching recipient against session tradeKey.public or sharedKey.public, and extract chat DM vs P2P chat via new _handleTradeKeyEvent and _handleP2PChatEvent flows (new Action.chatMessage used for P2P chat).

Changes

Cohort / File(s) Summary
Background Notification Decryption
lib/features/notifications/services/background_notification_service.dart
Refactored _decryptAndProcessEvent to early-return on null recipient; route to _handleTradeKeyEvent when recipient == session.tradeKey.public; route to _handleP2PChatEvent when recipient == session.sharedKey.public; moved tradeKey JSON decode/DM detection and added p2p unwrap/error handling and logging.
Action & Notification Mapping
lib/data/models/enums/action.dart, lib/data/models/notification.dart
Added Action.chatMessage('chat-message') and map it to NotificationType.message in notification type resolution.
Notification Extraction & Messaging
lib/features/notifications/utils/notification_data_extractor.dart, lib/features/notifications/utils/notification_message_mapper.dart, lib/features/notifications/widgets/notification_item.dart
Treat chatMessage as non-temporary in extractor; map title/message keys to notification_new_message_*; route notification tap for chatMessage to /trade_detail/{orderId}.
Background Foreground Flag
lib/background/background.dart
Initialize isAppForeground to false and short-circuit background notification retry when app is foreground.
Tests
test/features/notifications/services/background_notification_dm_detection_test.dart
Expanded tests to cover P2P chat semantics: Action.chatMessage identity/serialization, MostroMessage construction for P2P unwrap, extractor/mapping assertions, and route resolution for P2P chat.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Event as EventSource
participant Service as BackgroundNotificationService
participant Sessions as SessionStore
participant Crypto as CryptoLayer
participant Builder as MostroMessageBuilder
Event->>Service: deliver event (recipient, payload)
Service->>Sessions: find session where recipient == session.tradeKey.public
alt tradeKey match
Service->>Crypto: unwrap with tradeKey
Crypto-->>Service: decrypted JSON / content
Service->>Builder: parse JSON → MostroMessage or admin DM
Service-->>Builder: process/emit notification
else no tradeKey match
Service->>Sessions: find session where recipient == session.sharedKey.public
alt sharedKey match
Service->>Crypto: p2pUnwrap with sharedKey
Crypto-->>Service: decrypted content
Service->>Builder: build MostroMessage(Action.chatMessage)
Service-->>Builder: process/emit notification
else
Service-->>Event: return null (no matching session)
end
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐰
I hopped through trade-keys and shared-key lanes,
Unwrapped soft messages, chased tiny trains.
A chat found its order, a bell gently rang,
I nibbled a carrot and gave it a twang.
Hooray for new hops — code stories to sang!

🚥 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 'Add P2P chat background notifications (Phase 2)' directly and specifically describes the main change—adding P2P chat background notification support as part of a phased feature rollout.
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 feat/p2p-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.

@AndreaDiazCorreia AndreaDiazCorreia force-pushed the feat/p2p-chat-background-notifications branch from c0bc2b6 to ac3cfbe Compare April 3, 2026 01:08
@AndreaDiazCorreia AndreaDiazCorreia marked this pull request as ready for review April 3, 2026 01:14
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

🤖 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 293-310: The _handleP2PChatEvent function currently builds a
MostroMessage using session.orderId without checking for null; update
_handleP2PChatEvent to validate session.orderId != null (mirroring the pattern
in _processAdminDm and _handleTradeKeyEvent), and if it's null log a warning
(use the same logger) and return null instead of creating the sendDm message to
avoid creating notifications before orderId is populated.
- Around line 221-225: Add a null-check for session.orderId in
_handleP2PChatEvent before constructing the MostroMessage: if session.orderId is
null, log a warning (include event.recipient or other context) and return null
to avoid creating a message with id: null; mirror the existing checks in
_processAdminDm and _handleTradeKeyEvent so the function only builds and returns
a MostroMessage when session.orderId is non-null.
🪄 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: 6a7334b9-d14c-4d35-a1bb-86a7f62e1378

📥 Commits

Reviewing files that changed from the base of the PR and between fca09d4 and ac3cfbe.

📒 Files selected for processing (1)
  • lib/features/notifications/services/background_notification_service.dart

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.

🧹 Nitpick comments (1)
lib/features/notifications/services/background_notification_service.dart (1)

261-291: Add try-catch for consistency with sibling handlers.

Unlike _processAdminDm (lines 255-258) and _handleP2PChatEvent (lines 311-314), this function lacks its own try-catch block. While the outer _decryptAndProcessEvent catch block will handle exceptions, the inconsistent pattern makes debugging harder—exceptions from this function will log as generic "Decrypt error" rather than identifying the specific handler that failed.

Additionally, result[0] passed to MostroMessage.fromJson on line 287 is typed as dynamic and may not be a Map<String, dynamic> for malformed payloads.

♻️ Proposed fix to add error handling
 /// Handle events matched by tradeKey (Mostro protocol + admin/dispute DMs)
 Future<MostroMessage?> _handleTradeKeyEvent(NostrEvent event, Session session) async {
+  try {
     final decryptedEvent = await event.unWrap(session.tradeKey.private);
     if (decryptedEvent.content == null) {
       return null;
     }
 
     final result = jsonDecode(decryptedEvent.content!);
     if (result is! List || result.isEmpty) {
       return null;
     }
 
     // Detect admin/dispute DM format that arrived via tradeKey
     final firstItem = result[0];
     if (NostrUtils.isDmPayload(firstItem)) {
       if (session.orderId == null) {
         logger.w('DM received but session has no orderId (recipient: ${event.recipient}), skipping notification');
         return null;
       }
       return MostroMessage(
         action: mostro_action.Action.sendDm,
         id: session.orderId,
         timestamp: event.createdAt?.millisecondsSinceEpoch,
       );
     }
 
+    if (result[0] is! Map<String, dynamic>) {
+      logger.w('Unexpected payload format in tradeKey event');
+      return null;
+    }
     final mostroMessage = MostroMessage.fromJson(result[0]);
     mostroMessage.timestamp = event.createdAt?.millisecondsSinceEpoch;
 
     return mostroMessage;
+  } catch (e) {
+    logger.e('Trade key decrypt error: $e');
+    return null;
+  }
 }
🤖 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 261 - 291, _handleTradeKeyEvent lacks its own try/catch and
performs an unsafe cast when calling MostroMessage.fromJson(result[0]); wrap the
body of _handleTradeKeyEvent in a try/catch, catch (e, st) and log a
handler-specific error via logger.e (include the exception and stack) and return
null on error; before calling MostroMessage.fromJson, validate that result[0] is
a Map<String, dynamic> (or safely convert) and if not, log a warning and return
null; keep existing behavior for DM detection using NostrUtils.isDmPayload and
preserve timestamp assignment on the MostroMessage instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/features/notifications/services/background_notification_service.dart`:
- Around line 261-291: _handleTradeKeyEvent lacks its own try/catch and performs
an unsafe cast when calling MostroMessage.fromJson(result[0]); wrap the body of
_handleTradeKeyEvent in a try/catch, catch (e, st) and log a handler-specific
error via logger.e (include the exception and stack) and return null on error;
before calling MostroMessage.fromJson, validate that result[0] is a Map<String,
dynamic> (or safely convert) and if not, log a warning and return null; keep
existing behavior for DM detection using NostrUtils.isDmPayload and preserve
timestamp assignment on the MostroMessage instance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0a6ef65-b28f-4588-9ca7-3bc4498ce338

📥 Commits

Reviewing files that changed from the base of the PR and between ac3cfbe and 7bc248a.

📒 Files selected for processing (1)
  • lib/features/notifications/services/background_notification_service.dart

@mostronatorcoder
Copy link
Copy Markdown
Contributor

Thanks for the update — I reviewed the full diff in background_notification_service.dart.

I found one blocking behavior regression:

  • _handleP2PChatEvent() returns a synthetic MostroMessage(action: Action.sendDm, ...).
  • In the current notification pipeline, Action.sendDm maps to admin/dispute copy (notification_admin_message_*) and is serialized with payload type admin_dm.
  • Result: regular P2P chat notifications can be presented as "Message from admin", which is incorrect UX and can mislead users.

Why this matters

This PR introduces P2P chat background notifications by reusing the same sendDm action currently used for dispute/admin DMs. Without a discriminator in mapping/rendering, both flows collapse into the admin wording/path.

Suggested fix

Use separate handling for P2P chat vs admin dispute DMs (either a distinct action/key path, or an explicit metadata flag that reaches NotificationMessageMapper and payload serialization), so:

  • P2P chat → generic "new message" copy
  • admin/dispute DM → "message from admin" copy

Also recommended before merge

Add focused tests for this branch:

  1. sharedKey-matched P2P event produces a notification
  2. wording is generic (not admin)
  3. payload routing remains correct

Once that separation is in place, the structural refactor here looks good.

Introduce Action.chatMessage to distinguish P2P chat notifications from admin/dispute DMs (Action.sendDm). Update background notification pipeline to use chatMessage for P2P events, add notification string mappings, and route to trade detail screen. Expand test coverage for action separation and notification data extraction.
@mostronatorcoder
Copy link
Copy Markdown
Contributor

Great progress on this PR — the chatMessage split fixed the admin-vs-P2P wording issue. I found one remaining gap before approval:

In lib/data/models/notification.dart, NotificationModel.getNotificationTypeFromAction() does not include Action.chatMessage.

Right now chatMessage falls through to default -> NotificationType.system, but it should be categorized as NotificationType.message (same category as Action.sendDm).

Why this matters

Even if build/tests pass, this can cause misclassification in notification UI behavior (filtering/grouping/icon logic tied to type).

Suggested fix

Add case Action.chatMessage: alongside case Action.sendDm: and return NotificationType.message.

After this is addressed, this looks ready to approve from my side.

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

254-309: Exercise the new sharedKey.public decrypt path directly.

These assertions start after a MostroMessage already exists, so a regression in background_notification_service.dart's new shared-key match or p2pUnwrap(session.sharedKey!) branch would still pass. Please add one test that feeds a matching shared-key event through the background pipeline and asserts it emits Action.chatMessage.

🤖 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 254 - 309, Add a test that constructs a MostroMessage whose payload
is encrypted for the test session's sharedKey.public and then runs it through
the background pipeline to ensure the p2pUnwrap(session.sharedKey!) branch is
exercised: create or reuse a test Session with a matching sharedKey.public,
build an encrypted MostroMessage that would decrypt to Action.chatMessage, call
the background pipeline entrypoint
(BackgroundNotificationService.processIncoming / handleMostroMessage or
equivalent used in your codebase) and assert the emitted/produced
NotificationData has action == Action.chatMessage; this ensures the new
shared-key decrypt path (p2pUnwrap) is exercised rather than bypassed by
directly calling NotificationDataExtractor.extractFromMostroMessage.
🤖 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/widgets/notification_item.dart`:
- Around line 101-103: The two actions are currently grouped; split
mostro_action.Action.sendDm and mostro_action.Action.chatMessage into separate
cases in notification_item.dart so chatMessage continues to navigate to
context.push('/trade_detail/${notification.orderId}') while sendDm checks
notification.disputeId and, if present, navigates to
context.push('/dispute_details/${notification.disputeId}'); if disputeId is
null/absent, fall back to the previous behavior (e.g., push the trade detail or
DM tray as appropriate) and ensure notification.disputeId is persisted when
creating sendDm notifications.

---

Nitpick comments:
In
`@test/features/notifications/services/background_notification_dm_detection_test.dart`:
- Around line 254-309: Add a test that constructs a MostroMessage whose payload
is encrypted for the test session's sharedKey.public and then runs it through
the background pipeline to ensure the p2pUnwrap(session.sharedKey!) branch is
exercised: create or reuse a test Session with a matching sharedKey.public,
build an encrypted MostroMessage that would decrypt to Action.chatMessage, call
the background pipeline entrypoint
(BackgroundNotificationService.processIncoming / handleMostroMessage or
equivalent used in your codebase) and assert the emitted/produced
NotificationData has action == Action.chatMessage; this ensures the new
shared-key decrypt path (p2pUnwrap) is exercised rather than bypassed by
directly calling NotificationDataExtractor.extractFromMostroMessage.
🪄 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: d59d424b-cb7d-4f8c-b995-65bf6064da3c

📥 Commits

Reviewing files that changed from the base of the PR and between 7bc248a and 3b66675.

📒 Files selected for processing (7)
  • lib/data/models/enums/action.dart
  • lib/data/models/notification.dart
  • lib/features/notifications/services/background_notification_service.dart
  • lib/features/notifications/utils/notification_data_extractor.dart
  • lib/features/notifications/utils/notification_message_mapper.dart
  • lib/features/notifications/widgets/notification_item.dart
  • test/features/notifications/services/background_notification_dm_detection_test.dart
✅ Files skipped from review due to trivial changes (2)
  • lib/data/models/enums/action.dart
  • lib/features/notifications/utils/notification_data_extractor.dart

mostronatorcoder[bot]
mostronatorcoder bot previously approved these changes Apr 3, 2026
Copy link
Copy Markdown
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.

Thanks for the follow-up fixes. The chatMessage action is now consistently separated from admin DM handling and correctly mapped to NotificationType.message. LGTM.

Copy link
Copy Markdown
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

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

When my order went to Active status and i stared to receive DMs from my counterpert, I wasn't getting notifications for incoming messages until I closed and reopened the app.

Then I started getting notifications for messages from the other person, but I was also getting a notification for every message I sent.

Then I closed the app again, and when I reopened it, I didn't get any message notifications from either the other person or me.

I closed and reopened it again, and I started getting notifications for incoming messages again, but also for all the messages I sent.

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

261-291: Consider adding try-catch for consistent error handling.

Both _processAdminDm and _handleP2PChatEvent wrap their logic in local try-catch blocks with handler-specific error messages (e.g., 'Admin DM decrypt error', 'P2P chat decrypt error'). This handler relies on the parent catch, which logs a generic 'Decrypt error' message, making it harder to distinguish trade-key failures in logs.

♻️ Optional: Add local try-catch for consistent observability
 /// Handle events matched by tradeKey (Mostro protocol + admin/dispute DMs)
 Future<MostroMessage?> _handleTradeKeyEvent(NostrEvent event, Session session) async {
+  try {
     final decryptedEvent = await event.unWrap(session.tradeKey.private);
     if (decryptedEvent.content == null) {
       return null;
     }
 
     final result = jsonDecode(decryptedEvent.content!);
     if (result is! List || result.isEmpty) {
       return null;
     }
 
     // Detect admin/dispute DM format that arrived via tradeKey
     final firstItem = result[0];
     if (NostrUtils.isDmPayload(firstItem)) {
       if (session.orderId == null) {
         logger.w('DM received but session has no orderId (recipient: ${event.recipient}), skipping notification');
         return null;
       }
       return MostroMessage(
         action: mostro_action.Action.sendDm,
         id: session.orderId,
         timestamp: event.createdAt?.millisecondsSinceEpoch,
       );
     }
 
     final mostroMessage = MostroMessage.fromJson(result[0]);
     mostroMessage.timestamp = event.createdAt?.millisecondsSinceEpoch;
 
     return mostroMessage;
+  } catch (e) {
+    logger.e('Trade key decrypt error: $e');
+    return null;
+  }
 }
🤖 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 261 - 291, The _handleTradeKeyEvent function lacks a local
try-catch around the decryption/parsing steps (event.unWrap and jsonDecode) so
trade-key failures get logged only by the parent generic handler; wrap the
decryption and JSON parsing in a local try-catch, log a clear, specific message
like "Trade-key decrypt error" including the error and stack, and return null
(or otherwise short-circuit) on error—follow the pattern used in _processAdminDm
and _handleP2PChatEvent to improve observability for MostroMessage creation and
handling.
🤖 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 293-311: The P2P unwrap in _handleP2PChatEvent assumes
session.sharedKey is non-null; add a defensive guard that returns null if
session.sharedKey (or its public value if applicable) is null before calling
event.p2pUnwrap(session.sharedKey!) to avoid a potential crash, and similarly
ensure the earlier matching logic that compares s?.sharedKey?.public to
event.recipient never treats two nulls as a match by requiring a non-null
recipient and non-null sharedKey.public when matching.

---

Nitpick comments:
In `@lib/features/notifications/services/background_notification_service.dart`:
- Around line 261-291: The _handleTradeKeyEvent function lacks a local try-catch
around the decryption/parsing steps (event.unWrap and jsonDecode) so trade-key
failures get logged only by the parent generic handler; wrap the decryption and
JSON parsing in a local try-catch, log a clear, specific message like "Trade-key
decrypt error" including the error and stack, and return null (or otherwise
short-circuit) on error—follow the pattern used in _processAdminDm and
_handleP2PChatEvent to improve observability for MostroMessage creation and
handling.
🪄 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: 2424221a-52a8-43f8-821f-21806568cfd7

📥 Commits

Reviewing files that changed from the base of the PR and between 3b66675 and 0e0c5e8.

📒 Files selected for processing (2)
  • lib/background/background.dart
  • lib/features/notifications/services/background_notification_service.dart

@AndreaDiazCorreia AndreaDiazCorreia requested a review from Catrya April 7, 2026 05:38
Copy link
Copy Markdown
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.

Re-reviewed after the latest updates. The foreground suppression, null-safe session matching, own-message filtering, and dynamic background chat subscription flow all look good. The action/type mapping remains consistent (chatMessage vs sendDm). LGTM.

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