Skip to content

Conversation

@AndreaDiazCorreia
Copy link
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Oct 10, 2025

Dispute Chat Communication (Admin ↔ User)

Implements bidirectional encrypted messaging between admin (CLI) and users (mobile) for dispute resolution, following NIP-59 (Gift Wrap) protocol.

Changes

Encryption/Decryption (nostr_event.dart)

  • Rewrote mostroUnWrap and mostroWrap to follow correct NIP-59 flow: RUMOR (kind 1) → SEAL (kind 13) → Gift Wrap (kind 1059)
  • Added _sanitizeEventJson to handle null id/sig in unsigned RUMORs without breaking validation

Dispute Chat (dispute_chat_notifier.dart)

  • Outgoing: Format [{"dm": {"version": 1, "action": "send-dm", "payload": {"text_message": "..."}}}, null] matching CLI expectations
  • Incoming: Dual parser for CLI format ({"dm": {...}}) and legacy MostroMessage format
  • Null-safe handling for event IDs and timestamps

Message Routing (mostro_service.dart)

  • Added filter to skip dispute messages (with "dm" key) and delegate to DisputeChatNotifier
  • Prevents parsing errors when receiving admin messages

Chat Room (chat_room_notifier.dart)

  • Updated mostroWrap call signature to match new two-parameter API

Testing

  • User → Admin messages delivered and parsed correctly in CLI
  • Admin → User messages received and displayed in mobile chat
  • Existing Mostro daemon messages unaffected

Summary by CodeRabbit

  • New Features

    • Full dispute chat: historical loading, live wrapped-message subscription, send with optimistic UI, publish handling, local persistence, and per-dispute read-status provider.
    • New wrap/unwrap flow for secure Gift Wrap messaging and message-format support.
  • Improvements

    • Stronger multi-step validation, clearer error reporting, and Riverpod-driven UI/input components with improved loading/error states.
  • Bug Fixes

    • More reliable timestamps, admin extraction, message ID fallback, and skipping of non-chat payloads.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Implements Mostro Gift Wrap wrap/unwrap flows (1059→13→1) with JSON sanitization and layered errors; changes mostroWrap signature to accept sender keys + receiver pubkey; replaces dispute-chat stubs with a Riverpod-backed DisputeChatNotifier/state, read-status provider/service, UI widget migrations, and MostroService message filtering/storage key fallbacks.

Changes

Cohort / File(s) Summary
Mostro Gift Wrap & Nostr event logic
lib/data/models/nostr_event.dart
Adds _sanitizeEventJson; rewrites mostroUnWrap to validate kind 1059 → decrypt SEAL (13) → decrypt RUMOR (1), sanitize/validate RUMOR, and layered error messages; changes mostroWrap signature to (NostrKeyPairs senderKeys, String receiverPubkey) and implements RUMOR→SEAL→Gift Wrap creation with randomized timestamp.
Mostro service & utils
lib/services/mostro_service.dart, lib/shared/utils/nostr_utils.dart
Validates decrypted payload is a non-empty List; skips dispute-chat payloads when first element is a Map with "dm"; updates message storage key fallback (decryptedEvent.id → event.id → timestamp); trivial whitespace cleanup.
Payload parsing
lib/data/models/payload.dart
Adds TextMessage import and support in Payload.fromJson when JSON contains text_message.
Chat send path
lib/features/chat/notifiers/chat_room_notifier.dart
Updates mostroWrap invocation to pass session.tradeKey and session.sharedKey!.public (new two-arg signature); wraps publish in try/catch, publishes wrapped event, and handles publish failure by removing the pending inner event.
Dispute chat notifier & providers
lib/features/disputes/notifiers/dispute_chat_notifier.dart, lib/features/disputes/providers/dispute_providers.dart, lib/features/disputes/providers/dispute_read_status_provider.dart, lib/services/dispute_read_status_service.dart
Replaces stub with stateful DisputeChatNotifier exposing DisputeChatState; adds initialize/subscribe/load/send/persist flows, session discovery, event unwrapping/validation, dedupe/sort, and error handling; reshapes userDisputeDataProvider to Provider<AsyncValue<List<DisputeData>>>; adds disputeReadStatusProvider and DisputeReadStatusService (SharedPreferences-backed).
Dispute UI widgets/screens
lib/features/disputes/screens/dispute_chat_screen.dart, lib/features/disputes/widgets/dispute_message_input.dart, lib/features/disputes/widgets/dispute_messages_list.dart, lib/features/disputes/widgets/dispute_content.dart, lib/features/disputes/widgets/dispute_list_item.dart
Converts multiple widgets/screens to Riverpod ConsumerStatefulWidget/ConsumerState, integrates with new notifier/provider state, marks disputes read on open/tap, updates layouts, adds optional ScrollController, optimistic UI send/update, and provider-driven data resolution.
Order state adjustments
lib/features/order/models/order_state.dart
Normalizes dispute createdAt from seconds→milliseconds when needed and extracts/propagates adminPubkey on admin-take paths.
Dispute chat model
lib/data/models/dispute_chat.dart
Adds isPending and error fields, updates JSON (de)serialization and adds copyWith.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Sender as User A
  participant AppA as App (A)
  participant Relays as Nostr Relays
  participant AppB as App (B)
  actor Receiver as User B

  rect rgb(240,248,255)
    note right of AppA: Wrap (send)
    Sender->>AppA: Compose message
    AppA->>AppA: Build RUMOR (kind 1) payload (signed)
    AppA->>AppA: Encrypt RUMOR -> SEAL (kind 13) using sender priv + receiver pub
    AppA->>AppA: Encrypt SEAL -> Gift Wrap (kind 1059) with ephemeral key + receiver pub
    AppA->>Relays: Publish Gift Wrap (1059)
  end

  rect rgb(245,255,240)
    note right of AppB: Unwrap (receive)
    Relays-->>AppB: Deliver Gift Wrap (1059)
    AppB->>AppB: Decrypt Gift Wrap -> SEAL (13)
    AppB->>AppB: Decrypt SEAL -> RUMOR (1) using sender pub
    AppB->>AppB: Sanitize & validate RUMOR (ensure unsigned fields normalized)
    AppB-->>Receiver: Render message
  end
Loading
sequenceDiagram
  autonumber
  participant UI as Dispute UI
  participant Notifier as DisputeChatNotifier
  participant Store as Local Storage
  participant Relays as Nostr Relays

  rect rgb(255,250,240)
    note over Notifier: Initialization
    UI->>Notifier: initialize()
    Notifier->>Store: load historical messages
    Notifier->>Relays: subscribe kind 1059 for session/dispute
  end

  rect rgb(250,240,255)
    note over Notifier: Receiving events
    Relays-->>Notifier: Gift Wrap 1059
    Notifier->>Notifier: unwrap (1059→13→1) & parse payload
    alt valid dispute DM
      Notifier->>Store: save message
      Notifier-->>UI: update state (deduped, sorted)
    else skip/invalid
      Notifier-->>Notifier: ignore/log
    end
  end

  rect rgb(240,255,255)
    note over Notifier: Sending
    UI->>Notifier: sendMessage(text)
    Notifier->>Notifier: wrap (RUMOR→SEAL→Gift Wrap)
    Notifier->>Relays: publish 1059
    Notifier->>Store: save optimistic message
    Notifier-->>UI: update state
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

