|
|
|
|
@ -35,6 +35,20 @@ class ChatVM extends BaseVM {
|
|
|
|
|
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) {
|
|
|
|
|
@ -211,6 +225,28 @@ class ChatVM extends BaseVM {
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
@ -234,6 +270,28 @@ class ChatVM extends BaseVM {
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
@ -242,6 +300,13 @@ class ChatVM extends BaseVM {
|
|
|
|
|
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();
|
|
|
|
|
@ -251,7 +316,19 @@ class ChatVM extends BaseVM {
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _subscribeToReceiveRequestOfferMessages(BuildContext context) {
|
|
|
|
|
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<Object?>? arguments) {
|
|
|
|
|
logger.i(arguments);
|
|
|
|
|
|
|
|
|
|
@ -261,9 +338,10 @@ class ChatVM extends BaseVM {
|
|
|
|
|
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>, isForReqOfferImagesURLs: true);
|
|
|
|
|
chat.add(chatMessage);
|
|
|
|
|
}
|
|
|
|
|
onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: context.read<RequestsVM>());
|
|
|
|
|
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 {
|
|
|
|
|
@ -308,6 +386,7 @@ class ChatVM extends BaseVM {
|
|
|
|
|
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) {
|
|
|
|
|
@ -346,13 +425,17 @@ class ChatVM extends BaseVM {
|
|
|
|
|
// Clear old listeners before subscribing (prevents duplicates)
|
|
|
|
|
_clearAllListeners();
|
|
|
|
|
|
|
|
|
|
// Subscribe to messages (only if context is available for first-time connection)
|
|
|
|
|
// For reconnections, we'll re-subscribe without context
|
|
|
|
|
if (context != null) {
|
|
|
|
|
_subscribeToReceiveRequestOfferMessages(context);
|
|
|
|
|
_subscribeToReceiveAdMessages(context);
|
|
|
|
|
_subscribeToReceiveGeneralMessages(context);
|
|
|
|
|
// 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
|
|
|
|
|
@ -367,11 +450,22 @@ class ChatVM extends BaseVM {
|
|
|
|
|
_isSubscribed = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
hubConnection!.onreconnected((connectionId) {
|
|
|
|
|
hubConnection!.onreconnected((connectionId) async {
|
|
|
|
|
logger.i("onReconnected: ${connectionId.toString()}");
|
|
|
|
|
// Re-subscribe after automatic reconnect (SignalR's own reconnect, not ours)
|
|
|
|
|
// This is safe because we clear listeners in onreconnecting
|
|
|
|
|
_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()}");
|
|
|
|
|
@ -527,83 +621,112 @@ class ChatVM extends BaseVM {
|
|
|
|
|
}) async {
|
|
|
|
|
if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false;
|
|
|
|
|
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
logger.i("Hub Not Connected!! Attempting to connect...");
|
|
|
|
|
await buildHubConnection(context: context);
|
|
|
|
|
// ✅ Prevent duplicate sends
|
|
|
|
|
if (isSendingMessage) {
|
|
|
|
|
logger.w("Message already being sent, ignoring duplicate tap");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait a bit for connection to establish
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
isSendingMessage = true;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
// Check again after attempt
|
|
|
|
|
try {
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
Utils.showToast("Failed to connect. Please try again.");
|
|
|
|
|
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());
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
|
|
|
|
|
log("providerIndex2:$providerIndex");
|
|
|
|
|
if (providerIndex != -1) {
|
|
|
|
|
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.e("error in invoking SendMessage: ${e.toString()}");
|
|
|
|
|
Utils.showToast("Failed to send message");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSendingMessage = false;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return false;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}");
|
|
|
|
|
isSendingMessage = false;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<ServiceProvidersOffers> serviceProviderOffersList = [];
|
|
|
|
|
@ -806,7 +929,19 @@ class ChatVM extends BaseVM {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _subscribeToReceiveAdMessages(BuildContext 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<Object?>? arguments) {
|
|
|
|
|
if (arguments == null || arguments.isEmpty) return;
|
|
|
|
|
List<ChatMessageModel> chat = [];
|
|
|
|
|
@ -818,10 +953,28 @@ class ChatVM extends BaseVM {
|
|
|
|
|
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) {
|
|
|
|
|
// 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();
|
|
|
|
|
@ -837,63 +990,89 @@ class ChatVM extends BaseVM {
|
|
|
|
|
}) async {
|
|
|
|
|
if (message.isEmpty) return false;
|
|
|
|
|
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
logger.i("Hub Not Connected!! Attempting to connect...");
|
|
|
|
|
await buildHubConnection(context: context);
|
|
|
|
|
// ✅ Prevent duplicate sends
|
|
|
|
|
if (isSendingMessage) {
|
|
|
|
|
logger.w("Message already being sent, ignoring duplicate tap");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait a bit for connection to establish
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
isSendingMessage = true;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
// Check again after attempt
|
|
|
|
|
try {
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
Utils.showToast("Failed to connect. Please try again.");
|
|
|
|
|
return false;
|
|
|
|
|
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 {}
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.e("error in invoking SendMessage: ${e.toString()}");
|
|
|
|
|
Utils.showToast("Failed to send message");
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================General ==================
|
|
|
|
|
@ -928,13 +1107,38 @@ class ChatVM extends BaseVM {
|
|
|
|
|
|
|
|
|
|
Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> 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(BuildContext context) {
|
|
|
|
|
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<Object?>? arguments) {
|
|
|
|
|
if (arguments == null || arguments.isEmpty) return;
|
|
|
|
|
List<ChatMessageModel> chat = [];
|
|
|
|
|
@ -946,6 +1150,121 @@ class ChatVM extends BaseVM {
|
|
|
|
|
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<Object?>? 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<String, dynamic>;
|
|
|
|
|
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<String, dynamic> 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<void> _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<ChatMessageModel> 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<bool> onTextMessageSendForGeneralChat({
|
|
|
|
|
@ -957,59 +1276,85 @@ class ChatVM extends BaseVM {
|
|
|
|
|
}) async {
|
|
|
|
|
if (message.isEmpty) return false;
|
|
|
|
|
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
logger.i("Hub Not Connected!! Attempting to connect...");
|
|
|
|
|
await buildHubConnection(context: context);
|
|
|
|
|
// ✅ Prevent duplicate sends
|
|
|
|
|
if (isSendingMessage) {
|
|
|
|
|
logger.w("Message already being sent, ignoring duplicate tap");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait a bit for connection to establish
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
isSendingMessage = true;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
// Check again after attempt
|
|
|
|
|
try {
|
|
|
|
|
// Ensure connection is established
|
|
|
|
|
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
|
|
|
|
|
Utils.showToast("Failed to connect. Please try again.");
|
|
|
|
|
return false;
|
|
|
|
|
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 {}
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.e("error in invoking SendMessage: ${e.toString()}");
|
|
|
|
|
Utils.showToast("Failed to send message");
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|