Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
90619cc
Feat: add subscription pooling to MostroService
chebizarro Jun 12, 2025
bcc0248
refactor: simplify order state management and remove unused order act…
chebizarro Jun 13, 2025
b78a077
feat: implement subscription manager for centralized Nostr event hand…
chebizarro Jun 17, 2025
eda3d92
Bug when selecting the premium or discount for an order
chebizarro Jun 19, 2025
4832686
refactor: redesign trades list UI and improve order state management
chebizarro Jun 24, 2025
87e95ee
refactor: revamp subscription management with session-based subscript…
chebizarro Jun 24, 2025
f95e669
refactor: migrate to centralized subscription management system using…
chebizarro Jun 25, 2025
197ca96
refactor: streamline subscription management and improve navigation flow
chebizarro Jun 26, 2025
de9a0c3
refactor: remove trades subscription type and related functionality
chebizarro Jun 26, 2025
cbf3ae1
Reopen the app to update the status of an order and chat messages.
chebizarro Jun 26, 2025
3e98534
refactor: update mock implementations and add SubscriptionManager moc…
chebizarro Jun 26, 2025
83450c4
feat: add DM action to order states and update trade detail screen ac…
chebizarro Jun 26, 2025
2ddcdfb
refactor: migrate to automatic subscription management using Subscrip…
chebizarro Jun 26, 2025
8c2d58a
refactor: simplify subscription filter logic and remove unused subscr…
chebizarro Jun 26, 2025
3764000
refactor: update mock subscription manager with proper stream handlin…
chebizarro Jun 26, 2025
71834ff
fix: prevent duplicate events in chat and handle unix timestamps corr…
chebizarro Jul 2, 2025
9aeb5f7
Merge branch 'main' into chebizarro/issue57
chebizarro Jul 3, 2025
b285968
refactor: add session-based action validation to reactive button widget
chebizarro Jul 4, 2025
e568960
Reopen the app to update the status of an order and chat messages.
chebizarro Jul 4, 2025
7b65ede
refactor: clean up notification service by removing unused code and c…
chebizarro Jul 7, 2025
16d8e4b
Merge branch 'main' into chebizarro/issue57
chebizarro Jul 7, 2025
3b0873d
refactor: enhance model validation and error handling across data models
chebizarro Jul 7, 2025
cf07cf2
chore: upgrade go_router to v16 and share_plus to v10
chebizarro Jul 8, 2025
a218df3
refactor: improve session validation and error handling in models and…
chebizarro Jul 8, 2025
48939f7
Merge branch 'main' into chebizarro/issue57
chebizarro Jul 8, 2025
92ed140
Reopen the app to update the status of an order and chat messages.
chebizarro Jul 8, 2025
b003bee
refactor: standardize button text keys across language files
chebizarro Jul 8, 2025
c71927d
refactor: simplify event storage and optimize trade filtering logic
chebizarro Jul 9, 2025
455f0de
chore: exclude generated files from analysis and add logging for DM e…
chebizarro Jul 9, 2025
c6cdeb1
fix: handle empty sessions and missing shared keys in subscription fi…
chebizarro Jul 9, 2025
efe3e4c
feat: unsubscribe all active subscriptions when app enters background…
chebizarro Jul 10, 2025
2a55d83
Reopen the app to update the status of an order and chat messages.
chebizarro Jul 10, 2025
c2d6b16
feat: add fetchEvents method to NostrService for async event retrieval
chebizarro Jul 10, 2025
7c447a8
merge upstream changes
chebizarro Jul 15, 2025
77c7d14
chore: remove unused order-related translations from English locale file
chebizarro Jul 15, 2025
150ea82
refactor: standardize done button text and remove unused wallet butto…
chebizarro Jul 15, 2025
82183d5
merge main
chebizarro Jul 21, 2025
950c6c1
fix: update button labels to use shorter translation keys
chebizarro Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ web/.packages
web/build/

# Windows
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.*
windows/flutter/generated_plugins.cmake
windows/.flutter-plugins
windows/.flutter-plugins-dependencies