A rabbit nibbles bytes in moonlit code,
SEALs fold into rumors on a secret road.
Gift Wrap hops from key to key,
Riverpod tends the chat-tree free.
Hooray — disputes are read and sowed. 🥕✨

Pre-merge checks and finishing touches

✅ 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 "Dispute Chat implementation" is fully related to the main changes in this pull request. The changeset introduces a complete dispute chat system including encrypted messaging infrastructure (NIP-59 Gift Wrap protocol via mostroUnWrap/mostroWrap), a new DisputeChatNotifier with state management, message routing logic, UI widgets (dispute chat screen, message input, messages list), and read status tracking. The title clearly captures the primary objective and would allow a teammate scanning the repository history to immediately understand that this PR introduces the dispute chat feature. The title is concise at four words and avoids vague terms while remaining appropriately high-level (per the instructions, not all implementation details are expected in the title).
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch andrea/dispute-chat

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
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

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

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: 4

Caution

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

⚠️ Outside diff range comments (3)
lib/features/order/models/order_state.dart (1)

115-131: Existing peer may be lost when Order payload lacks trade pubkeys

In the Order branch, if neither buyerTradePubkey nor sellerTradePubkey exists, newPeer stays null and overrides the existing peer in the final state. Preserve the current peer in that case and avoid misleading logs.

Apply this diff:

-    Peer? newPeer;
+    Peer? newPeer;
     if (message.payload is Peer &&
         message.getPayload<Peer>()!.publicKey.isNotEmpty) {
       newPeer = message.getPayload<Peer>();
       _logger.i('👤 New Peer found in message');
     } else if (message.payload is Order) {
       if (message.getPayload<Order>()!.buyerTradePubkey != null) {
         newPeer =
             Peer(publicKey: message.getPayload<Order>()!.buyerTradePubkey!);
       } else if (message.getPayload<Order>()!.sellerTradePubkey != null) {
         newPeer =
             Peer(publicKey: message.getPayload<Order>()!.sellerTradePubkey!);
       }
-      _logger.i('👤 New Peer found in message');
+      if (newPeer != null) {
+        _logger.i('👤 New Peer found in message');
+      } else {
+        newPeer = peer; // Preserve existing if no trade pubkey present
+        _logger.i('👤 Peer unchanged (no trade pubkey in Order)');
+      }
     } else {
       newPeer = peer; // Preserve existing
     }
lib/features/chat/notifiers/chat_room_notifier.dart (2)

153-156: Dedup by nullable id collapses chat history; add robust key and null-safe sort

Unwrapped/sent inner events may have null id (RUMOR). Current dedup keeps only the last null-id message and sort uses createdAt! risking NPEs.

Use a stable fallback key and null-safe sort.

Apply these replacements at the noted spots:

-      final deduped = {for (var m in allMessages) m.id: m}.values.toList();
-      deduped.sort((a, b) => b.createdAt!.compareTo(a.createdAt!));
+      String _msgKey(NostrEvent m) =>
+          m.id ??
+          '${m.createdAt?.millisecondsSinceEpoch ?? 0}:${m.pubkey}:${m.content.hashCode}';
+      final deduped = {for (var m in allMessages) _msgKey(m): m}.values.toList();
+      deduped.sort((a, b) =>
+          (b.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0))
+              .compareTo(a.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0)));

Do this in both runtime receive and historical load paths.

(Line ranges: 153-156, 197-201, 336-340)

Also applies to: 197-201, 336-340


213-216: Send-failure removal may delete unrelated messages with null ids

Filtering by id removes all messages whose id is null. Remove by a stable key aligned with your dedup key.

-      final updatedMessages =
-          state.messages.where((msg) => msg.id != innerEvent.id).toList();
+      String _msgKey(NostrEvent m) =>
+          m.id ??
+          '${m.createdAt?.millisecondsSinceEpoch ?? 0}:${m.pubkey}:${m.content.hashCode}';
+      final targetKey = _msgKey(innerEvent);
+      final updatedMessages =
+          state.messages.where((m) => _msgKey(m) != targetKey).toList();
🧹 Nitpick comments (11)
lib/data/models/dispute.dart (1)

380-409: Normalize status and use a Set to avoid duplicates and edge cases

terminalStatusList contains both hyphen and underscore variants. Normalize once and compare against a Set to reduce duplication and avoid mismatches.

Apply this diff:

-    // Terminal dispute states where admin should not be used as counterparty
-    // Terminal statuses where the dispute is finished and admin should not be shown as counterparty
-    final terminalStatusList = [
-      'resolved',
-      'closed',
-      'seller-refunded',
-      'seller_refunded',
-      'admin-canceled',
-      'admin_canceled',
-      'admin-settled',
-      'admin_settled',
-      'solved',
-      'completed',
-    ];
+    // Terminal dispute states where admin should not be shown as counterparty
+    const terminalStatusSet = <String>{
+      'resolved',
+      'closed',
+      'seller-refunded',
+      'admin-canceled',
+      'admin-settled',
+      'solved',
+      'completed',
+    };
@@
-    // 3. Dispute is not in a terminal state (normalize status to lowercase for comparison)
-    final normalizedStatus = dispute.status?.toLowerCase().trim() ?? '';
+    // 3. Dispute is not in a terminal state (normalize to lowercase, unify underscores to hyphens)
+    final normalizedStatus =
+        (dispute.status ?? '').toLowerCase().replaceAll('_', '-').trim();
     if (orderState?.peer == null && 
         dispute.adminPubkey != null && 
-        !terminalStatusList.contains(normalizedStatus)) {
+        !terminalStatusSet.contains(normalizedStatus)) {
       counterpartyName = dispute.adminPubkey;
     }
lib/features/order/notfiers/abstract_mostro_notifier.dart (1)

150-174: DRY up role-aware peer derivation/update

Same logic appears in buyerTookOrder and holdInvoicePaymentAccepted. Extract a helper to compute/update peer once.

Example:

Peer? _derivePeerFromOrder(Session session, Order order) {
  final peerPubkey = session.role == Role.buyer
      ? order.sellerTradePubkey
      : session.role == Role.seller
          ? order.buyerTradePubkey
          : null;
  return peerPubkey != null ? Peer(publicKey: peerPubkey) : null;
}

void _applyPeer(SessionProvider notifier, String orderId, Peer? peer) {
  notifier.updateSession(orderId, (s) => s.peer = peer);
  state = state.copyWith(peer: peer);
}

Then call _applyPeer(..., _derivePeerFromOrder(session, order));

Also applies to: 198-221

lib/features/disputes/providers/dispute_providers.dart (1)

