Skip to content

feat: wire P2P chat bridge (send, receive, chat rooms list) - #95

Merged
grunch merged 4 commits into
mainfrom
feat/p2p-chat-bridge
Apr 5, 2026
Merged

feat: wire P2P chat bridge (send, receive, chat rooms list)#95
grunch merged 4 commits into
mainfrom
feat/p2p-chat-bridge

Conversation

@mostronatorcoder

@mostronatorcoder mostronatorcoder Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Connects the P2P trade chat end-to-end — from NIP-59 gift-wrap publishing to real-time incoming message delivery and chat room population from trades.

Previously the chat screen had optimistic local state only; the Rust bridge had all the building blocks (send_message, get_messages, mark_as_read, on_new_message) but no wiring connected them to the live Nostr layer.

Root cause

Three gaps prevented chat from working:

  1. take_order did not create a session or subscribe gift-wraps — the session never existed, so send_message always failed with "session not found".
  2. process_gift_wrap_rumor did not handle BuyerTookOrder / HoldInvoicePaymentAccepted — these are the protocol actions that carry the peer's trade pubkey. Without them, session.peer_pubkey was always None and send_message returned an error.
  3. No subscription for incoming P2P messages — kind-1059 gift-wraps addressed to the ECDH shared-key pubkey were never subscribed, so incoming messages were never received.

Additionally, send_message was wrapping to the raw peer pubkey instead of the ECDH shared-key pubkey, which violates the Mostro P2P chat protocol spec.

Changes

Rust rust/src/api/orders.rs

  • take_order: after a successful publish, now calls subscribe_gift_wraps + session_manager().create_session() so the session exists before the first daemon response arrives.
  • process_gift_wrap_rumor: new arms for Action::BuyerTookOrder and Action::HoldInvoicePaymentAccepted. Both carry the counterpart's trade pubkey in SmallOrder.{buyer,seller}_trade_pubkey. On receipt, calls on_peer_pubkey_received.
  • New on_peer_pubkey_received: derives the NIP-04 ECDH shared secret from (our_trade_key, peer_trade_pubkey), computes the shared-key pubkey (treating the 32-byte secret as a private scalar), stores both in the session, and spawns subscribe_incoming_chat.
  • Tests: session idempotency, initial-state assertions, graceful no-op on missing session.

Rust rust/src/api/messages.rs

  • New subscribe_incoming_chat (pub(crate)): subscribes to kind-1059 gift-wrap events with p tag == shared-key pubkey. Decrypts each rumor, ignores own echoes (sender == trade key), constructs a ChatMessage, appends to message_store(), which fires the on_new_message stream. Exits after 30 min of inactivity.
  • send_message: now wraps to shared_pubkey (ECDH shared-key pubkey) instead of peer_pubkey directly, per the protocol spec.
  • send_file: same fix.
  • Tests: deduplication contract, on_new_message trade-id isolation.

Dart lib/features/chat/providers/chat_providers.dart

  • chatRoomsFromTradesProvider: new FutureProvider that converts rawTradesProvider entries with a non-empty counterpartyPubkey into ChatRoomStates — resolves NymIdentity, loads last-message preview and unread count.
  • incomingMessageProvider: StreamProvider.family wrapping messages_api.onNewMessage. Consumed by ChatRoomScreen via ref.listen.
  • messageHistoryProvider: FutureProvider.family over messages_api.getMessages.

Dart lib/features/chat/screens/chat_room_screen.dart

  • Replaces hardcoded optimistic list with bridge-backed state:
    • _loadHistory seeds from messages_api.getMessages on initState.
    • _markRead calls messages_api.markAsRead and resets unread badge.
    • _onSend calls messages_api.sendMessage, appends returned message.
    • ref.listen(incomingMessageProvider) appends live incoming messages.
  • Empty-state placeholder and loading indicator while history loads.
  • Adapts FRB rust_types.ChatMessage to Dart ChatMessage for MessageBubble.

Dart lib/features/chat/screens/chat_rooms_screen.dart

  • ChatRoomsScreen -> ConsumerStatefulWidget: calls _syncRoomsFromTrades on init, populating chatRoomsNotifierProvider from chatRoomsFromTradesProvider.

Tests

cargo test --lib passes 77/77 (0 failures, 6 ignored integration tests).

