Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions lib/data/models/dispute.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:mostro_mobile/data/models/order.dart';
enum DisputeDescriptionKey {
initiatedByUser, // You opened this dispute
initiatedByPeer, // A dispute was opened against you
initiatedPendingAdmin,// Dispute initiated, waiting for admin assignment
inProgress, // Dispute is being reviewed by an admin
resolved, // Dispute has been resolved
sellerRefunded, // Dispute resolved - seller refunded
Expand Down Expand Up @@ -375,8 +376,8 @@ class DisputeData {

return DisputeData(
disputeId: dispute.disputeId,
orderId: dispute.orderId, // No fallback to hardcoded string
status: dispute.status ?? 'unknown',
orderId: dispute.orderId ?? (orderState?.order?.id), // Use order from orderState if dispute.orderId is null
status: dispute.status ?? 'initiated',
descriptionKey: descriptionKey,
counterparty: counterpartyName,
isCreator: isUserCreator,
Expand Down Expand Up @@ -428,10 +429,10 @@ class DisputeData {
switch (status.toLowerCase()) {
case 'initiated':
if (isUserCreator == null) {
return DisputeDescriptionKey.unknown; // Unknown creator state
return DisputeDescriptionKey.initiatedPendingAdmin; // Waiting for admin assignment
}
return isUserCreator
? DisputeDescriptionKey.initiatedByUser
return isUserCreator
? DisputeDescriptionKey.initiatedByUser
: DisputeDescriptionKey.initiatedByPeer;
case 'in-progress':
return DisputeDescriptionKey.inProgress;
Expand All @@ -452,6 +453,8 @@ class DisputeData {
return 'You opened this dispute';
case DisputeDescriptionKey.initiatedByPeer:
return 'A dispute was opened against you';
case DisputeDescriptionKey.initiatedPendingAdmin:
return 'Waiting for admin assignment';
case DisputeDescriptionKey.inProgress:
return 'Dispute is being reviewed by an admin';
case DisputeDescriptionKey.resolved:
Expand Down
79 changes: 49 additions & 30 deletions lib/data/repositories/dispute_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:mostro_mobile/data/models/mostro_message.dart';
import 'package:mostro_mobile/data/models/enums/action.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart';
import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart';

/// Repository for managing dispute creation
class DisputeRepository {
Expand Down Expand Up @@ -63,39 +64,57 @@ class DisputeRepository {
}

Future<List<Dispute>> getUserDisputes() async {
// Mock implementation for UI testing
await Future.delayed(const Duration(milliseconds: 500));

return [
Dispute(
disputeId: 'dispute_001',
orderId: 'order_001',
status: 'initiated',
createdAt: DateTime.now().subtract(const Duration(hours: 1)),
action: 'dispute-initiated-by-you',
),
Dispute(
disputeId: 'dispute_002',
orderId: 'order_002',
status: 'in-progress',
createdAt: DateTime.now().subtract(const Duration(days: 1)),
action: 'dispute-initiated-by-peer',
adminPubkey: 'admin_123',
),
];
try {
_logger.d('Getting user disputes from sessions');

// Get all user sessions and check their order states for disputes
final sessions = _ref.read(sessionNotifierProvider);
final disputes = <Dispute>[];

for (final session in sessions) {
if (session.orderId != null) {
try {
// Get the order state for this session
final orderState = _ref.read(orderNotifierProvider(session.orderId!));

if (orderState.dispute != null) {
disputes.add(orderState.dispute!);
}
} catch (e) {
_logger.w('Failed to get order state for order ${session.orderId}: $e');
}
}
}

_logger.d('Found ${disputes.length} disputes from sessions');
return disputes;
} catch (e) {
_logger.e('Failed to get user disputes: $e');
return [];
}
}

Future<Dispute?> getDispute(String disputeId) async {
// Mock implementation
await Future.delayed(const Duration(milliseconds: 300));

return Dispute(
disputeId: disputeId,
orderId: 'order_${disputeId.substring(0, 8)}',
status: 'initiated',
createdAt: DateTime.now().subtract(const Duration(hours: 2)),
action: 'dispute-initiated-by-you',
);
try {
_logger.d('Getting dispute by ID: $disputeId');

// Get all user disputes and find the one with matching ID
final disputes = await getUserDisputes();
final dispute = disputes.firstWhereOrNull(
(d) => d.disputeId == disputeId,
);

if (dispute != null) {
_logger.d('Found dispute with ID: $disputeId');
} else {
_logger.w('No dispute found with ID: $disputeId');
}

return dispute;
} catch (e) {
_logger.e('Failed to get dispute by ID $disputeId: $e');
return null;
}
}

Future<void> sendDisputeMessage(String disputeId, String message) async {
Expand Down
90 changes: 68 additions & 22 deletions lib/features/chat/screens/chat_rooms_list.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// NostrEvent is now accessed through ChatRoom model
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mostro_mobile/core/app_theme.dart';
import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart';
Expand Down Expand Up @@ -51,8 +52,11 @@ class ChatRoomsScreen extends ConsumerWidget {
),
),
),
// Tab bar
ChatTabs(currentTab: currentTab),
// Tab bar - only show disputes tab in debug mode
if (kDebugMode)
ChatTabs(currentTab: currentTab)
else
_buildMessagesTabHeader(context),
// Description text
Container(
width: double.infinity,
Expand All @@ -66,7 +70,9 @@ class ChatRoomsScreen extends ConsumerWidget {
),
),
child: Text(
_getTabDescription(context, currentTab),
kDebugMode ? _getTabDescription(context, currentTab) :
S.of(context)?.conversationsDescription ??
'Here you\'ll find your conversations with other users during trades.',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
Expand All @@ -75,25 +81,30 @@ class ChatRoomsScreen extends ConsumerWidget {
),
// Content area with gesture detection
Expanded(
child: GestureDetector(
onHorizontalDragEnd: (details) {
if (details.primaryVelocity != null &&
details.primaryVelocity! < 0) {
// Swipe left - go to disputes
ref.read(chatTabProvider.notifier).state = ChatTabType.disputes;
} else if (details.primaryVelocity != null &&
details.primaryVelocity! > 0) {
// Swipe right - go to messages
ref.read(chatTabProvider.notifier).state = ChatTabType.messages;
}
},
child: Container(
color: AppTheme.backgroundDark,
child: currentTab == ChatTabType.messages
? _buildBody(context, ref)
: const DisputesList(),
),
),
child: kDebugMode
? GestureDetector(
onHorizontalDragEnd: (details) {
if (details.primaryVelocity != null &&
details.primaryVelocity! < 0) {
// Swipe left - go to disputes
ref.read(chatTabProvider.notifier).state = ChatTabType.disputes;
} else if (details.primaryVelocity != null &&
details.primaryVelocity! > 0) {
// Swipe right - go to messages
ref.read(chatTabProvider.notifier).state = ChatTabType.messages;
}
},
child: Container(
color: AppTheme.backgroundDark,
child: currentTab == ChatTabType.messages
? _buildBody(context, ref)
: const DisputesList(),
),
)
: Container(
color: AppTheme.backgroundDark,
child: _buildBody(context, ref),
),
),
// Add bottom padding to prevent content from being covered by BottomNavBar
SizedBox(
Expand Down Expand Up @@ -136,6 +147,41 @@ class ChatRoomsScreen extends ConsumerWidget {
);
}

Widget _buildMessagesTabHeader(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppTheme.backgroundDark,
border: Border(
bottom: BorderSide(
color: Colors.white.withValues(alpha: 0.1),
width: 1.0,
),
),
),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppTheme.mostroGreen,
width: 3.0,
),
),
),
child: Text(
S.of(context)!.messages,
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.mostroGreen,
fontWeight: FontWeight.w600,
fontSize: 15,
letterSpacing: 0.5,
),
),
),
);
}