98-113: Prefer typing order state over dynamic

Use a typed OrderState? instead of dynamic for clarity and safety.

-        Session? matchingSession;
-        dynamic matchingOrderState;
+        Session? matchingSession;
+        OrderState? matchingOrderState;
...
-          return DisputeData.fromDispute(
-            dispute, 
-            orderState: matchingOrderState,
-            userRole: userRole,
-          );
+          return DisputeData.fromDispute(
+            dispute,
+            orderState: matchingOrderState,
+            userRole: userRole,
+          );

Also applies to: 133-146

lib/features/disputes/screens/dispute_chat_screen.dart (5)

96-108: Make dependent reads reactive (watch) to avoid stale UI

Using ref.read for sessionNotifierProvider and orderNotifierProvider means changes in sessions/orders won’t rebuild this screen. Prefer ref.watch so disputeData stays current.

Example:

-      final sessions = ref.read(sessionNotifierProvider);
+      final sessions = ref.watch(sessionNotifierProvider);
...
-            final orderState = ref.read(orderNotifierProvider(session.orderId!));
+            final orderState = ref.watch(orderNotifierProvider(session.orderId!));

If reactivity here is intentionally avoided to reduce rebuilds, add a comment explaining the decision.

Also applies to: 165-171


68-71: Avoid stringly-typed status checks

disputeData.status == 'in-progress' risks drift. Prefer an enum or centralized constants (e.g., DisputeStatus.inProgress or DisputeStatuses.inProgress).

-              if (disputeData.status == 'in-progress')
+              if (disputeData.status == DisputeStatuses.inProgress)
                 DisputeMessageInput(disputeId: disputeId)

217-219: Don’t swallow errors silently

The catch block drops the error. Log it for diagnostics.

-    } catch (e) {
-      // Fallback to basic conversion on error
-    }
+    } catch (e, st) {
+      debugPrint('DisputeChatScreen: _convertToDisputeData fallback due to error: $e\n$st');
+      // Fallback to basic conversion on error
+    }

241-252: Ensure counterpartyDisplay reflects overridden counterparty

You override counterparty but downstream UI (DisputeStatusContent) reads counterpartyDisplay. If counterpartyDisplay isn’t derived from counterparty, nickname resolution may still see “unknown”.

  • Verify DisputeData.counterpartyDisplay derives from counterparty or add a copyWith(counterpartyDisplay: …) equivalent if needed.
  • Alternatively, use a copyWith if available instead of rebuilding by hand.

100-101: Avoid dynamic for matchingOrderState
Replace

dynamic matchingOrderState;

with

OrderState matchingOrderState;

to restore type safety and IntelliSense.

lib/features/disputes/widgets/dispute_status_content.dart (3)

61-66: Use withOpacity for broader Flutter SDK compatibility

Color.withValues(alpha: …) requires newer Flutter. To avoid build issues on older SDKs, prefer withOpacity.

-          color: AppTheme.mostroGreen.withValues(alpha: 0.1),
+          color: AppTheme.mostroGreen.withOpacity(0.1),
...
-            color: AppTheme.mostroGreen.withValues(alpha: 0.3),
+            color: AppTheme.mostroGreen.withOpacity(0.3),

If your minimum SDK guarantees withValues, keep as-is.


26-28: Centralize status strings

Comparing lowercase strings ('resolved'|'closed'|'seller-refunded') is brittle. Prefer enum/constant source of truth shared with DisputeData.

final isResolved = {
  DisputeStatuses.resolved,
  DisputeStatuses.closed,
  DisputeStatuses.sellerRefunded,
}.contains(dispute.status);

158-195: Switch mixes descriptionKey and action/status; consider normalizing

You branch on descriptionKey here, but resolved text also depends on action and sometimes status. Consider centralizing the resolution of “state + action → message” to a single helper to avoid divergent logic across widgets.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a1d667 and 258e52b.

📒 Files selected for processing (18)
  • lib/data/models/dispute.dart (3 hunks)
  • lib/data/models/nostr_event.dart (1 hunks)
  • lib/data/models/payload.dart (2 hunks)
  • lib/features/chat/notifiers/chat_room_notifier.dart (1 hunks)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
  • lib/features/disputes/providers/dispute_providers.dart (3 hunks)
  • lib/features/disputes/screens/dispute_chat_screen.dart (3 hunks)
  • lib/features/disputes/widgets/dispute_info_card.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_message_input.dart (4 hunks)
  • lib/features/disputes/widgets/dispute_messages_list.dart (6 hunks)
  • lib/features/disputes/widgets/dispute_status_content.dart (6 hunks)
  • lib/features/order/models/order_state.dart (2 hunks)
  • lib/features/order/notfiers/abstract_mostro_notifier.dart (5 hunks)
  • lib/l10n/intl_en.arb (2 hunks)
  • lib/l10n/intl_es.arb (2 hunks)
  • lib/l10n/intl_it.arb (2 hunks)
  • lib/services/mostro_service.dart (1 hunks)
  • lib/shared/utils/nostr_utils.dart (0 hunks)
💤 Files with no reviewable changes (1)
  • lib/shared/utils/nostr_utils.dart
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-08T23:54:09.987Z
Learnt from: chebizarro
PR: MostroP2P/mobile#110
File: lib/features/trades/widgets/mostro_message_detail_widget.dart:134-141
Timestamp: 2025-06-08T23:54:09.987Z
Learning: In the OrderState refactor, public keys should be accessed via `tradeState.peer?.publicKey` from the OrderState instance rather than through `session?.peer?.publicKey`, as the OrderState encapsulates peer information directly.

Applied to files:

  • lib/features/order/notfiers/abstract_mostro_notifier.dart
🔇 Additional comments (11)
lib/features/order/models/order_state.dart (1)

160-167: Admin pubkey extraction on adminTookDispute looks good

Pulling admin pubkey from Peer payload and preserving existing when absent is correct. Setting adminTookAt now and status to in-progress matches the flow.

Also applies to: 172-173

lib/l10n/intl_it.arb (1)

758-759: New dispute localization keys added (IT) — verify locale parity

Strings look good and placeholders are defined. Please ensure the same keys exist in the base/EN ARB (and any other supported locales) so gen_l10n doesn’t fail at build time.

Would you confirm intl_en.arb contains:

  • disputeChatClosed
  • disputeSettledBuyerMessage / disputeSettledSellerMessage
  • disputeCanceledBuyerMessage / disputeCanceledSellerMessage
  • disputeOpenedByYouAgainstSeller / disputeOpenedByYouAgainstBuyer

Also applies to: 771-775, 781-792

lib/data/models/dispute.dart (1)

340-347: Role-aware DisputeData factory: LGTM

  • Passing userRole into DisputeData and defaulting to unknown is sensible.
  • Counterparty preference: peer first, then admin (non-terminal) is correct.
  • Debug prints aid diagnosis.

Also applies to: 374-379, 395-398, 410-412, 429-431