New test coverage:

  • orders::tests::create_session_is_idempotent
  • orders::tests::new_session_has_no_peer_keys
  • orders::tests::peer_pubkey_with_no_session_does_not_panic
  • api::messages::tests::add_duplicate_message_is_ignored_in_count
  • api::messages::tests::on_new_message_stream_fires_for_correct_trade

Protocol compliance

This implementation follows the Mostro P2P chat spec:

  • The p-tag of all outbound gift-wraps is the ECDH shared-key pubkey, not the peer's trade pubkey.
  • Incoming subscriptions filter by the same shared-key pubkey.
  • Own echoes (sender pubkey == our trade pubkey) are silently ignored.

Note on FRB bindings

subscribe_incoming_chat is pub(crate) — it is not exposed to Dart via FRB. The Dart side drives the UI via on_new_message (already in the generated bindings). No binding regeneration is required for the Dart changes to work.

What is still deferred

  • Sembast persistence of read status (currently in-memory).
  • File attachment UI in ChatRoomScreen (Rust send_file is wired; tap handler is a stub).
  • Polling to reactive stream migration for trade status (tracked separately).

Closes the Bridge no cableado and Siempre vacio items in the v2 feature matrix.

Summary by CodeRabbit

  • New Features

    • Chat rooms auto-populate from your active trades with peer identity and avatar fallbacks.
    • Real-time incoming messages and live updates to room previews and unread counts.
    • Message history loads on open; new messages stream in afterward.
  • Improvements

    • Safer, more reliable encrypted messaging and file wraps.
    • Sending pipeline with duplicate-send protection, error feedback, and read-marking.

## Summary

Connects the P2P trade chat end-to-end — from NIP-59 gift-wrap publishing
to real-time incoming message delivery and chat room population from trades.

## Root cause analysis

The chat screen had optimistic local state only; the Rust bridge had all the
building blocks (send/receive/mark-as-read) but was missing three wiring points:

1. `take_order` did not create a session or subscribe gift-wraps.
2. `process_gift_wrap_rumor` did not handle `BuyerTookOrder` /
   `HoldInvoicePaymentAccepted` — the actions that carry the peer's trade
   pubkey.
3. No background task subscribed to NIP-59 kind-1059 events addressed to the
   shared-key pubkey for incoming P2P messages.

## Changes

### Rust (`rust/src/api/orders.rs`)
- `take_order`: after successful publish, now calls `subscribe_gift_wraps` +
  `session_manager().create_session()` so the session exists before the first
  daemon response arrives.
- `process_gift_wrap_rumor`: handles `Action::BuyerTookOrder` and
  `Action::HoldInvoicePaymentAccepted`. Both carry the counterpart's trade
  pubkey in the SmallOrder payload. On receipt, calls `on_peer_pubkey_received`.
- New `on_peer_pubkey_received(order_id, trade_pubkey_hex, peer_pubkey_hex)`:
  derives the NIP-04 ECDH shared secret, computes the shared-key pubkey
  (shared_secret used as a private scalar), stores both in the session, and
  spawns `subscribe_incoming_chat`.
- Tests: session idempotency, initial state, graceful no-op on missing session.

### Rust (`rust/src/api/messages.rs`)
- `subscribe_incoming_chat`: new `pub(crate)` function. Subscribes to
  kind-1059 gift-wrap events with p-tag == shared-key pubkey. Decrypts each
  rumor, ignores own echoes (sender == trade key), constructs a `ChatMessage`,
  appends it to `message_store()`, which fires the `on_new_message` stream.
  Exits after 30 min of inactivity.
- `send_message`: now wraps to `shared_pubkey` (ECDH shared-key pubkey) instead
  of `peer_pubkey` directly, per the Mostro P2P chat protocol spec.
- `send_file`: same fix — wraps to shared-key pubkey.
- Tests: deduplication contract, `on_new_message` trade-id filtering.

### Dart (`lib/features/chat/providers/chat_providers.dart`)
- `chatRoomsFromTradesProvider`: new `FutureProvider` that converts
  `rawTradesProvider` entries with a known `counterpartyPubkey` into
  `ChatRoomState`s (resolves NymIdentity, loads last message preview, counts
  unread).