String _getTabDescription(BuildContext context, ChatTabType currentTab) {
if (currentTab == ChatTabType.messages) {
// Messages tab
Expand Down
101 changes: 48 additions & 53 deletions lib/features/disputes/providers/dispute_providers.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mostro_mobile/data/models/dispute.dart';
import 'package:mostro_mobile/data/models/dispute_chat.dart';
import 'package:mostro_mobile/data/models/session.dart';
import 'package:mostro_mobile/data/repositories/dispute_repository.dart';
import 'package:mostro_mobile/features/disputes/notifiers/dispute_chat_notifier.dart';
import 'package:mostro_mobile/features/disputes/data/dispute_mock_data.dart';
import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart';
import 'package:mostro_mobile/features/settings/settings_provider.dart';
import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart';
import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart';

/// Provider for the dispute repository
final disputeRepositoryProvider = Provider.autoDispose<DisputeRepository>((ref) {
Expand All @@ -16,43 +18,12 @@ final disputeRepositoryProvider = Provider.autoDispose<DisputeRepository>((ref)
return DisputeRepository(nostrService, mostroPubkey, ref);
});

/// Provider for dispute details - uses mock data when enabled
/// Provider for dispute details - uses real data from repository
final disputeDetailsProvider = FutureProvider.family<Dispute?, String>((ref, disputeId) async {
// Simulate loading time
await Future.delayed(const Duration(milliseconds: 300));

if (!DisputeMockData.isMockEnabled) {
// TODO: Implement real dispute loading here
return null;
}

// Get mock dispute data
final disputeData = DisputeMockData.getDisputeById(disputeId);
if (disputeData == null) return null;

return Dispute(
disputeId: disputeData.disputeId,
orderId: disputeData.orderId,
status: disputeData.status,
createdAt: disputeData.createdAt,
action: _getActionFromStatus(disputeData.status, disputeData.userRole.name),
adminPubkey: disputeData.status != 'initiated' ? 'admin_123' : null,
);
final repository = ref.watch(disputeRepositoryProvider);
return repository.getDispute(disputeId);
});

/// Helper function to convert status to action
String _getActionFromStatus(String status, String initiatorRole) {
switch (status) {
case 'initiated':
return 'dispute-initiated-by-you';
case 'in-progress':
return initiatorRole == 'buyer' ? 'dispute-initiated-by-you' : 'dispute-initiated-by-peer';
case 'resolved':
return 'dispute-resolved';
default:
return 'dispute-initiated-by-you';
}
}

/// Stub provider for dispute chat messages - UI only implementation
final disputeChatProvider = StateNotifierProvider.family<DisputeChatNotifier, List<DisputeChat>, String>(
Expand All @@ -61,23 +32,47 @@ final disputeChatProvider = StateNotifierProvider.family<DisputeChatNotifier, Li
},
);

/// Provider for user disputes list - uses mock data when enabled
/// Provider for user disputes list - uses real data from repository
final userDisputesProvider = FutureProvider<List<Dispute>>((ref) async {
// Simulate loading time
await Future.delayed(const Duration(milliseconds: 500));

if (!DisputeMockData.isMockEnabled) {
// TODO: Implement real disputes loading here
return [];
}

// Convert mock dispute data to Dispute objects
return DisputeMockData.mockDisputes.map((disputeData) => Dispute(
disputeId: disputeData.disputeId,
orderId: disputeData.orderId,
status: disputeData.status,
createdAt: disputeData.createdAt,
action: _getActionFromStatus(disputeData.status, disputeData.userRole.name),
adminPubkey: disputeData.status != 'initiated' ? 'admin_123' : null,
)).toList();
final repository = ref.watch(disputeRepositoryProvider);
return repository.getUserDisputes();
});

/// Provider for user disputes as DisputeData (UI view models)
final userDisputeDataProvider = FutureProvider<List<DisputeData>>((ref) async {
final disputes = await ref.watch(userDisputesProvider.future);
final sessions = ref.read(sessionNotifierProvider);

return disputes.map((dispute) {
// Find the specific session for this dispute's order
Session? matchingSession;
dynamic matchingOrderState;

// Try to find the session and order state that contains this dispute
for (final session in sessions) {
if (session.orderId != null) {
try {
final orderState = ref.read(orderNotifierProvider(session.orderId!));

// Check if this order state contains our dispute
if (orderState.dispute?.disputeId == dispute.disputeId) {
matchingSession = session;
matchingOrderState = orderState;
break;
}
} catch (e) {
// Continue checking other sessions
continue;
}
}
}

// If we found matching order state, use it for context
if (matchingSession != null && matchingOrderState != null) {
return DisputeData.fromDispute(dispute, orderState: matchingOrderState);
}

// Fallback: create DisputeData without order context
return DisputeData.fromDispute(dispute);
}).toList();
});
2 changes: 2 additions & 0 deletions lib/features/disputes/widgets/dispute_status_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class DisputeStatusContent extends StatelessWidget {
return S.of(context)!.disputeOpenedByYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.initiatedByPeer:
return S.of(context)!.disputeOpenedAgainstYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.initiatedPendingAdmin:
return S.of(context)!.disputeWaitingForAdmin;
case DisputeDescriptionKey.inProgress:
// Use status text with a descriptive message
return "${S.of(context)!.disputeStatusInProgress}: ${dispute.description}";
Expand Down
Loading