lib/l10n/intl_es.arb (1)

704-705: New dispute localization keys added (ES) — verify locale parity

Translations and placeholder blocks look consistent. Please ensure base/EN and other locales define the same keys to keep gen_l10n happy.

Confirm intl_en.arb includes:

  • disputeChatClosed
  • disputeSettledBuyerMessage / disputeSettledSellerMessage
  • disputeCanceledBuyerMessage / disputeCanceledSellerMessage
  • disputeOpenedByYouAgainstSeller / disputeOpenedByYouAgainstBuyer

Also applies to: 717-720, 727-738

lib/data/models/payload.dart (1)

9-10: Call sites correctly pass inner payload dispute_chat_notifier unwraps the dm wrapper (CLI format handles payload['text_message'] directly; old format calls MostroMessage.fromJson on the inner payload), so Payload.fromJson never receives the outer wrapper—no further action needed.

lib/features/disputes/widgets/dispute_info_card.dart (1)

22-26: Counterparty fallback logic looks good; verify provider return type

Condition is correct and prevents unnecessary provider work. Please confirm nickNameProvider returns a String (not AsyncValue); otherwise unwrap appropriately.

lib/features/chat/notifiers/chat_room_notifier.dart (1)

191-194: Wrap call updated to (senderKeys, receiverPubkey) — OK

This matches the new mostroWrap signature and NIP-59 flow.

lib/services/mostro_service.dart (1)

82-88: Message key fallback for storage is solid

Using decryptedEvent.id with fallback to envelope id (and timestamp) avoids missing IDs from admin messages.

lib/features/order/notfiers/abstract_mostro_notifier.dart (1)

293-303: Dispute createdAt propagation and logging — OK

Using event timestamp when present and persisting createdAt/action/status improves consistency and UI behavior.

Also applies to: 336-376

lib/features/disputes/providers/dispute_providers.dart (1)

44-59: Session/order watchers for fine-grained invalidation — OK

Targeted ref.watch to the containing order state is a good optimization; fallback path is sensible.

lib/features/disputes/widgets/dispute_status_content.dart (1)

21-23: Remove AsyncValue handling — nickNameProvider is synchronous
nickNameProvider is defined as Provider.family<String, String>, returning a String directly. There’s no AsyncValue to unwrap; the existing usage is correct.

@AndreaDiazCorreia
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 12, 2025

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

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 (2)
lib/features/disputes/screens/dispute_chat_screen.dart (2)

111-114: Consider logging errors before continuing.

Empty catch blocks silently swallow errors. Add debug logging to aid troubleshooting.

         } catch (e) {
-          // Continue checking other sessions
+          debugPrint('DisputeChatScreen: Error reading order state for session ${session.orderId}: $e');
           continue;
         }

Also applies to: 210-213


227-257: Consider optimizing object creation.

Creating standardDisputeData and then recreating a new DisputeData with most identical fields is inefficient. If DisputeData has a copyWith method, use it instead.

Example optimization if copyWith exists:

DisputeData _createDisputeDataWithChatInfo(
  Dispute dispute, 
  dynamic orderState, 
  String peerPubkey,
  UserRole? userRole,
) {
  return DisputeData.fromDispute(
    dispute, 
    orderState: orderState,
    userRole: userRole,
  ).copyWith(counterparty: peerPubkey);
}

Alternatively, consider adding a counterparty parameter to DisputeData.fromDispute.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 258e52b and 01a7c71.