- `incomingMessageProvider`: new `StreamProvider.family` that wraps
  `messages_api.onNewMessage` for a given trade ID. Consumed by
  `ChatRoomScreen` via `ref.listen`.
- `messageHistoryProvider`: `FutureProvider.family` over
  `messages_api.getMessages` (kept as reference; screen uses it directly).

### Dart (`lib/features/chat/screens/chat_room_screen.dart`)
- Replaces the hardcoded optimistic `_messages` list with bridge-backed state:
  - `_loadHistory`: seeds from `messages_api.getMessages` on `initState`.
  - `_markRead`: calls `messages_api.markAsRead` and resets unread badge.
  - `_onSend`: calls `messages_api.sendMessage`, appends returned message.
  - `ref.listen(incomingMessageProvider)`: appends live incoming messages.
- Adds empty-state placeholder and loading indicator.
- Adapts FRB `rust_types.ChatMessage` → Dart `ChatMessage` for `MessageBubble`.

### Dart (`lib/features/chat/screens/chat_rooms_screen.dart`)
- `ChatRoomsScreen` → `ConsumerStatefulWidget`: calls `_syncRoomsFromTrades`
  on init, populating `chatRoomsNotifierProvider` from `chatRoomsFromTradesProvider`.
@grunch

grunch commented Apr 5, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor
✅ 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

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86e30e8f-54d7-4658-96ca-1f8fa3cb6a9b

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

Walkthrough

This PR connects Rust-backed NIP-59 gift-wrap messaging to the Flutter chat UI: it adds bridge providers mapping trades to chat rooms, streaming incoming messages and history, updates UI screens for lifecycle and realtime updates, and extends Rust order/session logic to derive shared keys and spawn incoming-chat subscriptions.

Changes

Cohort / File(s) Summary
Chat Bridge & Providers
lib/features/chat/providers/chat_providers.dart
Added tradeInfoToChatRoom() to map TradeInfoChatRoomState (skips trades with empty counterpartyPubkey, resolves NymIdentity or deterministic fallback), plus chatRoomsFromTradesProvider, incomingMessageProvider (stream per trade), and messageHistoryProvider (one-shot history fetch).
Chat UI Screens
lib/features/chat/screens/chat_room_screen.dart, lib/features/chat/screens/chat_rooms_screen.dart
Switched room list/messages to use Rust-backed rust_types.ChatMessage; added lifecycle hooks to load history, mark read, and subscribe to incomingMessageProvider; async send flow with send guard, error handling, preview upsert; ChatRoomsScreen becomes ConsumerStatefulWidget and seeds rooms from chatRoomsFromTradesProvider.
Rust Messages API
rust/src/api/messages.rs
Refined send wrapping to use ECDH-derived shared-key pubkey for NIP-59 p tag; added subscribe_incoming_chat(...) to subscribe/decrypt Kind 1059 gift-wraps, filter echoes, insert into in-memory message store, and timeout after 30m idle.
Rust Orders & Sessions
rust/src/api/orders.rs
Extended post-take_order flow: subscribe to gift-wraps for derived trade key, best-effort Mostro session creation, explicit handling of BuyerTookOrder/HoldInvoicePaymentAccepted, and on_peer_pubkey_received() helper to derive shared keys, update session fields, and spawn incoming-chat subscription.

Sequence Diagram(s)

sequenceDiagram
    participant Flutter as Flutter App
    participant Riverpod as Riverpod Provider<br/>(incomingMessageProvider)
    participant RustAPI as Rust Messages API<br/>(subscribe_incoming_chat)
    participant Nostr as Nostr Daemon<br/>(Gift-wrap Events)
    participant MsgStore as In-Memory<br/>Message Store

    Flutter->>Riverpod: ref.listen(incomingMessageProvider(tradeId))
    Riverpod->>RustAPI: subscribe_incoming_chat(shared_pubkey, trade_keys)
    RustAPI->>Nostr: subscribe to Kind 1059 (gift-wrap)
    Nostr-->>RustAPI: gift-wrap event (encrypted)
    RustAPI->>RustAPI: decrypt with recipient_keys
    RustAPI->>RustAPI: parse rumor JSON → ChatMessage
    RustAPI->>RustAPI: filter echo (by trade_pubkey)
    RustAPI->>MsgStore: insert ChatMessage
    MsgStore-->>Riverpod: emit new message on stream
    Riverpod-->>Flutter: deliver AsyncValue.data(msg)
    Flutter->>Flutter: dedupe, append, scroll, mark read, update preview
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 Hopping through trades, the gift-wraps sing,

