You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
car_common_app/lib/view_models/chat_view_model.dart

1137 lines
39 KiB
Dart

// 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;
final ScrollController scrollController = ScrollController();
void scrollChatDown() {
if (!isUserOnChatScreen) {
return;
}
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent + 200, // for the text field
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
);
}
}
List<OfferRequestCommentModel> 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<int> indexesForCancelSpecialCarOffer = [0, 1, 6, 10];
List<int> indexesForCancelSparePartOffer = [2, 10];
List<int> indexesForRejectOffer = [3, 4, 10];
List<int> indexesForDealNotCompleted = [7, 4, 8, 9, 10];
List<OfferRequestCommentModel> 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<void> onNewMessageReceivedForRequestOffer({required List<ChatMessageModel> 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) {
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) {
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;
}
}
}
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;
}
hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List<Object?>? arguments) {
logger.i(arguments);
if (arguments == null || arguments.isEmpty) return;
List<ChatMessageModel> chat = [];
for (var message in arguments) {
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>, isForReqOfferImagesURLs: true);
chat.add(chatMessage);
}
onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: _requestsVM!);
// Utils.showToast(arguments.toString());
});
logger.i("Subscribed to RequestOffer messages");
}
Future<bool> markMessagesAsRead(BuildContext context, List<int> 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<void> _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);
_isSubscribed = false;
logger.i("Cleared all SignalR listeners");
} catch (e) {
logger.e("Error clearing listeners: ${e.toString()}");
}
}
}
Future<void> 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();
_isSubscribed = true;
logger.i("All listeners subscribed successfully");
} 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();
_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<void> 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<bool> 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<dynamic> 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 = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"RequestID": requestId,
"ReqOfferID": 0,
"ReqOffer": <String, dynamic>{
"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: <Object>[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<int> imageBytes = file.readAsBytesSync();
String image = base64Encode(imageBytes);
MessageImageModel vehiclePostingImages = MessageImageModel(imageStr: image, id: 0, isFromNetwork: false, reqOfferID: latestOfferId, imagePath: file.path);
return vehiclePostingImages;
}
List<MessageImageModel> getMessagesImageList() {
List<MessageImageModel> requestImages = [];
for (var image in pickedImagesForMessage) {
var value = convertFileToMessageImageModel(file: File(image.filePath!));
requestImages.add(value);
}
return requestImages;
}
List<ImageModel> 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<ImageModel> imageModels = [];
List<File> 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<bool> 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<Map<String, dynamic>> requestImagesMap = [];
List<MessageImageModel> messageImages = getMessagesImageList();
if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) {
for (var element in messageImages) {
requestImagesMap.add(element.toJson());
}
}
final obj = <String, dynamic>{
"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: <Object>[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<ServiceProvidersOffers> serviceProviderOffersList = [];
Future<void> 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<void> 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<ChatMessageModel> 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<RequestsVM>().updateAcceptedReqOffer(message.reqOffer!);
context.read<RequestsVM>().updateAcceptedRequestOfferProviderName(serviceProviderOffersList[providerOfferIndex].name ?? "");
}
log("latestOfferId: $latestOfferId");
}
}
}
List<int> 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<void> 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<ChatMessageModel> chatMessages = await chatRepo.getUsersChatMessagesForRequests(providerId: providerId, customerId: customerId, requestOfferId: requestOfferId, requestId: requestId);
await context.read<RequestsVM>().overwriteChatMessagesInRequestsModel(messages: chatMessages, index: customerRequestIndex, context: context);
List<int> 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<int> 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<bool> 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 = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": comments,
"RequestID": requestId,
"ReqOfferID": requestOfferID,
"ReqOffer": <String, dynamic>{
"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: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessageRequestOffer: ${e.toString()}");
Utils.showToast(e.toString());
return false;
});
}
return true;
}
// =================== Ads ======================
List<ChatMessageModel> currentMessagesForAds = [];
Future<void> 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<int> 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;
}
hubConnection!.on(SignalrConsts.receiveMessageAds, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return;
List<ChatMessageModel> chat = [];
for (var message in arguments) {
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>);
chat.add(chatMessage);
}
onNewMessageReceivedForAds(messages: chat);
logger.i(arguments);
// Utils.showToast(arguments.toString());
});
logger.i("Subscribed to Ads messages");
}
Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
currentMessagesForAds.add(msg);
}
scrollChatDown();
notifyListeners();
}
Future<bool> 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 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 = <String, dynamic>{
"ReceiverUserID": receiverId,
// "SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"AdsID": adId,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[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<ChatMessageModel> currentMessagesForGeneralChat = [];
Future<void> 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<int> 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<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
currentMessagesForGeneralChat.add(msg); // ✅ FIXED: Use correct list
}
scrollChatDown();
notifyListeners();
}
void _subscribeToReceiveGeneralMessages() {
// General messages don't need specific VM, just storing messages
hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return;
List<ChatMessageModel> chat = [];
for (var message in arguments) {
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>);
chat.add(chatMessage);
}
onNewMessageReceivedForGeneral(messages: chat);
logger.i(arguments);
// Utils.showToast(arguments.toString());
});
logger.i("Subscribed to General messages");
}
Future<bool> 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 = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[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;
}
}
}