// Haveno App extends the features of Haveno, supporting mobile devices and more. // Copyright (C) 2024 Kewbit (https://kewbit.org) // Source Code: https://git.haveno.com/haveno/haveno-app.git // // Author: Kewbit // Website: https://kewbit.org // Contact Email: me@kewbit.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import 'dart:async'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/widgets.dart'; import 'package:haveno/haveno_client.dart'; import 'package:haveno/haveno_service.dart'; import 'package:haveno/profobuf_models.dart'; class DisputesProvider with ChangeNotifier { final HavenoChannel _havenoChannel = HavenoChannel(); List _disputes = []; // Map to hold StreamControllers for each chat final Map>> _chatControllers = {}; // Map to store unique chat messages for each dispute final Map> _chatMessages = {}; // Map to store tradeId to disputeId mapping final Map _disputeToTradeIdMap = {}; // TradeID to dispute map final Map _tradeIdToDisputeMap = {}; DisputesProvider(); //: super(const Duration(minutes: 1)); @override void dispose() { // Close all StreamControllers when the provider is disposed _chatControllers.forEach((_, controller) => controller.close()); super.dispose(); } // Method to get or create a StreamController for a specific chat Stream> chatMessagesStream(String disputeId) { if (!_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId] = StreamController>.broadcast(); // Start polling for new messages (if needed) _startPolling(disputeId); } return _chatControllers[disputeId]!.stream; } // Load initial messages independently of stream creation Future loadInitialMessages(String disputeId) async { // Retrieve the tradeId from the mapping final tradeId = _disputeToTradeIdMap[disputeId]; if (tradeId == null) return; // Retrieve the dispute from the _disputes list using the disputeId Dispute? dispute = _disputes.firstWhere((d) => d!.id == disputeId, orElse: () => null); if (dispute != null) { print("Setting chat messages for dispute: $disputeId with ${dispute.chatMessage.length} messages"); // Store the chat messages in _chatMessages for this disputeId _chatMessages[disputeId] = dispute.chatMessage; // If a stream controller exists for this disputeId, add the messages to the stream if (_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); } } else { // Handle case where no dispute is found print("No dispute found for ID: $disputeId"); _chatMessages[disputeId] = []; } } List getInitialChatMessages(String disputeId) { print("Getting initial chat messages for dispute: $disputeId"); return _chatMessages[disputeId] ?? []; } /// Probsblt just redo the polling when not tired void _startPolling(String disputeId) { Timer.periodic(const Duration(minutes: 2), (timer) async { print("POLLIN FROM DISPUTES PROVIER"); // Get the tradeId from the mapping using disputeId final tradeId = _disputeToTradeIdMap[disputeId]; if (tradeId == null) { print("No tradeId found for disputeId: $disputeId"); return; } Dispute? dispute = await getDispute(tradeId); if (dispute != null && dispute.id == disputeId) { for (var message in dispute.chatMessage) { if (message.senderIsTrader) { return; } if (!_chatMessages[disputeId]!.contains(message)) { _chatMessages[disputeId]!.add(message); if (_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); } } } } }); } void debugPrintTradeIdToDisputeMap() { if (_tradeIdToDisputeMap.isEmpty) { print("The _tradeIdToDisputeMap is currently empty."); } else { //_tradeIdToDisputeMap.forEach((tradeId, dispute) { //print('Trade ID: $tradeId, Dispute ID: ${dispute.id}'); //}); } } Dispute? getDisputeByTradeId(String tradeId) { print("Attempting to retrieve dispute for Trade ID: $tradeId"); if (_tradeIdToDisputeMap.containsKey(tradeId)) { print("Dispute found for Trade ID: $tradeId"); debugPrintTradeIdToDisputeMap(); // Print the entire map for debugging return _tradeIdToDisputeMap[tradeId]; } else { print("No dispute found for Trade ID: $tradeId"); //debugPrintTradeIdToDisputeMap(); // Print the entire map for debugging return null; } } Future?> getDisputes() async { List disputes = []; // Attempt to retrieve disputes from the service try { var disputeClient = DisputeService(); disputes = await disputeClient.getDisputes(); } catch (e) { return null; } // Extract the list of disputes List disputesList = disputes; // Check if the disputes list is empty if (disputesList.isEmpty) { print("No disputes found."); } else { // Iterate through each dispute and map the tradeId to the dispute for (var dispute in disputesList) { _disputeToTradeIdMap[dispute.id] = dispute.tradeId; _tradeIdToDisputeMap[dispute.tradeId] = dispute; // Debugging output to verify the mapping print("Mapping added: Trade ID ${dispute.tradeId} -> Dispute ID ${dispute.id}"); print("Current _tradeIdToDisputeMap contents:"); _tradeIdToDisputeMap.forEach((tradeId, mappedDispute) { print("Trade ID: $tradeId, Dispute ID: ${mappedDispute.id}"); }); } } // Assign disputes to the internal list and notify listeners _disputes = disputesList; notifyListeners(); return null; } Future getDispute(String tradeId) async { try { await _havenoChannel.onConnected; Dispute? dispute; var disputeClient = DisputeService(); dispute = await disputeClient.getDispute(tradeId); Dispute? newDispute = dispute; if (newDispute != null) { _disputeToTradeIdMap[newDispute.id] = tradeId; _tradeIdToDisputeMap[newDispute.tradeId] = newDispute; // Update the _disputes list or map if necessary final int existingIndex = _disputes.indexWhere((d) => d!.id == newDispute.id); if (existingIndex != -1) { _disputes[existingIndex] = newDispute; } else { _disputes.add(newDispute); } notifyListeners(); return newDispute; } return null; } catch (e) { print("Failed to get dispute: $e"); return null; } } Future resolveDispute(String tradeId, DisputeResult_Winner? winner, DisputeResult_Reason? reason, String? summaryNotes, Int64? customPayoutAmount) async { await _havenoChannel.onConnected; Dispute? dispute = await getDispute(tradeId); if (!dispute!.isOpener) { throw Exception("You can't close a dispute you didn't open!"); } try { DisputeService().resolveDispute( tradeId, winner, reason, summaryNotes, customPayoutAmount, ); getDisputes(); } catch (e) { print("Failed to resolve dispute: $e"); rethrow; } } Future openDispute(String tradeId) async { await _havenoChannel.onConnected; try { await DisputeService().openDispute(tradeId); await getDisputes(); Dispute? dispute = _disputes.firstWhere((d) => d!.tradeId == tradeId, orElse: () => null); return dispute; } catch (e) { print("Failed to open dispute: $e"); rethrow; } } Future sendDisputeChatMessage(String disputeId, String message, Iterable attachments) async { await _havenoChannel.onConnected; try { await DisputeService().sendDisputeChatMessage( disputeId, message, attachments, ); // Might need to create a fake protobuf message and add it to the stream controller just to conform (so the message appears instantly) } catch (e) { print("Failed to send dispute chat message: $e"); rethrow; } } // Optional: Method to close a specific chat stream void closeChat(String disputeId) { if (_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId]!.close(); _chatControllers.remove(disputeId); _chatMessages.remove(disputeId); _disputeToTradeIdMap.remove(disputeId); } } void addChatMessage(ChatMessage chatMessage) { // do something special!!! } // @override // Future pollAction() async { // getDisputes(); // } } class DisputeChatProvider with ChangeNotifier { final HavenoChannel _havenoChannel; // Map to store StreamControllers for each chat final Map>> _chatControllers = {}; // Map to store chat messages final Map> _chatMessages = {}; DisputeChatProvider(this._havenoChannel); // Get or create a StreamController for specific chat Stream> chatMessagesStream(String disputeId) { if (!_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId] = StreamController>.broadcast(); // Load initial messages and start polling if needed _startPolling(disputeId); } return _chatControllers[disputeId]!.stream; } // Load initial messages for the dispute Future loadInitialMessages(String disputeId, Dispute dispute) async { _chatMessages[disputeId] = dispute.chatMessage; if (_chatControllers.containsKey(disputeId)) { _chatControllers[disputeId]!.add(_chatMessages[disputeId]!); } } // Add a new message void addChatMessage(String disputeId, ChatMessage message) { if (!_chatMessages.containsKey(disputeId)) { _chatMessages[disputeId] = []; } _chatMessages[disputeId]!.add(message); _chatControllers[disputeId]?.add(_chatMessages[disputeId]!); } Future sendDisputeChatMessage(String disputeId, String message, Iterable attachments) async { await _havenoChannel.onConnected; try { await DisputeService().sendDisputeChatMessage( disputeId, message, attachments, ); } catch (e) { print("Failed to send dispute chat message: $e"); rethrow; } } void _startPolling(String disputeId) { Timer.periodic(const Duration(minutes: 2), (timer) async { // Implement polling logic to fetch new messages periodically print("Polling for new messages for dispute: $disputeId"); // Fetch dispute or chat messages and update the stream }); } @override void dispose() { _chatControllers.forEach((_, controller) => controller.close()); super.dispose(); } }