Messages unwrapped on a shared-key string.
Bridges build rooms where identities bloom,
Rust whispers to Flutter, and UI makes room.
I nibble code carrots — chat blossoms in spring!

🚥 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 accurately summarizes the main change: wiring the P2P chat bridge with send, receive, and chat rooms list functionality across Rust and Dart layers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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-bridge

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
lib/features/chat/screens/chat_room_screen.dart (1)

164-177: ⚠️ Potential issue | 🟡 Minor

Fallback ChatRoomState uses empty orderId instead of widget.orderId.

When _resolveRoom() doesn't find a matching room, the fallback has orderId: ''. This could cause issues in _buildRoomPreview() which uses room.copyWith() — the upserted room would have an empty orderId and not match the current trade.

🐛 Suggested fix
       orElse: () => ChatRoomState(
-        orderId: '',
+        orderId: widget.orderId,
         peerPubkey: '',
         peerHandle: 'Unknown',
         peerIconIndex: 0,
         peerColorHue: 180,
         isSelling: false,
       ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/chat/screens/chat_room_screen.dart` around lines 164 - 177, The
fallback ChatRoomState returned by _resolveRoom() uses orderId: '' which will
break matching when _buildRoomPreview() later calls room.copyWith(); update the
fallback to use the current widget.orderId instead (i.e. set orderId:
widget.orderId) so the upserted room retains the correct orderId; change only
the fallback ChatRoomState in _resolveRoom() to use widget.orderId while keeping
other default fields intact.
🧹 Nitpick comments (5)
rust/src/api/orders.rs (2)

992-994: Extraneous whitespace in log message.

The log message has inconsistent spacing due to string literal continuation across lines. This could make log parsing harder.

📝 Clean up log formatting
         log::warn!(
-            "[orders] on_peer_pubkey_received: session not found for order={order_id},              skipping session update — incoming subscription still spawned"
+            "[orders] on_peer_pubkey_received: session not found for order={order_id}, \
+             skipping session update — incoming subscription still spawned"
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 992 - 994, The log::warn! call inside
on_peer_pubkey_received contains extraneous whitespace from a broken string
literal; replace the multi-line/space-padded message with a single cleanly
formatted string (keeping the {order_id} interpolation) so it reads e.g.
"[orders] on_peer_pubkey_received: session not found for order={order_id},
skipping session update — incoming subscription still spawned" to ensure
consistent log formatting and easier parsing.

899-912: Verify peer pubkey extraction logic for each action type.

The logic extracts buyer_trade_pubkey for BuyerTookOrder and seller_trade_pubkey for all other cases (line 901). However, the match only covers BuyerTookOrder and HoldInvoicePaymentAccepted. For HoldInvoicePaymentAccepted, the buyer receives the seller's pubkey, which is correct. The wildcard _ on line 901 could be more explicit.

📝 Make the match exhaustive for clarity
             let peer_pubkey_hex = match kind.action {
                 Action::BuyerTookOrder => small_order.buyer_trade_pubkey.clone(),
-                _ => small_order.seller_trade_pubkey.clone(),
+                Action::HoldInvoicePaymentAccepted => small_order.seller_trade_pubkey.clone(),
+                _ => unreachable!("only BuyerTookOrder and HoldInvoicePaymentAccepted reach here"),
             };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 899 - 912, The peer pubkey selection
currently uses a wildcard for all non-BuyerTookOrder cases which is ambiguous;
update the match on kind.action so it explicitly lists the Action variants that
should use small_order.seller_trade_pubkey (e.g.,
Action::HoldInvoicePaymentAccepted and any other specific seller-side actions)
and keep Action::BuyerTookOrder mapped to small_order.buyer_trade_pubkey; use an
explicit catch-all arm only if you add a clear comment explaining why it falls
back to seller_trade_pubkey, referencing the symbols kind.action,
Action::BuyerTookOrder, Action::HoldInvoicePaymentAccepted,
small_order.buyer_trade_pubkey, and small_order.seller_trade_pubkey so the
intent is unambiguous and future additions won’t accidentally change behavior.
rust/src/api/messages.rs (2)

926-959: Test name is misleading — it documents that duplicates are not ignored.

The test name add_duplicate_message_is_ignored_in_count suggests duplicates are ignored, but the assertion assert_eq!(msgs.len(), 2) and the comments explicitly state that both messages are stored. Consider renaming to better reflect the documented behavior.

📝 Suggested test rename
-    /// Verify that the message store deduplicates by id.
-    /// subscribe_incoming_chat relies on this to ignore echo messages.
+    /// Documents that the message store does NOT deduplicate by id.
+    /// The Dart UI layer must deduplicate incoming messages.
     #[tokio::test]
-    async fn add_duplicate_message_is_ignored_in_count() {
+    async fn message_store_does_not_deduplicate_by_id() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/messages.rs` around lines 926 - 959, Rename the test function
add_duplicate_message_is_ignored_in_count to reflect that the Rust store does
NOT deduplicate (e.g., add_duplicate_message_is_stored_twice or
add_duplicate_message_is_not_deduplicated_in_store) and update the test doc
comment accordingly; locate the async test function named
add_duplicate_message_is_ignored_in_count (which uses message_store(),
ChatMessage, and get_messages()) and change the function name and top comment so
they document that two messages with the same id are both persisted and the Dart
layer is responsible for deduplication.

654-665: Misleading comment contradicts the actual (correct) code pattern.

The comment on lines 654-655 says "Subscribe BEFORE obtaining the receiver" but the actual code correctly obtains the receiver first (line 660), then subscribes (line 662). This matches the pattern in subscribe_gift_wraps and is the correct approach to avoid missing events. The comment should be updated to match reality.

📝 Suggested comment fix
-    // Subscribe BEFORE obtaining the receiver to avoid missing events that
-    // arrive between the two calls.
+    // Obtain the receiver BEFORE subscribing to avoid missing events that
+    // arrive between the two calls — same pattern as subscribe_gift_wraps.
     let filter = nostr_sdk::Filter::new()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/messages.rs` around lines 654 - 665, The inline comment above
the receiver/subscription sequence is incorrect: update the comment near
client.notifications() / client.subscribe(...) in messages.rs to state that we
obtain the receiver first and then subscribe (matching the pattern used in
subscribe_gift_wraps and the actual code) so the comment reflects the correct
order and intent to avoid missing events.
lib/features/chat/providers/chat_providers.dart (1)

231-236: Consider parallelizing trade-to-room conversion for better performance.

The loop awaits each tradeInfoToChatRoom call sequentially. With many trades, this could be slow since each call may hit the Rust bridge and message store. Using Future.wait would parallelize the conversions.

⚡ Suggested parallel conversion
 final chatRoomsFromTradesProvider =
     FutureProvider<List<ChatRoomState>>((ref) async {
   final trades = await ref.watch(rawTradesProvider.future);

-  final rooms = <ChatRoomState>[];
-  for (final trade in trades) {
-    final room = await tradeInfoToChatRoom(trade);
-    if (room != null) rooms.add(room);
-  }
+  final roomFutures = trades.map(tradeInfoToChatRoom);
+  final results = await Future.wait(roomFutures);
+  final rooms = results.whereType<ChatRoomState>().toList();

   rooms.sort((a, b) => b.lastMessageAt.compareTo(a.lastMessageAt));
   return rooms;
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/chat/providers/chat_providers.dart` around lines 231 - 236, The
current sequential loop awaiting tradeInfoToChatRoom for each element (trades ->
rooms) is slow; convert it to parallel by mapping trades to a list of futures
and using Future.wait to await them concurrently, then filter out nulls to
produce a List<ChatRoomState>. Update the block that builds rooms (references:
trades, tradeInfoToChatRoom, rooms, ChatRoomState) to collect the results of
Future.wait(trades.map(...)) and then use whereType or remove nulls to get the
final rooms list.
🤖 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/chat/screens/chat_room_screen.dart`:
- Around line 68-83: _loadHistory clears and replaces the _messages list which
can drop or duplicate messages that arrive via the stream; instead, fetch
history with messages_api.getMessages(tradeId: widget.orderId) and merge the
returned msgs into the existing _messages while holding mounted checks,
performing deduplication (use the same identity check as _onIncomingMessage) and
preserving message order, then set _historyLoaded = true and call
_scrollToBottom(); alternatively, start the stream listener only after merge
completes to avoid race; update the _loadHistory function and its interaction
with _onIncomingMessage to merge rather than clear-and-replace.

In `@lib/features/chat/screens/chat_rooms_screen.dart`:
- Around line 39-48: _syncRoomsFromTrades currently replaces the notifier state
with the fetched rooms which can overwrite rooms added concurrently (e.g., via
ChatRoomScreen calling upsertRoom); change _syncRoomsFromTrades to merge the
fetched list into the existing notifier state instead of calling setRooms with a
full replacement: read the current state from chatRoomsNotifierProvider, for
each room from chatRoomsFromTradesProvider.future call the notifier's upsertRoom
(or perform a dedupe-by-id merge) so new incoming rooms are preserved and
duplicates are replaced/updated rather than lost.

---

Outside diff comments:
In `@lib/features/chat/screens/chat_room_screen.dart`:
- Around line 164-177: The fallback ChatRoomState returned by _resolveRoom()
uses orderId: '' which will break matching when _buildRoomPreview() later calls
room.copyWith(); update the fallback to use the current widget.orderId instead
(i.e. set orderId: widget.orderId) so the upserted room retains the correct
orderId; change only the fallback ChatRoomState in _resolveRoom() to use
widget.orderId while keeping other default fields intact.

---

Nitpick comments:
In `@lib/features/chat/providers/chat_providers.dart`:
- Around line 231-236: The current sequential loop awaiting tradeInfoToChatRoom
for each element (trades -> rooms) is slow; convert it to parallel by mapping
trades to a list of futures and using Future.wait to await them concurrently,
then filter out nulls to produce a List<ChatRoomState>. Update the block that
builds rooms (references: trades, tradeInfoToChatRoom, rooms, ChatRoomState) to
collect the results of Future.wait(trades.map(...)) and then use whereType or
remove nulls to get the final rooms list.

In `@rust/src/api/messages.rs`:
- Around line 926-959: Rename the test function
add_duplicate_message_is_ignored_in_count to reflect that the Rust store does
NOT deduplicate (e.g., add_duplicate_message_is_stored_twice or
add_duplicate_message_is_not_deduplicated_in_store) and update the test doc
comment accordingly; locate the async test function named
add_duplicate_message_is_ignored_in_count (which uses message_store(),
ChatMessage, and get_messages()) and change the function name and top comment so
they document that two messages with the same id are both persisted and the Dart
layer is responsible for deduplication.
- Around line 654-665: The inline comment above the receiver/subscription
sequence is incorrect: update the comment near client.notifications() /
client.subscribe(...) in messages.rs to state that we obtain the receiver first
and then subscribe (matching the pattern used in subscribe_gift_wraps and the
actual code) so the comment reflects the correct order and intent to avoid
missing events.

In `@rust/src/api/orders.rs`:
- Around line 992-994: The log::warn! call inside on_peer_pubkey_received
contains extraneous whitespace from a broken string literal; replace the
multi-line/space-padded message with a single cleanly formatted string (keeping
the {order_id} interpolation) so it reads e.g. "[orders]
on_peer_pubkey_received: session not found for order={order_id}, skipping
session update — incoming subscription still spawned" to ensure consistent log
formatting and easier parsing.
- Around line 899-912: The peer pubkey selection currently uses a wildcard for
all non-BuyerTookOrder cases which is ambiguous; update the match on kind.action
so it explicitly lists the Action variants that should use
small_order.seller_trade_pubkey (e.g., Action::HoldInvoicePaymentAccepted and
any other specific seller-side actions) and keep Action::BuyerTookOrder mapped
to small_order.buyer_trade_pubkey; use an explicit catch-all arm only if you add
a clear comment explaining why it falls back to seller_trade_pubkey, referencing
the symbols kind.action, Action::BuyerTookOrder,
Action::HoldInvoicePaymentAccepted, small_order.buyer_trade_pubkey, and
small_order.seller_trade_pubkey so the intent is unambiguous and future
additions won’t accidentally change behavior.
🪄 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: 0078d78b-aa81-47e7-98ed-592162c0ddd2

📥 Commits

Reviewing files that changed from the base of the PR and between 2cd1b4b and 6162af0.

📒 Files selected for processing (5)
  • lib/features/chat/providers/chat_providers.dart
  • lib/features/chat/screens/chat_room_screen.dart
  • lib/features/chat/screens/chat_rooms_screen.dart
  • rust/src/api/messages.rs
  • rust/src/api/orders.rs

Comment thread lib/features/chat/screens/chat_room_screen.dart
Comment thread lib/features/chat/screens/chat_rooms_screen.dart
- messages.rs: fix comment order near notifications()/subscribe() to match
  actual code; rename test to add_duplicate_message_is_not_deduplicated_in_store
  to accurately document that the Rust store does not deduplicate; fix extra
  whitespace in subscribe_incoming_chat log::info string
- orders.rs: fix extraneous whitespace in log::warn string literal inside
  on_peer_pubkey_received
- chat_room_screen.dart: _loadHistory now merges history into _messages with
  dedup-by-id + sort instead of clear-and-replace, eliminating the race where
  stream messages arriving during the await could be lost or duplicated;
  _resolveRoom fallback now uses widget.orderId instead of '' so _buildRoomPreview
  produces a room with the correct orderId
- chat_rooms_screen.dart: _syncRoomsFromTrades now calls upsertRoom for each
  fetched room instead of setRooms, preserving concurrent upserts from
  ChatRoomScreen
- chat_providers.dart: chatRoomsFromTradesProvider now uses Future.wait to
  resolve all rooms in parallel instead of sequentially
…eceived

- Replace wildcard arm in peer-pubkey selection with explicit
  Action::HoldInvoicePaymentAccepted, add unreachable!() for the catch-all
  so future action additions don't silently pick the wrong pubkey field
- Fix log::warn string: remove embedded spaces from multi-line literal so
  the message renders cleanly in structured log parsers
@grunch

grunch commented Apr 5, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor
✅ 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 coderabbitai Bot left a comment

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.

🧹 Nitpick comments (3)
rust/src/api/messages.rs (1)

724-735: Verify the echo-filtering logic covers all scenarios.

The echo filter compares sender_pubkey (from the inner rumor's pubkey field) against trade_pubkey_hex. This assumes the sender always signs with their trade pubkey. If the sender ever signs with a different key (e.g., due to key rotation or an edge case), echoes could leak through.

Consider adding a comment clarifying this assumption, or verify that the protocol guarantees the inner rumor pubkey always matches the trade pubkey.

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

In `@rust/src/api/messages.rs` around lines 724 - 735, The echo filter currently
compares the inner rumor's sender_pubkey to trade_pubkey_hex which assumes the
sender always signs with their trade key; update the code to either (1) document
this assumption with a clear comment near sender_pubkey/trade_pubkey_hex
explaining that protocol guarantees inner.pubkey == trade pubkey (or note if key
rotation is possible), and/or (2) strengthen the check by also comparing the
outer/top-level event pubkey (if available) or other authoritative author
identity (e.g., event.pubkey or signature-verified author) against
trade_pubkey_hex so echoes signed by a different key don't bypass the filter;
reference the sender_pubkey, inner, and trade_pubkey_hex symbols when making the
change.
lib/features/chat/screens/chat_room_screen.dart (1)

187-196: Consider using the Rust-side isRead flag instead of local recomputation.

_buildRoomPreview recomputes unreadCount from the local _messages list. Since messages arrive with isRead: false from the Rust layer and _markRead() calls the bridge to update read status, the local recomputation may drift from the bridge's state if there's any timing mismatch.

However, since _markRead() is called immediately after receiving messages and the bridge call is awaited before any UI update, this is likely fine in practice.

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

In `@lib/features/chat/screens/chat_room_screen.dart` around lines 187 - 196,
_buildRoomPreview currently recomputes unreadCount from the local _messages list
which can drift from the Rust bridge state; instead, use the Rust-provided
unread/read state (e.g., the room model returned by _resolveRoom or the
message's isRead flag) to populate unreadCount. Replace the local recompute (the
unread variable using _messages.where(...).length) and set unreadCount to the
value provided by the resolved room (room.unreadCount) or, if the room model
lacks it, derive from the incoming rust_types.ChatMessage.isRead values provided
by the bridge, and ensure this aligns with _markRead bridge calls.
rust/src/api/orders.rs (1)

996-1001: Fix malformed log message with embedded whitespace.

The log message spans multiple lines with irregular indentation that will appear in the output. Consider consolidating to a single line or using proper formatting.

✨ Suggested fix
-        log::warn!(
-            "[orders] on_peer_pubkey_received: session not found for              order={order_id}, skipping session update —              incoming subscription still spawned"
-        );
+        log::warn!(
+            "[orders] on_peer_pubkey_received: session not found for order={order_id}, \
+             skipping session update — incoming subscription still spawned"
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/api/orders.rs` around lines 996 - 1001, The log::warn! invocation
inside on_peer_pubkey_received currently contains embedded newlines and
irregular whitespace; consolidate it into a single-line message and use proper
formatting placeholders instead of inline braces so it prints cleanly, e.g.
replace the multi-line string with a single line like "session not found for
order={}, skipping session update — incoming subscription still spawned" and
pass order_id as the argument to log::warn!(...) (or use a named parameter like
order_id = order_id) to ensure correct formatting.
🤖 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/chat/screens/chat_room_screen.dart`:
- Around line 187-196: _buildRoomPreview currently recomputes unreadCount from
the local _messages list which can drift from the Rust bridge state; instead,
use the Rust-provided unread/read state (e.g., the room model returned by
_resolveRoom or the message's isRead flag) to populate unreadCount. Replace the
local recompute (the unread variable using _messages.where(...).length) and set
unreadCount to the value provided by the resolved room (room.unreadCount) or, if
the room model lacks it, derive from the incoming rust_types.ChatMessage.isRead
values provided by the bridge, and ensure this aligns with _markRead bridge
calls.

In `@rust/src/api/messages.rs`:
- Around line 724-735: The echo filter currently compares the inner rumor's
sender_pubkey to trade_pubkey_hex which assumes the sender always signs with
their trade key; update the code to either (1) document this assumption with a
clear comment near sender_pubkey/trade_pubkey_hex explaining that protocol
guarantees inner.pubkey == trade pubkey (or note if key rotation is possible),
and/or (2) strengthen the check by also comparing the outer/top-level event
pubkey (if available) or other authoritative author identity (e.g., event.pubkey
or signature-verified author) against trade_pubkey_hex so echoes signed by a
different key don't bypass the filter; reference the sender_pubkey, inner, and
trade_pubkey_hex symbols when making the change.

In `@rust/src/api/orders.rs`:
- Around line 996-1001: The log::warn! invocation inside on_peer_pubkey_received
currently contains embedded newlines and irregular whitespace; consolidate it
into a single-line message and use proper formatting placeholders instead of
inline braces so it prints cleanly, e.g. replace the multi-line string with a
single line like "session not found for order={}, skipping session update —
incoming subscription still spawned" and pass order_id as the argument to
log::warn!(...) (or use a named parameter like order_id = order_id) to ensure
correct formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc3ddc09-167b-40f7-bfb4-b75e78f5587d

📥 Commits

Reviewing files that changed from the base of the PR and between 6162af0 and e53ce85.

📒 Files selected for processing (5)
  • lib/features/chat/providers/chat_providers.dart
  • lib/features/chat/screens/chat_room_screen.dart
  • lib/features/chat/screens/chat_rooms_screen.dart
  • rust/src/api/messages.rs
  • rust/src/api/orders.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/chat/providers/chat_providers.dart

- orders.rs: consolidate log::warn into single clean line (no embedded spaces)
- messages.rs: document echo-filter assumption — inner.pubkey is the sender's
  trade key per protocol; outer event pubkey is ephemeral and non-authoritative;
  key rotation is not supported within a session; comment explains why comparing
  inner sender_pubkey against trade_pubkey_hex is the correct dedup strategy
- chat_room_screen.dart: _buildRoomPreview now uses room.unreadCount (bridge
  state) + increments by 1 for new unread peer messages instead of recomputing
  from local _messages list which could drift from the async markAsRead call
@grunch
grunch merged commit e4ac732 into main Apr 5, 2026
1 check passed
@grunch
grunch deleted the feat/p2p-chat-bridge branch April 5, 2026 20:25
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