// ignore_for_file: use_build_context_synchronously import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/models/chat_models/chat_message_model.dart'; import 'package:mc_common_app/models/general_models/generic_resp_model.dart'; import 'package:mc_common_app/models/requests_models/provider_offers_model.dart'; import 'package:mc_common_app/repositories/chat_repo.dart'; import 'package:mc_common_app/repositories/request_repo.dart'; import 'package:mc_common_app/services/common_services.dart'; import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:mc_common_app/view_models/base_view_model.dart'; import 'package:mc_common_app/view_models/requests_view_model.dart'; import 'package:provider/provider.dart'; import 'package:signalr_core/signalr_core.dart'; class ChatVM extends BaseVM { final ChatRepo chatRepo; final RequestRepo requestRepo; final CommonAppServices commonServices; ChatVM({required this.chatRepo, required this.requestRepo, required this.commonServices}); HubConnection? hubConnection; bool _isSubscribed = false; // Track if listeners are subscribed // Store ViewModels to avoid context dependency RequestsVM? _requestsVM; AdVM? _adVM; // Track if a message is currently being sent (prevents duplicate sends) bool isSendingMessage = false; // Method to inject ViewModels (called once from Dashboard) void setViewModels({required RequestsVM requestsVM, required AdVM adVM}) { _requestsVM = requestsVM; _adVM = adVM; logger.i("ViewModels injected into ChatVM"); } String chatMessageText = ""; updateChatMessageText(String value) { chatMessageText = value; } clearChatMessageText() { chatMessageText = ""; notifyListeners(); } int latestOfferId = 0; updateLatestOfferId(var value) { latestOfferId = value; notifyListeners(); } bool acknowledgePaymentToMowaterStatus = false; updateAcknowledgePaymentToMowaterStatus(var value) { acknowledgePaymentToMowaterStatus = value; notifyListeners(); } bool isUserOnChatScreen = false; // Track currently active chat int? currentAdsId; String? currentReceiverId; int? currentRequestId; // Update current chat context void setCurrentChatContext({int? adsId, String? receiverId, int? requestId}) { currentAdsId = adsId; currentReceiverId = receiverId; currentRequestId = requestId; } // Clear current chat context void clearCurrentChatContext() { currentAdsId = null; currentReceiverId = null; currentRequestId = null; } // Check if currently on a specific ad chat bool isOnAdChat(int adsId, String receiverId) { return isUserOnChatScreen && currentAdsId == adsId && currentReceiverId == receiverId; } // Check if currently on a specific request chat bool isOnRequestChat(int requestId, String receiverId) { return isUserOnChatScreen && currentRequestId == requestId && currentReceiverId == receiverId; } final ScrollController scrollController = ScrollController(); void scrollChatDown() { if (!isUserOnChatScreen) { return; } if (scrollController.hasClients) { // Check if attached to exactly one scroll view try { if (scrollController.positions.length == 1) { scrollController.animateTo( scrollController.position.maxScrollExtent + 200, // for the text field duration: const Duration(seconds: 1), curve: Curves.fastOutSlowIn, ); } else { log("âš ī¸ ScrollController attached to ${scrollController.positions.length} scroll views, skipping scroll"); } } catch (e) { log("Error scrolling chat: $e"); } } } List dealOptionsModelList = [ OfferRequestCommentModel( index: 0, isSelected: false, title: LocaleKeys.dealCompleted.tr(), ), OfferRequestCommentModel( index: 1, isSelected: false, title: LocaleKeys.theDealNotCompleted.tr(), ), ]; updateIsSelectedInDealOptionsModelList(int index, bool value) { for (var element in dealOptionsModelList) { element.isSelected = false; } dealOptionsModelList[index].isSelected = true; notifyListeners(); } List indexesForCancelSpecialCarOffer = [0, 1, 6, 10]; List indexesForCancelSparePartOffer = [2, 10]; List indexesForRejectOffer = [3, 4, 10]; List indexesForDealNotCompleted = [7, 4, 8, 9, 10]; List offerRejectModelList = [ OfferRequestCommentModel( index: 0, isSelected: true, title: LocaleKeys.dealOutsideApp.tr(), ), OfferRequestCommentModel( index: 1, isSelected: false, title: LocaleKeys.noAgreementCustomer.tr(), ), OfferRequestCommentModel( index: 2, isSelected: false, title: LocaleKeys.itemNoLongerAvailable.tr(), ), OfferRequestCommentModel( index: 3, isSelected: true, title: LocaleKeys.changedMind.tr(), ), OfferRequestCommentModel( index: 4, isSelected: false, title: LocaleKeys.veryHighPrice.tr(), ), OfferRequestCommentModel( index: 5, isSelected: false, title: LocaleKeys.alreadySold.tr(), ), OfferRequestCommentModel( index: 6, isSelected: false, title: LocaleKeys.customerNotResponding.tr(), ), OfferRequestCommentModel( index: 7, isSelected: false, title: LocaleKeys.cancelRequest.tr(), ), OfferRequestCommentModel( index: 8, isSelected: false, title: LocaleKeys.offerNotMatched.tr(), ), OfferRequestCommentModel( index: 9, isSelected: false, title: LocaleKeys.testTheService.tr(), ), OfferRequestCommentModel( index: 10, isSelected: false, title: LocaleKeys.otherVar.tr(), ), ]; void updateSelectionInOfferRejectModelList(int index) { for (var value in offerRejectModelList) { value.isSelected = false; } selectedOfferRequestCommentModel = offerRejectModelList[index]; offerRejectModelList[index].isSelected = true; notifyListeners(); } OfferRequestCommentModel selectedOfferRequestCommentModel = OfferRequestCommentModel( index: 0, isSelected: true, title: LocaleKeys.changedMind.tr(), ); String rejectOfferDescription = ""; String rejectOfferDescriptionError = ""; void updateRejectOfferDescription(String value) { rejectOfferDescription = value; if (value.isNotEmpty) { rejectOfferDescriptionError = ""; } } //REJECTING OFFER bool isRejectOfferButtonValidated() { if (selectedOfferRequestCommentModel.index != offerRejectModelList.length - 1) { return true; } bool isValidated = true; if (rejectOfferDescription.isEmpty) { rejectOfferDescriptionError = GlobalConsts.descriptionError; isValidated = false; } else { rejectOfferDescriptionError = ""; } notifyListeners(); return isValidated; } Future onNewMessageReceivedForRequestOffer({required List messages, bool isMyOwnOffer = false, required RequestsVM requestsVM}) async { if (AppState().currentAppType == AppType.customer) { for (var currentMessage in messages) { logger.i(currentMessage); int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == currentMessage.senderUserID); if (providerIndex != -1) { // Check if message already exists to prevent duplicates bool messageExists = false; if (currentMessage.id != null) { // For messages with ID, use ID for duplicate detection messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) => msg.id == currentMessage.id); } else { // For messages without ID (locally created), use content-based duplicate detection // Check if same sender, same text, same reqOfferID within short time window messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) { return msg.senderUserID == currentMessage.senderUserID && msg.chatText == currentMessage.chatText && msg.reqOfferID == currentMessage.reqOfferID && msg.messageType == currentMessage.messageType; }); } if (messageExists) { log("Message already exists (id: ${currentMessage.id}), skipping duplicate"); continue; } if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) { for (var chatMessage in serviceProviderOffersList[providerIndex].chatMessages!) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { chatMessage.reqOffer!.requestOfferStatusEnum = currentMessage.reqOffer!.requestOfferStatusEnum; } } } serviceProviderOffersList[providerIndex].chatMessages!.add(currentMessage); scrollChatDown(); } } } else { for (var currentMessage in messages) { logger.i(currentMessage); // Where we need to call this function for saving a message in chat we will use receiverUserID and in those cases where received ID is null , it means it is a signal R call int requestIndex = -1; if (isMyOwnOffer) { requestIndex = requestsVM.myFilteredRequests.indexWhere((element) => (element.customerUserID == currentMessage.receiverUserID && element.id == currentMessage.requestID)); } else { requestIndex = requestsVM.myFilteredRequests.indexWhere((element) => (element.customerUserID == currentMessage.senderUserID && element.id == currentMessage.requestID)); } log("requestIndex: $requestIndex"); if (requestIndex != -1) { // Check if message already exists to prevent duplicates bool messageExists = false; if (currentMessage.id != null) { // For messages with ID, use ID for duplicate detection messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) => msg.id == currentMessage.id); } else { // For messages without ID (locally created), use content-based duplicate detection // Check if same sender, same text, same reqOfferID within short time window messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) { return msg.senderUserID == currentMessage.senderUserID && msg.chatText == currentMessage.chatText && msg.reqOfferID == currentMessage.reqOfferID && msg.messageType == currentMessage.messageType; }); } if (messageExists) { log("Message already exists (id: ${currentMessage.id}), skipping duplicate"); continue; } if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) { for (var chatMessage in requestsVM.myFilteredRequests[requestIndex].chatMessages) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { logger.i("chatMessage reqOfferID: ${chatMessage.reqOffer!.id.toString()}"); logger.i("currentMessage reqOfferID: ${currentMessage.reqOffer!.id.toString()}"); chatMessage.reqOffer!.requestOfferStatusEnum = currentMessage.reqOffer!.requestOfferStatusEnum; } } // If this offer was accepted, update the request status to inProgress // This ensures all other offers show "offerNoLongerAvailable" on provider side if (currentMessage.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.accepted) { log("Offer accepted, updating request status to inProgress"); requestsVM.myFilteredRequests[requestIndex].requestStatus = RequestStatusEnum.inProgress; } } requestsVM.addChatMessagesInRequestsModel(msg: currentMessage, index: requestIndex); scrollChatDown(); } } } notifyListeners(); } void _subscribeToReceiveRequestOfferMessages() { if (_requestsVM == null) { logger.e("RequestsVM not set! Cannot subscribe to request offer messages."); return; } // Clear existing listener first to prevent duplicates try { hubConnection!.off(SignalrConsts.receiveMessageRequestOffer); } catch (e) { // Ignore if no listener exists } hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List? arguments) { logger.i(arguments); if (arguments == null || arguments.isEmpty) return; List chat = []; for (var message in arguments) { final chatMessage = ChatMessageModel.fromJson(message as Map, isForReqOfferImagesURLs: true); chat.add(chatMessage); } onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: _requestsVM!); // Utils.showToast(arguments.toString()); }); logger.i("Subscribed to RequestOffer messages"); } Future markMessagesAsRead(BuildContext context, List messageIds, ChatTypeEnum chatTypeEnum) async { bool status = false; try { // Utils.showLoading(context); status = await chatRepo.markMessageAsRead(messageIds: messageIds, chatTypeEnum: chatTypeEnum); // Utils.hideLoading(context); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); // Utils.hideLoading(context); status = false; } return status; } int _retryCount = 0; final int _maxRetries = 5; // Maximum number of consecutive retries before increasing delay bool _isReconnecting = false; Future _reconnectWithBackoff() async { // Never stop trying, but increase delays after max retries int delaySeconds; if (_retryCount < _maxRetries) { delaySeconds = 2 * (_retryCount + 1); // 2, 4, 6, 8, 10 seconds } else { delaySeconds = 30; // After 5 failures, retry every 30 seconds indefinitely } logger.i("Retrying connection in $delaySeconds seconds (attempt ${_retryCount + 1})..."); await Future.delayed(Duration(seconds: delaySeconds)); _retryCount++; await buildHubConnection(); // Try reconnecting (no context needed) } // Clear all existing listeners to prevent duplicates void _clearAllListeners() { if (hubConnection != null && _isSubscribed) { try { hubConnection!.off(SignalrConsts.receiveMessageRequestOffer); hubConnection!.off(SignalrConsts.receiveMessageAds); hubConnection!.off(SignalrConsts.receiveMessageGeneral); hubConnection!.off(SignalrConsts.requestOfferReadUpdate); _isSubscribed = false; logger.i("Cleared all SignalR listeners"); } catch (e) { logger.e("Error clearing listeners: ${e.toString()}"); } } } Future buildHubConnection({BuildContext? context}) async { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (_isReconnecting) { logger.i("Reconnection already in progress"); return; } try { _isReconnecting = true; // Create new hub connection hubConnection = await chatRepo.getHubConnection(); // Start with timeout await hubConnection!.start()?.timeout( const Duration(seconds: 30), onTimeout: () { throw Exception("Hub connection timeout"); }, ); // Reset retry count on successful connection _retryCount = 0; _isReconnecting = false; logger.i("Hub Connected Successfully: ${hubConnection!.connectionId}"); // Clear old listeners before subscribing (prevents duplicates) _clearAllListeners(); // Subscribe to messages (no context needed anymore!) if (_requestsVM != null && _adVM != null) { _subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveAdMessages(); _subscribeToReceiveGeneralMessages(); _subscribeToRequestOfferReadUpdate(); _isSubscribed = true; log("✅ All SignalR listeners subscribed | Hub: ${hubConnection!.connectionId}"); log("📍 Events: ReceiveMessageRequestOffer, ReceiveMessageAds, ReceiveMessageGeneral, RequestOfferReadUpdate"); } else { logger.w("ViewModels not injected yet. Listeners will be subscribed after injection."); } // Configure handlers hubConnection!.onclose((exception) async { logger.i("onClose: ${exception.toString()}"); _isSubscribed = false; await _reconnectWithBackoff(); // No context needed }); hubConnection!.onreconnecting((exception) { logger.i("onReconnecting: ${exception?.toString()}"); _isSubscribed = false; }); hubConnection!.onreconnected((connectionId) async { logger.i("onReconnected: ${connectionId.toString()}"); _retryCount = 0; // Reset on successful reconnect // ✅ CRITICAL FIX: Re-subscribe listeners after automatic reconnect if (_requestsVM != null && _adVM != null) { _clearAllListeners(); _subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveAdMessages(); _subscribeToReceiveGeneralMessages(); _subscribeToRequestOfferReadUpdate(); _isSubscribed = true; logger.i("✅ Listeners re-subscribed after automatic reconnection"); } else { logger.e("❌ Cannot re-subscribe: ViewModels not set!"); } }); } catch (e) { logger.e("Error during hub connection setup: ${e.toString()}"); _isReconnecting = false; // Retry on failure await _reconnectWithBackoff(); } notifyListeners(); } else { logger.i("Hub Already Connected: ${hubConnection!.connectionId.toString()}"); } } Future closeHubConnection() async { try { _clearAllListeners(); if (hubConnection != null && hubConnection!.state == HubConnectionState.connected) { await hubConnection!.stop(); logger.i("Hub connection closed"); } hubConnection = null; _isSubscribed = false; _retryCount = 0; _isReconnecting = false; } catch (e) { logger.e("Error closing hub connection: ${e.toString()}"); } } Future onOfferSendForRequest({ required String receiverId, required ChatMessageTypeEnum chatMessageType, required String message, required int requestId, required String offerPrice, required bool isDeliveryAvailable, required RequestDeliveryOptionEnum offerDeliveryOption, required String serviceItemName, required String manufacturedByName, required String manufacturedOn, required List offerImages, required BuildContext context, }) async { // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { logger.i("Hub Not Connected!! Attempting to connect..."); await buildHubConnection(context: context); // Wait a bit for connection to establish await Future.delayed(const Duration(milliseconds: 500)); // Check again after attempt if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { Utils.showToast("Failed to connect. Please try again."); return false; } } if (hubConnection!.state == HubConnectionState.connected) { final providerId = AppState().getUser.data!.userInfo!.providerId; final obj = { "ReceiverUserID": receiverId, "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": message, "RequestID": requestId, "ReqOfferID": 0, "ReqOffer": { "RequestID": requestId, "Price": double.parse(offerPrice), "IsDeliveryAvailable": isDeliveryAvailable, "OfferDeliveryOption": offerDeliveryOption.getIdRequestDeliveryOptionEnum(), "ServiceItem": serviceItemName, "ReqOfferImages": offerImages, "OfferedItemCreatedByName": manufacturedByName, // "OfferedItemCreatedOn": manufacturedOn, // TODO: This should be in String on Server, Right now it is in DateTime "ServiceProviderID": providerId, "OfferStatus": RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(), "Comment": message, }, }; logger.i(obj); try { await hubConnection!.invoke( SignalrConsts.sendMessageRequestOffer, args: [obj], ); return true; } catch (e) { logger.e("error in invoking SendMessageRequestOffer: ${e.toString()}"); Utils.showToast("Failed to send offer"); return false; } } return false; } MessageImageModel convertFileToMessageImageModel({required File file}) { List imageBytes = file.readAsBytesSync(); String image = base64Encode(imageBytes); MessageImageModel vehiclePostingImages = MessageImageModel(imageStr: image, id: 0, isFromNetwork: false, reqOfferID: latestOfferId, imagePath: file.path); return vehiclePostingImages; } List getMessagesImageList() { List requestImages = []; for (var image in pickedImagesForMessage) { var value = convertFileToMessageImageModel(file: File(image.filePath!)); requestImages.add(value); } return requestImages; } List pickedImagesForMessage = []; void removeImageFromList(String filePath) { int index = pickedImagesForMessage.indexWhere((element) => element.filePath == filePath); if (index == -1) { return; } pickedImagesForMessage.removeAt(index); notifyListeners(); } void pickMultipleImages() async { List imageModels = []; List images = await commonServices.pickMultipleImages(); for (var element in images) { imageModels.add(ImageModel(filePath: element.path, isFromNetwork: false)); } pickedImagesForMessage.addAll(imageModels); if (pickedImagesForMessage.length > GlobalConsts.maxFileCount) { pickedImagesForMessage = pickedImagesForMessage.sublist(0, GlobalConsts.maxFileCount); Utils.showToast(LocaleKeys.maxFileSelection.tr()); } notifyListeners(); } void clearPickedImagesForMessage() { pickedImagesForMessage.clear(); notifyListeners(); } Future onMessageSendForRequest({ required String receiverId, required ChatMessageTypeEnum chatMessageType, required String message, required int requestId, required BuildContext context, }) async { if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false; // ✅ Prevent duplicate sends if (isSendingMessage) { logger.w("Message already being sent, ignoring duplicate tap"); return false; } isSendingMessage = true; notifyListeners(); try { // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { logger.i("Hub Not Connected!! Attempting to connect..."); await buildHubConnection(context: context); // Wait a bit for connection to establish await Future.delayed(const Duration(milliseconds: 500)); // Check again after attempt if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { Utils.showToast("Failed to connect. Please try again."); isSendingMessage = false; notifyListeners(); return false; } } if (hubConnection!.state == HubConnectionState.connected) { final userId = AppState().getUser.data!.userInfo!.userId.toString(); final name = AppState().getUser.data!.userInfo!.firstName.toString(); List> requestImagesMap = []; List messageImages = getMessagesImageList(); if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) { for (var element in messageImages) { requestImagesMap.add(element.toJson()); } } final obj = { "ReceiverUserID": receiverId, "SenderUserID": userId, "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message, "RequestID": requestId, "ReqOfferID": latestOfferId, }; if (chatMessageType == ChatMessageTypeEnum.image) { obj.addAll({"ReqOfferImages": requestImagesMap}); } logger.i(obj); try { await hubConnection!.invoke( SignalrConsts.sendMessageRequestOffer, args: [obj], ); ChatMessageModel chatMessageModel = ChatMessageModel( messageType: chatMessageType.getIdFromChatMessageTypeEnum(), chatText: message, isMyMessage: true, requestID: requestId, senderName: name, senderUserID: userId, chatMessageTypeEnum: chatMessageType, receiverUserID: receiverId, messageImages: messageImages, ); if (AppState().currentAppType == AppType.customer) { int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId); if (providerIndex != -1) { serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel); } } else { // ✅ Use stored RequestsVM instead of context.read if (_requestsVM != null) { int providerIndex = _requestsVM!.myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId)); log("providerIndex2:$providerIndex"); if (providerIndex != -1) { _requestsVM!.addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex); } } } isSendingMessage = false; notifyListeners(); return true; } catch (e) { logger.e("error in invoking SendMessage: ${e.toString()}"); Utils.showToast("Failed to send message"); isSendingMessage = false; notifyListeners(); return false; } } isSendingMessage = false; notifyListeners(); return false; } catch (e) { logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}"); isSendingMessage = false; notifyListeners(); return false; } } List serviceProviderOffersList = []; Future getOffersFromProvidersByRequest({required int requestId, bool isNeedLoading = true}) async { try { if (isNeedLoading) { setState(ViewState.busy); } ProviderOffersModel providerOffersModel = await requestRepo.getOffersFromProvidersByRequest(requestId: requestId); if (isNeedLoading) { setState(ViewState.idle); } serviceProviderOffersList.clear(); serviceProviderOffersList = providerOffersModel.serviceProviders ?? []; notifyListeners(); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); if (isNeedLoading) { setState(ViewState.idle); } } } Future getRequestsChatMessagesForCustomer({ required BuildContext context, required int providerId, required int requestOfferId, required int requestId, required int providerOfferIndex, }) async { try { int customerId = AppState().getUser.data!.userInfo!.customerId!; Utils.showLoading(context); List chatMessages = await chatRepo.getUsersChatMessagesForRequests(providerId: providerId, customerId: customerId, requestOfferId: requestOfferId, requestId: requestId); serviceProviderOffersList[providerOfferIndex].chatMessages = chatMessages; if (serviceProviderOffersList[providerOfferIndex].chatMessages != null) { for (var message in serviceProviderOffersList[providerOfferIndex].chatMessages!) { if (message.chatMessageTypeEnum == ChatMessageTypeEnum.offer) { updateLatestOfferId(message.reqOfferID ?? 0); if (message.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.accepted) { context.read().updateAcceptedReqOffer(message.reqOffer!); context.read().updateAcceptedRequestOfferProviderName(serviceProviderOffersList[providerOfferIndex].name ?? ""); } log("latestOfferId: $latestOfferId"); } } } List unreadMessageIds = []; for (var msg in chatMessages) { if (!msg.isRead! && !msg.isMyMessage!) { unreadMessageIds.add(msg.id!); } } if (unreadMessageIds.isNotEmpty) { log("unreadMessageIds: ${unreadMessageIds.toString()}"); await markMessagesAsRead(context, unreadMessageIds, ChatTypeEnum.requestOffer); } Utils.hideLoading(context); notifyListeners(); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); Utils.hideLoading(context); } } Future getRequestsChatMessagesForProvider({ required BuildContext context, required int customerId, required int requestOfferId, required int requestId, required int customerRequestIndex, }) async { try { int providerId = AppState().getUser.data!.userInfo!.providerId!; Utils.showLoading(context); List chatMessages = await chatRepo.getUsersChatMessagesForRequests(providerId: providerId, customerId: customerId, requestOfferId: requestOfferId, requestId: requestId); await context.read().overwriteChatMessagesInRequestsModel(messages: chatMessages, index: customerRequestIndex, context: context); List unreadMessageIds = []; for (var msg in chatMessages) { if (!msg.isRead! && !msg.isMyMessage!) { unreadMessageIds.add(msg.id!); } } if (unreadMessageIds.isNotEmpty) { await markMessagesAsRead(context, unreadMessageIds, ChatTypeEnum.requestOffer); } Utils.hideLoading(context); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); Utils.hideLoading(context); } } Future onActionOfferTapped({required BuildContext context, required RequestOfferStatusEnum requestOfferStatusEnum, required int reqOfferId, required String comments}) async { Utils.showLoading(context); try { GenericRespModel genericRespModel = await requestRepo.updateRequestOfferStatus(requestOfferStatusEnum: requestOfferStatusEnum, requestOfferId: reqOfferId, comments: comments); Utils.showToast(genericRespModel.message.toString()); Utils.hideLoading(context); return genericRespModel.messageStatus == 1 ? reqOfferId : -1; } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); Utils.hideLoading(context); return -1; } } Future onSendMessageForActionOnRequestOffer({ required String receiverId, required ChatMessageTypeEnum chatMessageType, required String comments, required int requestId, required int serviceProviderID, required int requestOfferID, required String offerPrice, required bool isDeliveryAvailable, required RequestDeliveryOptionEnum offerDeliveryOption, required String serviceItemName, required String manufacturedByName, required String manufacturedOn, required RequestOfferStatusEnum requestOfferStatusEnum, required BuildContext context, }) async { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { logger.i("Hub Not Connected!!"); await buildHubConnection(context: context); } if (hubConnection!.state == HubConnectionState.connected) { var obj = { "ReceiverUserID": receiverId, "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": comments, "RequestID": requestId, "ReqOfferID": requestOfferID, "ReqOffer": { "ID": requestOfferID, "RequestID": requestId, "Price": double.parse(offerPrice), "ServiceItem": serviceItemName, "OfferedItemCreatedByName": manufacturedByName, "IsDeliveryAvailable": isDeliveryAvailable, "OfferDeliveryOption": offerDeliveryOption.getIdRequestDeliveryOptionEnum(), // We have commented this because Backend Team is using their server time for this. It was the time offer created by // "OfferedItemCreatedOn": manufacturedOn, "ServiceProviderID": serviceProviderID, "OfferStatus": requestOfferStatusEnum.getIdFromRequestOfferStatusEnum(), "Comment": comments, }, }; logger.i(obj); hubConnection!.invoke( SignalrConsts.sendMessageRequestOffer, args: [obj], ).catchError((e) { logger.i("error in invoking SendMessageRequestOffer: ${e.toString()}"); Utils.showToast(e.toString()); return false; }); } return true; } // =================== Ads ====================== List currentMessagesForAds = []; Future getUsersChatMessagesForAd({required BuildContext context, int? adID, int? adsChatBuyerId, String? userID, String? senderName, required bool isForBuyer}) async { try { Utils.showLoading(context); currentMessagesForAds = await chatRepo.getUsersChatMessagesForAds(userID: userID, adID: adID, isForBuyer: isForBuyer, adsChatBuyerId: adsChatBuyerId); Utils.hideLoading(context); List unreadMessageIds = []; for (var msg in currentMessagesForAds) { msg.senderName = senderName ?? ""; if (!msg.isRead! && !msg.isMyMessage!) { unreadMessageIds.add(msg.id!); } } if (unreadMessageIds.isNotEmpty) { await markMessagesAsRead(context, unreadMessageIds, ChatTypeEnum.ads); } notifyListeners(); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); Utils.hideLoading(context); } } void _subscribeToReceiveAdMessages() { if (_adVM == null) { logger.e("AdVM not set! Cannot subscribe to ad messages."); return; } // Clear existing listener first to prevent duplicates try { hubConnection!.off(SignalrConsts.receiveMessageAds); } catch (e) { // Ignore if no listener exists } hubConnection!.on(SignalrConsts.receiveMessageAds, (List? arguments) { if (arguments == null || arguments.isEmpty) return; List chat = []; for (var message in arguments) { final chatMessage = ChatMessageModel.fromJson(message as Map); chat.add(chatMessage); } onNewMessageReceivedForAds(messages: chat); logger.i(arguments); // Utils.showToast(arguments.toString()); }); logger.i("Subscribed to Ads messages"); } Future onNewMessageReceivedForAds({required List messages}) async { final myUserId = AppState().getUser.data?.userInfo?.userId?.toString(); for (var msg in messages) { // Skip if this is my own message echoed back from server if (myUserId != null && msg.senderUserID == myUserId) { log("Skipping own message echoed from server (sender: ${msg.senderUserID})"); continue; } // Check if message already exists to prevent duplicates bool messageExists = false; if (msg.id != null) { // For messages with ID, use ID for duplicate detection messageExists = currentMessagesForAds.any((existingMsg) => existingMsg.id == msg.id); } else { // For messages without ID (locally created), use content-based duplicate detection messageExists = currentMessagesForAds.any((existingMsg) { return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType; }); } if (messageExists) { log("Message already exists in ads chat (id: ${msg.id}), skipping duplicate"); continue; } currentMessagesForAds.add(msg); } scrollChatDown(); notifyListeners(); } Future onTextMessageSendForAds({ required String receiverId, required ChatMessageTypeEnum chatMessageType, required String message, required int adId, required BuildContext context, }) async { if (message.isEmpty) return false; // ✅ Prevent duplicate sends if (isSendingMessage) { logger.w("Message already being sent, ignoring duplicate tap"); return false; } isSendingMessage = true; notifyListeners(); try { // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { logger.i("Hub Not Connected!! Attempting to connect..."); await buildHubConnection(context: context); // Wait longer for connection to fully establish and register with server await Future.delayed(const Duration(milliseconds: 1500)); // Check again after attempt if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { Utils.showToast("Failed to connect. Please try again."); isSendingMessage = false; notifyListeners(); return false; } // Extra verification: ensure connection ID is available if (hubConnection!.connectionId == null) { logger.w("Connection ID not yet available, waiting a bit more..."); await Future.delayed(const Duration(milliseconds: 500)); } logger.i("Connection ready! ConnectionId: ${hubConnection!.connectionId}"); } if (hubConnection!.state == HubConnectionState.connected) { final userId = AppState().getUser.data!.userInfo!.userId.toString(); final name = AppState().getUser.data!.userInfo!.firstName.toString(); final obj = { "ReceiverUserID": receiverId, "SenderUserID": userId, // ✅ Send SenderUserID so backend knows who sent it "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": message, "AdsID": adId, }; logger.i("$obj"); try { await hubConnection!.invoke( SignalrConsts.sendMessageAds, args: [obj], ); ChatMessageModel chatMessageModel = ChatMessageModel( messageType: chatMessageType.getIdFromChatMessageTypeEnum(), chatText: message, isMyMessage: true, senderName: name, senderUserID: userId, chatMessageTypeEnum: ChatMessageTypeEnum.freeText, receiverUserID: receiverId, ); currentMessagesForAds.add(chatMessageModel); notifyListeners(); if (AppState().currentAppType == AppType.customer) { } else {} isSendingMessage = false; notifyListeners(); return true; } catch (e) { logger.e("error in invoking SendMessage: ${e.toString()}"); Utils.showToast("Failed to send message"); isSendingMessage = false; notifyListeners(); return false; } } isSendingMessage = false; notifyListeners(); return false; } catch (e) { logger.e("Unexpected error in onTextMessageSendForAds: ${e.toString()}"); isSendingMessage = false; notifyListeners(); return false; } } // ========================General ================== List currentMessagesForGeneralChat = []; Future getUsersChatMessagesForGeneralChat({required BuildContext context, int? adID, int? adsChatBuyerId, String? userID, String? senderName, required bool isForBuyer}) async { try { Utils.showLoading(context); currentMessagesForGeneralChat = await chatRepo.getUsersChatMessagesForGeneralChat(userID: userID, adID: adID, isForBuyer: isForBuyer, adsChatBuyerId: adsChatBuyerId); Utils.hideLoading(context); List unreadMessageIds = []; for (var msg in currentMessagesForGeneralChat) { msg.senderName = senderName ?? ""; if (!msg.isRead! && !msg.isMyMessage!) { unreadMessageIds.add(msg.id!); } } if (unreadMessageIds.isNotEmpty) { await markMessagesAsRead(context, unreadMessageIds, ChatTypeEnum.general); } notifyListeners(); } catch (e) { logger.i(e.toString()); Utils.showToast(e.toString()); Utils.hideLoading(context); } } Future onNewMessageReceivedForGeneral({required List messages}) async { for (var msg in messages) { // Check if message already exists to prevent duplicates bool messageExists = false; if (msg.id != null) { // For messages with ID, use ID for duplicate detection messageExists = currentMessagesForGeneralChat.any((existingMsg) => existingMsg.id == msg.id); } else { // For messages without ID (locally created), use content-based duplicate detection messageExists = currentMessagesForGeneralChat.any((existingMsg) { return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType; }); } if (messageExists) { log("Message already exists in general chat (id: ${msg.id}), skipping duplicate"); continue; } currentMessagesForGeneralChat.add(msg); // ✅ FIXED: Use correct list } scrollChatDown(); notifyListeners(); } void _subscribeToReceiveGeneralMessages() { // Clear existing listener first to prevent duplicates try { hubConnection!.off(SignalrConsts.receiveMessageGeneral); } catch (e) { // Ignore if no listener exists } // General messages don't need specific VM, just storing messages hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List? arguments) { if (arguments == null || arguments.isEmpty) return; List chat = []; for (var message in arguments) { final chatMessage = ChatMessageModel.fromJson(message as Map); chat.add(chatMessage); } onNewMessageReceivedForGeneral(messages: chat); logger.i(arguments); // Utils.showToast(arguments.toString()); }); logger.i("Subscribed to General messages"); } void _subscribeToRequestOfferReadUpdate() { if (_requestsVM == null) { logger.e("RequestsVM not set! Cannot subscribe to request offer read updates."); return; } // Clear existing listener first to prevent duplicates try { hubConnection!.off(SignalrConsts.requestOfferReadUpdate); } catch (e) { // Ignore if no listener exists } hubConnection!.on(SignalrConsts.requestOfferReadUpdate, (List? arguments) { log("🔔 RequestOfferReadUpdate EVENT TRIGGERED! Args: $arguments"); if (arguments == null || arguments.isEmpty) { log("âš ī¸ RequestOfferReadUpdate: Arguments are null or empty"); return; } try { final data = arguments[0] as Map; log("✅ Parsed RequestOfferReadUpdate data: reqOfferChatID=${data['reqOfferChatID']}, isRead=${data['isRead']}"); _handleRequestOfferReadUpdate(data); } catch (e) { log("❌ Error parsing RequestOfferReadUpdate: ${e.toString()}"); } }); log("✅ Subscribed to RequestOfferReadUpdate"); } void _handleRequestOfferReadUpdate(Map data) async { try { final int? messageId = data['reqOfferChatID']; final int? reqOfferID = data['reqOfferID']; final bool? isRead = data['isRead']; final String? readOn = data['readOn']; if (messageId == null) { log("❌ RequestOfferReadUpdate: reqOfferChatID is null"); return; } log("📨 Processing read update: msgID=$messageId, offerID=$reqOfferID, isRead=$isRead"); // For Provider App: Reload messages from API to get updated read status and IDs if (AppState().currentAppType == AppType.provider && _requestsVM != null) { // Check if viewing the chat currently (currentSelectedRequest is set) if (_requestsVM!.currentSelectedRequest != null) { log("🔄 Reloading messages for currentSelectedRequest (ID: ${_requestsVM!.currentSelectedRequest!.id})"); await _reloadRequestMessages( requestId: _requestsVM!.currentSelectedRequest!.id, requestOfferId: 0, // Provider side always uses 0 (sees all offers for request) customerId: _requestsVM!.currentSelectedRequest!.customerId ?? 0, ); log("✅ Reloaded messages for currentSelectedRequest - read status updated"); } else { // No current chat open, optionally reload all requests // (In most cases, we only care about updating the currently visible chat) log("â„šī¸ Read update received but no active chat open, skipping reload"); } } // For Customer App: Not needed (customer is the one marking as read) if (AppState().currentAppType == AppType.customer) { log("RequestOfferReadUpdate on customer side (ignored)"); } } catch (e) { log("❌ Error in _handleRequestOfferReadUpdate: ${e.toString()}"); } } /// Helper method to reload request messages from API without showing loading dialog Future _reloadRequestMessages({ required int requestId, required int requestOfferId, required int customerId, }) async { try { int providerId = AppState().getUser.data!.userInfo!.providerId!; log("🔄 Fetching messages from API: requestId=$requestId, requestOfferId=$requestOfferId"); List chatMessages = await chatRepo.getUsersChatMessagesForRequests( providerId: providerId, customerId: customerId, requestOfferId: requestOfferId, requestId: requestId, ); // Find the request in myFilteredRequests and update its messages int requestIndex = _requestsVM!.myFilteredRequests.indexWhere((req) => req.id == requestId); if (requestIndex != -1) { _requestsVM!.myFilteredRequests[requestIndex].chatMessages = chatMessages; // Also update currentSelectedRequest if it's the same request if (_requestsVM!.currentSelectedRequest != null && _requestsVM!.currentSelectedRequest!.id == requestId) { _requestsVM!.currentSelectedRequest!.chatMessages = chatMessages; } log("✅ Updated ${chatMessages.length} messages for request $requestId"); notifyListeners(); } else { log("âš ī¸ Request $requestId not found in myFilteredRequests"); } } catch (e) { log("❌ Error reloading messages: ${e.toString()}"); } } Future onTextMessageSendForGeneralChat({ required String receiverId, required ChatMessageTypeEnum chatMessageType, required String message, required int adId, required BuildContext context, }) async { if (message.isEmpty) return false; // ✅ Prevent duplicate sends if (isSendingMessage) { logger.w("Message already being sent, ignoring duplicate tap"); return false; } isSendingMessage = true; notifyListeners(); try { // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { logger.i("Hub Not Connected!! Attempting to connect..."); await buildHubConnection(context: context); // Wait a bit for connection to establish await Future.delayed(const Duration(milliseconds: 500)); // Check again after attempt if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { Utils.showToast("Failed to connect. Please try again."); isSendingMessage = false; notifyListeners(); return false; } } if (hubConnection!.state == HubConnectionState.connected) { final userId = AppState().getUser.data!.userInfo!.userId.toString(); final name = AppState().getUser.data!.userInfo!.firstName.toString(); final obj = { "ReceiverUserID": receiverId, "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": message, }; logger.i("$obj"); try { await hubConnection!.invoke( SignalrConsts.sendMessageGeneral, args: [obj], ); ChatMessageModel chatMessageModel = ChatMessageModel( messageType: chatMessageType.getIdFromChatMessageTypeEnum(), chatText: message, isMyMessage: true, senderName: name, senderUserID: userId, chatMessageTypeEnum: ChatMessageTypeEnum.freeText, receiverUserID: receiverId, ); currentMessagesForGeneralChat.add(chatMessageModel); notifyListeners(); if (AppState().currentAppType == AppType.customer) { } else {} isSendingMessage = false; notifyListeners(); return true; } catch (e) { logger.e("error in invoking SendMessage: ${e.toString()}"); Utils.showToast("Failed to send message"); isSendingMessage = false; notifyListeners(); return false; } } isSendingMessage = false; notifyListeners(); return false; } catch (e) { logger.e("Unexpected error in onTextMessageSendForGeneralChat: ${e.toString()}"); isSendingMessage = false; notifyListeners(); return false; } } }