📒 Files selected for processing (4)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
  • lib/features/disputes/screens/dispute_chat_screen.dart (3 hunks)
  • lib/features/order/models/order_state.dart (2 hunks)
  • lib/services/mostro_service.dart (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/order/models/order_state.dart
🔇 Additional comments (4)
lib/features/disputes/screens/dispute_chat_screen.dart (2)

23-90: LGTM! Clean provider integration and AsyncValue handling.

The migration to ConsumerWidget with AsyncValue.when pattern is well-implemented. The conditional input visibility based on dispute status is appropriate.


102-105: Previous NPE concern addressed.

The null checks have been added as suggested in the previous review, preventing NPE when both orderId values are null.

lib/services/mostro_service.dart (2)

72-82: LGTM! Empty payload guard correctly implemented.

The validation logic correctly addresses the past review comment by checking both that result is a List and that it's not empty before accessing result[0]. The subsequent dispute message skip logic safely accesses the first element after validation.

The defensive check result[0] is Map before the containsKey call is good defensive programming.


87-93: LGTM! Sensible ID fallback chain.

The three-tier fallback chain (decryptedEvent.id → event.id → timestamp-based key) appropriately handles edge cases where admin messages might not include an ID in the decrypted event.

Given that line 47 expects event.id to be non-null (via the ! operator), the timestamp-based fallback should rarely execute in practice, making it good defensive programming for unexpected edge cases.

The updated logging on line 92 correctly reflects the fallback logic.

@AndreaDiazCorreia AndreaDiazCorreia marked this pull request as ready for review October 13, 2025 22:05
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

♻️ Duplicate comments (1)
lib/features/disputes/screens/dispute_chat_screen.dart (1)

121-129: NPE guard on orderId compare is correctly fixed.

Both sides are null‑checked before deref; safe to call orderNotifierProvider.

🧹 Nitpick comments (11)
lib/features/disputes/providers/dispute_read_status_provider.dart (1)

5-7: Auto‑dispose this family provider (it’s a rebuild signal, not long‑lived state).

Prevents stale instances/memory retention and aligns with Riverpod best practices.

-final disputeReadStatusProvider = StateProvider.family<int, String>((ref, disputeId) {
-  return DateTime.now().millisecondsSinceEpoch;
-});
+final disputeReadStatusProvider =
+    StateProvider.autoDispose.family<int, String>((ref, disputeId) {
+  return DateTime.now().millisecondsSinceEpoch;
+});
lib/features/disputes/widgets/dispute_list_item.dart (2)

20-24: Ensure navigation still happens if marking as read fails.

Guard with try/finally so onTap always runs.

-      onTap: () async {
-        // Mark dispute as read when user opens it
-        await DisputeReadStatusService.markDisputeAsRead(dispute.disputeId);
-        onTap();
-      },
+      onTap: () async {
+        try {
+          // Mark dispute as read when user opens it
+          await DisputeReadStatusService.markDisputeAsRead(dispute.disputeId);
+        } finally {
+          onTap();
+        }
+      },

1-4: Optional: Make this a ConsumerWidget and bump the read‑status provider for instant UI updates.

This triggers list/UI to drop the unread dot immediately after marking as read.

 import 'package:flutter/material.dart';
 import 'package:mostro_mobile/features/disputes/widgets/dispute_icon.dart';
 import 'package:mostro_mobile/features/disputes/widgets/dispute_content.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mostro_mobile/features/disputes/providers/dispute_read_status_provider.dart';
 import 'package:mostro_mobile/services/dispute_read_status_service.dart';
 import 'package:mostro_mobile/data/models/dispute.dart';
 
-class DisputeListItem extends StatelessWidget {
+class DisputeListItem extends ConsumerWidget {
   final DisputeData dispute;
   final VoidCallback onTap;
 
   const DisputeListItem({
     super.key,
     required this.dispute,
     required this.onTap,
   });
 
   @override
-  Widget build(BuildContext context) {
+  Widget build(BuildContext context, WidgetRef ref) {
     return GestureDetector(
-      onTap: () async {
+      onTap: () async {
         // Mark dispute as read when user opens it
-        await DisputeReadStatusService.markDisputeAsRead(dispute.disputeId);
-        onTap();
+        try {
+          await DisputeReadStatusService.markDisputeAsRead(dispute.disputeId);
+          // Notify provider to refresh dependent widgets
+          ref.read(disputeReadStatusProvider(dispute.disputeId).notifier).state =
+              DateTime.now().millisecondsSinceEpoch;
+        } finally {
+          onTap();
+        }
       },

Also applies to: 7-15, 20-24

lib/services/dispute_read_status_service.dart (1)

1-20: Prefer SharedPreferencesAsync (newer API) for forward compatibility.

Library guidance recommends Async/WithCache variants over legacy API. This eases future migrations and aligns with package direction. Based on learnings

-    final prefs = await SharedPreferences.getInstance();
+    final prefs = await SharedPreferencesAsync.getInstance();
     final key = '$_keyPrefix$disputeId';
     final timestamp = DateTime.now().millisecondsSinceEpoch;
     await prefs.setInt(key, timestamp);
-    final prefs = await SharedPreferences.getInstance();
+    final prefs = await SharedPreferencesAsync.getInstance();
     final key = '$_keyPrefix$disputeId';
     return prefs.getInt(key);

Additionally, consider injecting a clock for testability:

typedef Now = int Function();
static Future<void> markDisputeAsRead(String disputeId, {Now now}) async {
  final prefs = await SharedPreferencesAsync.getInstance();
  final key = '$_keyPrefix$disputeId';
  final timestamp = (now ?? () => DateTime.now().millisecondsSinceEpoch)();
  await prefs.setInt(key, timestamp);
}

Please verify the project is on shared_preferences >= 2.3.0 before migrating.

lib/features/disputes/widgets/dispute_content.dart (2)

60-79: Model unread as a Riverpod FutureProvider instead of a FutureBuilder.

Keeps UI declarative and avoids recreating Futures in build. As per coding guidelines

Example:

final hasUnreadDisputeMessagesProvider = FutureProvider.family<bool, String>((ref, disputeId) async {
  final messages = ref.watch(disputeChatNotifierProvider(disputeId)).messages;
  return DisputeReadStatusService.hasUnreadMessages(disputeId, messages);
});

// In build:
final hasUnread = ref.watch(hasUnreadDisputeMessagesProvider(widget.dispute.disputeId));
if (normalizedStatus == 'in-progress') hasUnread.when(
  data: (v) => v ? _UnreadDot() : const SizedBox.shrink(),
  loading: () => const SizedBox.shrink(),
  error: (_, __) => const SizedBox.shrink(),
);

70-77: Add Semantics for the unread dot (accessibility).

Expose a short label for screen readers.

-                  return Container(
-                    width: 8,
-                    height: 8,
-                    decoration: const BoxDecoration(
-                      color: Colors.red,
-                      shape: BoxShape.circle,
-                    ),
-                  );
+                  return Semantics(
+                    label: 'Unread messages',
+                    child: Container(
+                      width: 8,
+                      height: 8,
+                      decoration: const BoxDecoration(
+                        color: Colors.red,
+                        shape: BoxShape.circle,
+                      ),
+                    ),
+                  );
lib/features/disputes/screens/dispute_chat_screen.dart (3)

30-39: Good post‑frame side‑effect. Consider also auto‑marking as read on new incoming messages.

Listen to chat messages while the screen is visible and refresh read‑status.

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    DisputeReadStatusService.markDisputeAsRead(widget.disputeId);
    ref.read(disputeReadStatusProvider(widget.disputeId).notifier).state =
        DateTime.now().millisecondsSinceEpoch;
  });

  // Mark as read when new messages arrive (admin -> user)
  ref.listen(
    disputeChatNotifierProvider(widget.disputeId).select((s) => s.messages.length),
    (prev, next) {
      DisputeReadStatusService.markDisputeAsRead(widget.disputeId);
      ref.read(disputeReadStatusProvider(widget.disputeId).notifier).state =
          DateTime.now().millisecondsSinceEpoch;
    },
  );
}

52-53: Remove hardcoded fallbacks; use localized strings only.

Aligns with project guideline to avoid hardcoded text. As per coding guidelines

-          S.of(context)?.disputeDetails ?? 'Dispute Details',
+          S.of(context).disputeDetails,
-                S.of(context)?.disputeNotFound ?? 'Dispute not found',
+                S.of(context).disputeNotFound,
-            S.of(context)?.errorLoadingDispute(error.toString()) ?? 'Error loading dispute: ${error.toString()}',
+            S.of(context).errorLoadingDispute(error.toString()),

Also applies to: 69-72, 100-106


111-223: Extract dispute→UI conversion into a provider/service for clarity and testability.

This logic is sizable, touches sessions/orders/roles/peers, and is easier to validate in isolation. Keep widgets lean and side‑effect free.

Sketch:

final disputeDataProvider = Provider.family<DisputeData, String>((ref, disputeId) {
  final dispute = ref.watch(disputeDetailsProvider(disputeId)).requireValue!;
  final sessions = ref.read(sessionNotifierProvider);
  // ...existing matching logic moved here...
  return resolvedDisputeData;
});

Then in build:

final disputeData = ref.watch(disputeDataProvider(widget.disputeId));
lib/features/disputes/widgets/dispute_messages_list.dart (2)

214-260: Consider passing messages instead of re-watching the provider.

The provider is already watched at line 68 (ref.watch(disputeChatNotifierProvider(widget.disputeId))), and then watched again at lines 224-225 within this builder. While Riverpod efficiently deduplicates watches, you could improve clarity by passing the messages list as a parameter to _buildAdminAssignmentNotification from the already-watched value.

For example:

Widget _buildAdminAssignmentNotification(BuildContext context, List<DisputeChat> messages) {
  // ... existing logic using messages parameter instead of re-watching
}

Then call it from line 80 and line 146:

_buildAdminAssignmentNotification(context, messages),

301-308: Hardcoded terminal states may need updates.

The _isResolvedStatus method hardcodes four terminal states: resolved, closed, solved, and seller-refunded. If the backend adds new terminal states in the future, this method will need to be updated. Consider whether these states should be centralized in a constants file or configuration.

For better maintainability:

// In a constants file:
class DisputeStatus {
  static const terminalStates = {'resolved', 'closed', 'solved', 'seller-refunded'};
  
  static bool isTerminal(String status) {
    final normalized = status.trim().toLowerCase().replaceAll(RegExp(r'[\s_]+'), '-');
    return terminalStates.contains(normalized);
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a7c71 and a175df6.

📒 Files selected for processing (6)
  • lib/features/disputes/providers/dispute_read_status_provider.dart (1 hunks)
  • lib/features/disputes/screens/dispute_chat_screen.dart (2 hunks)
  • lib/features/disputes/widgets/dispute_content.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_list_item.dart (2 hunks)
  • lib/features/disputes/widgets/dispute_messages_list.dart (6 hunks)
  • lib/services/dispute_read_status_service.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
lib/features/**/providers/**

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Riverpod providers by feature under features/{feature}/providers/

Files:

  • lib/features/disputes/providers/dispute_read_status_provider.dart
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/features/disputes/providers/dispute_read_status_provider.dart
  • lib/features/disputes/screens/dispute_chat_screen.dart
  • lib/features/disputes/widgets/dispute_messages_list.dart
  • lib/features/disputes/widgets/dispute_list_item.dart
  • lib/features/disputes/widgets/dispute_content.dart
  • lib/services/dispute_read_status_service.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/features/disputes/providers/dispute_read_status_provider.dart
  • lib/features/disputes/screens/dispute_chat_screen.dart
  • lib/features/disputes/widgets/dispute_messages_list.dart
  • lib/features/disputes/widgets/dispute_list_item.dart
  • lib/features/disputes/widgets/dispute_content.dart
  • lib/services/dispute_read_status_service.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/disputes/providers/dispute_read_status_provider.dart
  • lib/features/disputes/screens/dispute_chat_screen.dart
  • lib/features/disputes/widgets/dispute_messages_list.dart
  • lib/features/disputes/widgets/dispute_list_item.dart
  • lib/features/disputes/widgets/dispute_content.dart
lib/services/**

📄 CodeRabbit inference engine (AGENTS.md)

Place service-layer code in lib/services/

Files:

  • lib/services/dispute_read_status_service.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
lib/services/dispute_read_status_service.dart (1)

22-45: Unread logic looks correct (non‑user messages only).

Edge cases (no last read time, empty messages) are handled well.

lib/features/disputes/widgets/dispute_messages_list.dart (4)

11-26: LGTM! Proper Riverpod migration.

The migration from StatefulWidget to ConsumerStatefulWidget is correctly implemented, enabling direct access to Riverpod providers. The addition of an optional scrollController parameter provides good flexibility for parent widgets.


32-49: LGTM! Proper lifecycle management.

The scroll controller is correctly initialized (using provided or creating new) and disposed conditionally. The post-frame callback ensures scrolling occurs after the initial layout, and the conditional disposal prevents double-free errors.


131-168: LGTM! Well-structured empty state layout.

The use of LayoutBuilder, ConstrainedBox, and IntrinsicHeight ensures proper sizing and scrollability for empty states. The Spacer appropriately pushes the resolution notification to the bottom for resolved disputes.


338-393: LGTM! Clear and consistent UI components.

The chat-closed UI components use appropriate styling, localization, and visual indicators. The combination of lock icon with grey colors effectively communicates the closed state.

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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7febdec and 5029a4d.

📒 Files selected for processing (1)
  • lib/features/disputes/widgets/dispute_messages_list.dart (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
lib/features/disputes/widgets/dispute_messages_list.dart (2)

2-2: LGTM!

The Riverpod import and notifier import changes correctly support the widget's migration to a Riverpod-aware implementation.

Also applies to: 6-6


15-16: LGTM!

The migration to ConsumerStatefulWidget and ConsumerState follows the correct Riverpod pattern for stateful widgets that need to watch providers.

Also applies to: 31-31, 34-34

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: 0

🧹 Nitpick comments (3)
lib/features/disputes/widgets/dispute_messages_list.dart (3)

77-79: Make loading UI const.

Minor allocation win; both widgets support const.

-      return Center(
-        child: CircularProgressIndicator(),
-      );
+      return const Center(
+        child: CircularProgressIndicator(),
+      );

38-46: Auto-scroll on new messages (+ mounted check).

Add mounted check in the post‑frame callback and listen to message count to scroll when new messages arrive.

     // Scroll to bottom on first load
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      _scrollToBottom(animate: false);
+      if (!mounted) return;
+      _scrollToBottom(animate: false);
     });
+
+    // Auto-scroll when new messages arrive
+    ref.listen<int>(
+      disputeChatNotifierProvider(widget.disputeId).select((s) => s.messages.length),
+      (previous, next) {
+        if (previous == null || next > previous) {
+          if (!mounted) return;
+          WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
+        }
+      },
+    );

251-267: Prefer themed colors over hard-coded Colors. for consistency/dark-mode.*

Consider moving these to AppTheme to keep styling centralized and adaptable.

Also applies to: 359-371

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5029a4d and a203bc6.

📒 Files selected for processing (1)
  • lib/features/disputes/widgets/dispute_messages_list.dart (6 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/disputes/widgets/dispute_messages_list.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
lib/features/disputes/widgets/dispute_messages_list.dart (3)

72-92: LGTM: Provider state handling added correctly.

Clean loading/error branches with localized strings and themed styles. This prevents crashes and improves UX.


233-245: LGTM: Duplicate provider watch removed.

Passing messages into _buildAdminAssignmentNotification avoids extra ref.watch and rebuilds. Good cleanup.


143-151: LGTM: Simplified message indexing is correct.

Direct index offset is clear and aligns with _getItemCount contract.

Copy link
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.

En el chat solo se deben permitir mensajes que vengan del solver que tome la disputa, porque si no un estafador puede enviarle mensajes a los usuarios en disputa haciendose pasar por un admin.
En la captura de pantalla, el admin no habia tomado la disputa todavia, pero desde la contraparte le envie un gift wrap a este usuario y en el chat de disputa se le muestra:

Image

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: 2

🧹 Nitpick comments (1)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (1)

118-118: Declare Future<void> return type for async handler.

The method uses async/await but declares void return type. While Dart permits this, explicitly declaring Future<void> improves clarity and matches the async convention.

Apply this diff:

-  void _onChatEvent(NostrEvent event) async {
+  Future<void> _onChatEvent(NostrEvent event) async {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a203bc6 and b85721c.

📒 Files selected for processing (1)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
🔇 Additional comments (6)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (6)

19-42: LGTM: Clean state class implementation.

The immutable state class with copyWith follows best practices for Riverpod StateNotifier pattern.


90-115: Session hand-off now properly handled.

The listener now correctly watches sessionNotifierProvider for changes and retries subscription when a matching session appears, resolving the previously flagged issue where initialization would fail if the session arrived asynchronously.


145-187: LGTM: Dual-format message parsing.

The parser correctly handles both CLI format (with "dm" key) and legacy MostroMessage format, with appropriate error handling and null-safe navigation.


194-210: LGTM: Robust authorization checks.

The security validation properly enforces that admin messages must come from the assigned dispute.adminPubkey, with appropriate logging for audit trails.


436-466: LGTM: Defensive session lookup.

The method safely iterates through sessions with appropriate null checks and error handling. The per-session try-catch (lines 453-456) ensures that errors in one session don't prevent finding the match in others.


468-483: LGTM: Proper cleanup and provider setup.

The dispose() method correctly cancels both subscriptions, and the provider properly initializes each per-dispute instance.

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 (5)
lib/data/models/dispute_chat.dart (2)

66-71: Avoid double–int comparison for timestamp heuristic.

Use an int literal to prevent subtle numeric coercion.

-      int milliseconds = v < 1e12 ? v * 1000 : v;
+      // 1e12 ms ~= Sat Sep 09 2001; values below are likely seconds
+      final int milliseconds = v < 1000000000000 ? v * 1000 : v;

45-64: Consider equality semantics for list diffing and dedupe.

Override ==/hashCode (e.g., by id) or use Equatable to make UI updates and dedup predictable and cheaper. Optional, but helps when message lists grow.

lib/features/disputes/notifiers/dispute_chat_notifier.dart (3)

223-240: Persist admin_pubkey for inbound events (consistency).

You read admin_pubkey during historical load, but it’s never written here. Write it so rehydration is consistent.

         {
           'id': eventId,
           'content': messageText,
           'created_at': eventTimestamp.millisecondsSinceEpoch ~/ 1000,
           'kind': unwrappedEvent.kind,
           'pubkey': senderPubkey,
+          'admin_pubkey': isFromAdmin ? senderPubkey : null,
           'sig': unwrappedEvent.sig,
           'tags': unwrappedEvent.tags,
           'type': 'dispute_chat',
           'dispute_id': disputeId,
           'is_from_user': !isFromAdmin,
           'isPending': false,
         },

429-446: Persist admin_pubkey for outbound events (symmetry).

Store the assigned admin’s key alongside user-sent messages for uniform querying.

         {
           'id': rumorId,
           'content': text,
           'created_at': rumorTimestamp.millisecondsSinceEpoch ~/ 1000,
           'kind': rumor.kind,
           'pubkey': rumor.pubkey,
+          'admin_pubkey': dispute.adminPubkey,
           'sig': rumor.sig,
           'tags': rumor.tags,
           'type': 'dispute_chat',
           'dispute_id': disputeId,
           'is_from_user': true,
           'isPending': false,
         },

84-91: Optional: add a since to reduce backlog on subscribe.

To avoid replaying a large history, consider a since window (e.g., last 7–14 days) if acceptable for disputes.

       final request = NostrRequest(
         filters: [
           NostrFilter(
             kinds: [1059], // Gift Wrap
-            p: [session.tradeKey.public], // Messages to our tradeKey
+            p: [session.tradeKey.public], // or tags: {'p': [...]}
+            since: DateTime.now().subtract(const Duration(days: 14)).millisecondsSinceEpoch ~/ 1000,
           ),
         ],
       );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b85721c and dcecc3c.

📒 Files selected for processing (2)
  • lib/data/models/dispute_chat.dart (3 hunks)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/data/models/dispute_chat.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/data/models/dispute_chat.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
lib/data/**

📄 CodeRabbit inference engine (AGENTS.md)

Store persistence and API code under lib/data/

Files:

  • lib/data/models/dispute_chat.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
🔇 Additional comments (3)
lib/data/models/dispute_chat.dart (1)

8-19: Nice: pending/error state + copyWith added cleanly.

These additions enable clear UX for send/ack/failure flows and integrate well with state updates.

Also applies to: 45-64

lib/features/disputes/notifiers/dispute_chat_notifier.dart (2)

82-95: The code is correct as written—no changes needed.

dart_nostr's Filter has a dedicated property for pubkey tags (the "p" / #p filter), so using p: [session.tradeKey.public] directly is the proper approach. The suggested diff in the review comment is incorrect.

Likely an incorrect or invalid review comment.


269-279: No issues found—sort direction is correct.

The SortOrder constructor's second parameter is a boolean ascending flag: true (or omitted) sorts ascending, false sorts descending. Your code correctly uses SortOrder('created_at', true) to sort in ascending order, which for timestamps means oldest first, exactly as your comment states. No changes needed.

Comment on lines 420 to 427
// Publish to network
ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
_logger.i('Dispute message sent successfully to admin for dispute: $disputeId');

// Update message to isPending=false (success)
final sentMessage = pendingMessage.copyWith(isPending: false);
final updatedMessages = state.messages.map((m) => m.id == rumorId ? sentMessage : m).toList();
state = state.copyWith(messages: updatedMessages);
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 | 🔴 Critical

🧩 Analysis chain

Don’t mark sent before publish completes; await publish and ack.

publishEvent(wrappedEvent) isn’t awaited, so errors won’t be caught and isPending flips to false immediately. Await the publish and, if available, relay OK/ACK before clearing pending.

-      // Publish to network
-      ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
+      // Publish to network and await completion/ack
+      final nostr = ref.read(nostrServiceProvider);
+      await nostr.publishEvent(wrappedEvent); // if this returns Future
+      // If your service supports ACKs, prefer:
+      // final ok = await nostr.publishEventWithAck(wrappedEvent, timeout: const Duration(seconds: 5));
+      // if (!ok) throw Exception('Relay did not ACK publish');

Follow-up: only set isPending=false after the awaited success; on failure keep isPending=true or set error and offer retry. Based on learnings.


🏁 Script executed:

#!/bin/bash
# Verify publish API returns a Future and find an ACK-capable variant
rg -nP -C2 'class\s+.*NostrService|publishEvent\s*\(' --type=dart

Length of output: 5737


[/run_scripts]

Once the script results are provided, I'll examine the implementation to determine if:

  1. The publishEvent method can throw exceptions or propagate errors
  2. Whether errors from the underlying Nostr instance are handled/propagated
  3. Whether there's any relay ACK handling available

Then I'll be able to finalize the review comment.


🏁 Script executed:

#!/bin/bash
# Read full NostrService.publishEvent() implementation
cat lib/services/nostr_service.dart | sed -n '119,150p'

Length of output: 1147


Await publishEvent() to catch network errors; isPending flag change must follow successful publish.

publishEvent(wrappedEvent) is not awaited (lib/services/nostr_service.dart:119 confirms it returns Future<void>). Since publishEvent() throws on initialization failure, empty relays, or network errors, unawaiting it silently discards these exceptions. The isPending=false assignment happens immediately, marking the message as sent before the actual publish completes or fails.

Await the publish and handle errors appropriately:

-      // Publish to network
-      ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
-      _logger.i('Dispute message sent successfully to admin for dispute: $disputeId');
-
-      // Update message to isPending=false (success)
-      final sentMessage = pendingMessage.copyWith(isPending: false);
-      final updatedMessages = state.messages.map((m) => m.id == rumorId ? sentMessage : m).toList();
-      state = state.copyWith(messages: updatedMessages);
+      // Publish to network and await completion
+      try {
+        await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
+        _logger.i('Dispute message sent successfully to admin for dispute: $disputeId');
+        
+        // Update message to isPending=false only after successful publish
+        final sentMessage = pendingMessage.copyWith(isPending: false);
+        final updatedMessages = state.messages.map((m) => m.id == rumorId ? sentMessage : m).toList();
+        state = state.copyWith(messages: updatedMessages);
+      } catch (e, stackTrace) {
+        _logger.e('Failed to publish dispute message for dispute: $disputeId', error: e, stackTrace: stackTrace);
+        // Keep isPending=true or set error state; offer user retry option
+        rethrow;
+      }

Note: A similar unawaited call exists at lib/features/chat/notifiers/chat_room_notifier.dart:208 that should receive the same fix.

🤖 Prompt for AI Agents
In lib/features/disputes/notifiers/dispute_chat_notifier.dart around lines 420
to 427, publishEvent(wrappedEvent) is called without awaiting so
network/initialization errors are lost and the message is immediately marked
isPending=false; await ref.read(nostrServiceProvider).publishEvent(wrappedEvent)
and wrap it in try/catch, only set the message to isPending=false after the
await completes successfully, and on error log the exception and update the
message state to reflect failure (e.g., keep isPending=true or set an error
flag) so UI/state remains consistent; apply the same awaited+try/catch pattern
to the similar unawaited call in
lib/features/chat/notifiers/chat_room_notifier.dart around line 208.

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: 0

🧹 Nitpick comments (1)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (1)

528-558: Session lookup works correctly; consider optimization for many sessions.

The implementation correctly searches all sessions to find the one containing this dispute (lines 536-550). The current O(n) iteration is acceptable for typical use cases with few active sessions.

For scenarios with many concurrent sessions, consider maintaining a disputeId → sessionId index in a provider to avoid repeated iteration:

// In a separate provider
final disputeSessionIndexProvider = Provider<Map<String, String>>((ref) {
  final sessions = ref.watch(sessionNotifierProvider);
  final index = <String, String>{};
  for (final session in sessions) {
    if (session.orderId != null) {
      try {
        final order = ref.read(orderNotifierProvider(session.orderId!));
        if (order.dispute?.disputeId != null) {
          index[order.dispute!.disputeId!] = session.orderId!;
        }
      } catch (_) {}
    }
  }
  return index;
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcecc3c and 3134198.

📒 Files selected for processing (2)
  • lib/features/chat/notifiers/chat_room_notifier.dart (1 hunks)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use post-frame callbacks for UI side effects (e.g., SnackBars/dialogs)
Use S.of(context).keyName for all user-facing strings (no hardcoded text)
Always use localized strings instead of hardcoded text
Pass BuildContext to methods that need localization (use S.of(context))
Check mounted before using BuildContext after async gaps
Use const constructors where possible

Name Riverpod providers as Provider or Notifier

Files:

  • lib/features/chat/notifiers/chat_room_notifier.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: All code comments must be in English
Use clear, concise English for variable and function names
Maintain zero flutter analyze issues
Use latest Flutter/Dart APIs (e.g., prefer withValues() over withOpacity())
Remove unused imports and dependencies

**/*.dart: Use Dart formatter defaults (two-space indentation, trailing commas) and run flutter format before committing
Resolve all Flutter analyzer warnings (flutter analyze must be clean)

Files:

  • lib/features/chat/notifiers/chat_room_notifier.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
lib/features/**

📄 CodeRabbit inference engine (AGENTS.md)

Place application feature code under lib/features//, grouped by domain

Files:

  • lib/features/chat/notifiers/chat_room_notifier.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
🔇 Additional comments (10)
lib/features/chat/notifiers/chat_room_notifier.dart (2)

191-194: LGTM! Signature matches new Gift Wrap API.

The updated mostroWrap call correctly passes senderKeys and receiverPubkey parameters, aligning with the new NIP-59 Gift Wrap flow.


201-226: Good: Publish is now awaited and errors are handled.

The awaited publishEvent call with proper error handling addresses previous review feedback. The inner catch (lines 205-211) removes failed messages and rethrows, while the outer catch (lines 220-226) provides a final safety net for wrapping errors. The duplicate removal logic is harmless since the second filter is a no-op if the message was already removed.

lib/features/disputes/notifiers/dispute_chat_notifier.dart (8)

19-42: LGTM! Well-structured state class.

The DisputeChatState follows standard patterns with immutability, copyWith support, and const constructor.


66-95: Subscription leak fix confirmed.

The code now properly cancels the previous subscription before creating a new one (lines 76-80), addressing the prior review feedback.


97-122: Session hand-off now works correctly; consider error handling for unawaited subscribe.

The listener on sessionNotifierProvider (line 106) correctly watches for session availability and calls _subscribe() when found, addressing the previous critical review feedback. However, line 118 uses unawaited(_subscribe()), which means if _subscribe throws an exception (e.g., nostrService.subscribeToEvents fails), the error will be silently swallowed and the user won't receive messages.

Consider wrapping the unawaited call in a try-catch or using .catchError:

unawaited(_subscribe().catchError((error, stack) {
  _logger.e('Failed to subscribe after session available: $error', stackTrace: stack);
  // Optionally set state.error or retry
}));

124-261: LGTM! Solid security validation for incoming messages.

The event handler correctly unwraps Gift Wrap events, parses both CLI and legacy formats, and validates sender pubkeys (lines 201-217). The security check ensures admin messages originate from the expected dispute.adminPubkey, preventing impersonation.


263-337: LGTM! Historical messages properly filtered by authorized pubkeys.

The loader applies the same security validation as _onChatEvent (lines 293-310), ensuring only messages from the user or the assigned admin are displayed. Note that if an admin changes, messages from the previous admin will be filtered out—this appears intentional for security consistency.


420-460: Excellent: Publish is awaited with comprehensive error handling.

The awaited publishEvent (line 422) with proper error handling addresses previous critical review feedback. The implementation correctly:

  • Marks messages as isPending=false with error details on failure (lines 428-431)
  • Updates both state and storage to reflect the error
  • Returns early to prevent marking as success (line 459)

374-526: LGTM! Optimistic UI with proper rollback on failure.

The send flow now properly implements:

  • Optimistic update with isPending=true (line 380)
  • Awaited publish with error handling (lines 421-460)
  • Success confirmation with isPending=false (line 463)
  • Persistent error state in storage and UI (lines 438-458)

This addresses all previous review feedback about optimistic updates misleading users.


560-575: LGTM! Proper cleanup and provider initialization.

The dispose method correctly cancels subscriptions (lines 562-563), and the provider properly initializes the notifier (line 572).

Copy link
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.

tACK

@grunch grunch merged commit 973b017 into main Oct 21, 2025
2 checks passed
@grunch grunch deleted the andrea/dispute-chat branch October 21, 2025 18:01
@coderabbitai coderabbitai bot mentioned this pull request Oct 26, 2025
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Nov 10, 2025
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.

4 participants