# Linux
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.*
linux/flutter/generated_plugins.cmake
linux/.flutter-plugins
linux/.flutter-plugins-dependencies
Expand Down
8 changes: 8 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- "**/*.mocks.dart"
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.gr.dart"
- "**/*.config.dart"

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
Expand Down
15 changes: 7 additions & 8 deletions integration_test/test_helpers.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dart:async';
import 'package:dart_nostr/nostr/model/request/filter.dart';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
Expand Down Expand Up @@ -268,13 +268,7 @@ class FakeMostroService implements MostroService {
final Ref ref;

@override
void init() {}

@override
void subscribe(Session session) {}

@override
Session? getSessionByOrderId(String orderId) => null;
void init({List<NostrKeyPairs>? keys}) {}

@override
Future<void> submitOrder(MostroMessage order) async {
Expand Down Expand Up @@ -319,6 +313,11 @@ class FakeMostroService implements MostroService {

@override
void updateSettings(Settings settings) {}

@override
void dispose() {
// TODO: implement dispose
}
}

Future<void> pumpTestApp(WidgetTester tester) async {
Expand Down
13 changes: 7 additions & 6 deletions lib/background/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/nostr_filter.dart';
import 'package:mostro_mobile/data/repositories/event_storage.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
Expand All @@ -13,10 +14,6 @@ bool isAppForeground = true;

@pragma('vm:entry-point')
Future<void> serviceMain(ServiceInstance service) async {
// If on Android, set up a permanent notification so the OS won't kill it.
if (service is AndroidServiceInstance) {
service.setAsForegroundService();
}

final Map<String, Map<String, dynamic>> activeSubscriptions = {};
final nostrService = NostrService();
Expand Down Expand Up @@ -68,8 +65,12 @@ Future<void> serviceMain(ServiceInstance service) async {
};

subscription.listen((event) async {
if (await eventStore.hasItem(event.id!)) return;
await retryNotification(event);
try {
if (await eventStore.hasItem(event.id!)) return;
await retryNotification(event);
} catch (e) {
Logger().e('Error processing event', error: e);
}
});
});

Expand Down
8 changes: 0 additions & 8 deletions lib/background/desktop_background_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/nostr_filter.dart';
import 'package:mostro_mobile/data/repositories.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';
import 'abstract_background_service.dart';

class DesktopBackgroundService implements BackgroundService {
Expand All @@ -30,8 +28,6 @@ class DesktopBackgroundService implements BackgroundService {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);

final nostrService = NostrService();
final db = await openMostroDatabase('events.db');
final backgroundStorage = EventStorage(db: db);
final logger = Logger();
bool isAppForeground = true;

Expand Down Expand Up @@ -67,10 +63,6 @@ class DesktopBackgroundService implements BackgroundService {

final subscription = nostrService.subscribeToEvents(request);
subscription.listen((event) async {
await backgroundStorage.putItem(
event.id!,
event,
);
mainSendPort.send({
'event': event.toMap(),
});
Expand Down
6 changes: 3 additions & 3 deletions lib/background/mobile_background_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ class MobileBackgroundService implements BackgroundService {
Future<void> init() async {
await service.configure(
iosConfiguration: IosConfiguration(
autoStart: true,
autoStart: false,
onForeground: serviceMain,
onBackground: onIosBackground,
),
androidConfiguration: AndroidConfiguration(
autoStart: false,
onStart: serviceMain,
isForegroundMode: true,
autoStartOnBoot: true,
isForegroundMode: false,
autoStartOnBoot: false,
initialNotificationTitle: "Mostro",
initialNotificationContent: "Connected to Mostro service",
foregroundServiceTypes: [
Expand Down
7 changes: 5 additions & 2 deletions lib/core/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ class Config {
// Versión de Mostro
static int mostroVersion = 1;

static int expirationSeconds = 900;
static int expirationHours = 24;
static const int expirationSeconds = 900;
static const int expirationHours = 24;
static const int cleanupIntervalMinutes = 30;
static const int sessionExpirationHours = 36;


// Configuración de notificaciones
static String notificationChannelId = 'mostro_mobile';
Expand Down
53 changes: 52 additions & 1 deletion lib/data/models/amount.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import 'package:mostro_mobile/data/models/payload.dart';
class Amount implements Payload {
final int amount;

Amount({required this.amount});
Amount({required this.amount}) {
if (amount < 0) {
throw ArgumentError('Amount cannot be negative: $amount');
}
}

@override
Map<String, dynamic> toJson() {
Expand All @@ -12,6 +16,53 @@ class Amount implements Payload {
};
}

factory Amount.fromJson(dynamic json) {
try {
if (json == null) {
throw FormatException('Amount JSON cannot be null');
}

int amountValue;
if (json is Map<String, dynamic>) {
if (!json.containsKey('amount')) {
throw FormatException('Missing required field: amount');
}
final value = json['amount'];
if (value is int) {
amountValue = value;
} else if (value is String) {
amountValue = int.tryParse(value) ??
(throw FormatException('Invalid amount format: $value'));
} else {
throw FormatException('Invalid amount type: ${value.runtimeType}');
}
} else if (json is int) {
amountValue = json;
} else if (json is String) {
amountValue = int.tryParse(json) ??
(throw FormatException('Invalid amount format: $json'));
} else {
throw FormatException('Invalid JSON type for Amount: ${json.runtimeType}');
}

return Amount(amount: amountValue);
} catch (e) {
throw FormatException('Failed to parse Amount from JSON: $e');
}
}

@override
String get type => 'amount';

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Amount && other.amount == amount;
}

@override
int get hashCode => amount.hashCode;

@override
String toString() => 'Amount(amount: $amount)';
}
52 changes: 40 additions & 12 deletions lib/data/models/cant_do.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,40 @@ import 'package:mostro_mobile/data/models/payload.dart';
class CantDo implements Payload {
final CantDoReason cantDoReason;

CantDo({required this.cantDoReason});

factory CantDo.fromJson(Map<String, dynamic> json) {
if (json['cant_do'] is String) {
return CantDo(
cantDoReason: CantDoReason.fromString(
json['cant_do'],
),
);
} else {
try {
final cantDoValue = json['cant_do'];
if (cantDoValue == null) {
throw FormatException('Missing required field: cant_do');
}

String reasonString;
if (cantDoValue is String) {
reasonString = cantDoValue;
} else if (cantDoValue is Map<String, dynamic>) {
final cantDoReason = cantDoValue['cant-do'];
if (cantDoReason == null) {
throw FormatException('Missing required field: cant-do in cant_do object');
}
reasonString = cantDoReason.toString();
} else {
throw FormatException('Invalid cant_do type: ${cantDoValue.runtimeType}');
}
Comment on lines +10 to +27
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent key naming between 'cant_do' and 'cant-do'.

The implementation expects cant_do as the top-level key (line 11) but looks for cant-do within a nested object (line 20). This inconsistency could lead to parsing failures depending on the JSON structure.

Consider standardizing on one key format:

       } else if (cantDoValue is Map<String, dynamic>) {
-        final cantDoReason = cantDoValue['cant-do'];
+        final cantDoReason = cantDoValue['cant_do'];
         if (cantDoReason == null) {
-          throw FormatException('Missing required field: cant-do in cant_do object');
+          throw FormatException('Missing required field: cant_do in nested object');
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
final cantDoValue = json['cant_do'];
if (cantDoValue == null) {
throw FormatException('Missing required field: cant_do');
}
String reasonString;
if (cantDoValue is String) {
reasonString = cantDoValue;
} else if (cantDoValue is Map<String, dynamic>) {
final cantDoReason = cantDoValue['cant-do'];
if (cantDoReason == null) {
throw FormatException('Missing required field: cant-do in cant_do object');
}
reasonString = cantDoReason.toString();
} else {
throw FormatException('Invalid cant_do type: ${cantDoValue.runtimeType}');
}
try {
final cantDoValue = json['cant_do'];
if (cantDoValue == null) {
throw FormatException('Missing required field: cant_do');
}
String reasonString;
if (cantDoValue is String) {
reasonString = cantDoValue;
} else if (cantDoValue is Map<String, dynamic>) {
final cantDoReason = cantDoValue['cant_do'];
if (cantDoReason == null) {
throw FormatException('Missing required field: cant_do in nested object');
}
reasonString = cantDoReason.toString();
} else {
throw FormatException('Invalid cant_do type: ${cantDoValue.runtimeType}');
}
🤖 Prompt for AI Agents
In lib/data/models/cant_do.dart between lines 10 and 27, the code inconsistently
uses 'cant_do' as the top-level JSON key and 'cant-do' as the nested key, which
can cause parsing errors. To fix this, standardize the key naming by using the
same format (either 'cant_do' or 'cant-do') consistently for both the top-level
and nested keys throughout the code. Update the key access accordingly to match
the chosen format.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This inconsistency exists at the protocol level so changing to one key format will break message parsing

Copy link
Contributor

Choose a reason for hiding this comment

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

@chebizarro Thank you for the clarification! You're absolutely right - I should not have suggested changing the key naming since it would break protocol compliance. The inconsistency between cant_do and cant-do is clearly an intentional part of the protocol specification, not a code issue.

I withdraw my suggestion as it would introduce a breaking change that would make the implementation incompatible with the protocol.

(´。• ◡ •。`) ♡


✏️ Learnings added
Learnt from: chebizarro
PR: MostroP2P/mobile#127
File: lib/data/models/cant_do.dart:10-27
Timestamp: 2025-07-08T05:40:47.506Z
Learning: In the CantDo model parsing in lib/data/models/cant_do.dart, the inconsistent key naming between 'cant_do' (top-level) and 'cant-do' (nested) is required by the protocol specification and should not be changed as it would break message parsing compatibility.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: chebizarro
PR: MostroP2P/mobile#127
File: lib/features/order/notfiers/abstract_mostro_notifier.dart:45-54
Timestamp: 2025-06-26T15:03:23.529Z
Learning: In AbstractMostroNotifier, state updates occur for all messages regardless of timestamp to hydrate the OrderNotifier from MostroStorage during sync, while handleEvent is only called for recent messages (within 60 seconds) to prevent re-triggering side effects like notifications and navigation for previously handled messages. This design prevents displaying stale notifications when the app is reopened or brought to the foreground.


if (reasonString.isEmpty) {
throw FormatException('CantDo reason cannot be empty');
}

return CantDo(
cantDoReason: CantDoReason.fromString(
json['cant_do']['cant-do'],
),
cantDoReason: CantDoReason.fromString(reasonString),
);
} catch (e) {
throw FormatException('Failed to parse CantDo from JSON: $e');
}
}

CantDo({required this.cantDoReason});

@override
Map<String, dynamic> toJson() {
return {
Expand All @@ -33,4 +49,16 @@ class CantDo implements Payload {

@override
String get type => 'cant_do';

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CantDo && other.cantDoReason == cantDoReason;
}

@override
int get hashCode => cantDoReason.hashCode;

@override
String toString() => 'CantDo(cantDoReason: $cantDoReason)';
}
14 changes: 0 additions & 14 deletions lib/data/models/chat_model.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
class ChatModel {
final String id;
final String username;
final String lastMessage;
final String timeAgo;
final bool isUnread;

ChatModel({
required this.id,
required this.username,
required this.lastMessage,
required this.timeAgo,
this.isUnread = false,
});
}
25 changes: 25 additions & 0 deletions lib/data/models/chat_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class ChatRoom {
final List<NostrEvent> messages;

ChatRoom({required this.orderId, required this.messages}) {
if (orderId.isEmpty) {
throw ArgumentError('Order ID cannot be empty');
}
messages.sort((a, b) => a.createdAt!.compareTo(b.createdAt!));
}

Expand All @@ -16,4 +19,26 @@ class ChatRoom {
messages: messages ?? this.messages,
);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatRoom &&
other.orderId == orderId &&
_listEquals(other.messages, messages);
}

bool _listEquals<T>(List<T> a, List<T> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}

@override
int get hashCode => Object.hash(orderId, Object.hashAll(messages));

@override
String toString() => 'ChatRoom(orderId: $orderId, messages: ${messages.length} messages)';
}
Loading