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.
cloudsolutions-atoms/lib/modules/cx_module/chat/chat_provider.dart

752 lines
25 KiB
Dart

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:just_audio/just_audio.dart' as JustAudio;
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/signalr_client.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/main.dart';
import 'package:test_sa/modules/cx_module/chat/call/audio_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/model/chat_login_response_model.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/material.dart' as Material;
import 'package:flutter/material.dart';
import 'api_client.dart';
import 'chat_api_client.dart';
import 'model/chat_attachment_model.dart';
import 'model/chat_participant_model.dart';
import 'model/get_search_user_chat_model.dart';
import 'model/get_single_user_chat_list_model.dart';
import 'model/unread_message_model.dart';
import 'model/user_chat_history_model.dart';
import 'model/call_session.dart';
import 'call/call_debug_helper.dart';
import 'call/incoming_call_dialog.dart';
import 'call/services/webrtc_service.dart';
import 'call/services/callkit_service.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'call/call_error_handler.dart';
import 'call/services/call_notification_service.dart';
import 'services/call_manager.dart';
import 'services/signalr_service.dart';
import 'services/call_coordinator.dart';
import 'package:test_sa/core/di/service_locator.dart';
class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// ==================== CHAT-ONLY STATE ====================
// ChatProvider now ONLY manages chat-related state
// All call state is managed by CallManager
bool isTyping = false;
bool chatLoginTokenLoading = false;
ChatLoginResponse? chatLoginResponse;
bool chatParticipantLoading = false;
ChatParticipantModel? chatParticipantModel;
bool userChatHistoryLoading = false;
List<SingleUserChatModel>? userChatHistory;
bool messageIsSending = false;
List<SingleUserChatModel> chatResponseList = [];
Participants? sender;
Participants? recipient;
late String receiverID;
late int moduleID;
int? referenceID;
// SignalR Service - Retrieved from GetIt (Dependency Injection)
// DO NOT instantiate directly - always use getIt<SignalRService>()
SignalRService get _signalRService => getIt<SignalRService>();
// FIXED: Make chatHubConnection a regular property that can be set
HubConnection? _chatHubConnection;
// Getter for backward compatibility
HubConnection? get chatHubConnection => _chatHubConnection ?? _signalRService.hubConnection;
// Setter for backward compatibility
set chatHubConnection(HubConnection? connection) {
_chatHubConnection = connection;
}
// === CALL STATE - DELEGATED TO CallManager (READ-ONLY) ===
// These getters provide read-only access to call state for UI
// All call operations should go through CallManager directly
CallManager get _callManager => getIt<CallManager>();
CallStatus get callStatus => _callManager.callStatus;
CallSession? get currentCall => _callManager.currentCall;
Duration get callDuration => _callManager.callDuration;
bool get isMuted => _callManager.isMuted;
bool get isSpeakerOn => _callManager.isSpeakerOn;
bool get isCameraOn => _callManager.isCameraOn;
bool get isPeerMuted => _callManager.isPeerMuted;
bool get isPeerCameraOn => _callManager.isPeerCameraOn;
bool get isCallInProgress => _callManager.isCallInProgress;
WebRTCService? get webrtcService => _callManager.webrtcService;
// For backwards compatibility with UI components
bool get areCallHandlersRegistered => true; // Always true since CallManager handles it
// Private state for legacy ringtone support (chat-related)
final JustAudio.AudioPlayer _ringingPlayer = JustAudio.AudioPlayer();
bool _isRingingPlaying = false;
/// Update call status with detailed logging
void _updateCallStatus(CallStatus newStatus, {String? reason}) {
// Call status is now managed by CallManager
log(' [CALL STATUS] Call status is now managed by CallManager', name: 'ChatProvider');
notifyListeners();
}
/// OPTIMIZATION: Improved connection disposal to prevent memory leaks
/// This properly handles errors and ensures connection is always cleaned up
Future<void> _disposeConnection() async {
// CRITICAL FIX: DO NOT close the SignalR connection here
// SignalR is now a SINGLETON managed by SignalRService
// It's shared between ChatProvider and CallManager
// Closing it here would break ongoing calls and call setup
// Just clear the local reference
_chatHubConnection = null;
if (kDebugMode) {
print('🔌 [ChatProvider] Cleared local SignalR reference (connection remains active)');
}
}
/// Reset provider state and properly dispose SignalR connection
Future<void> reset() async {
// CRITICAL FIX: DO NOT dispose the SignalR connection
// Just clear ChatProvider's local state
// The SignalRService singleton will manage the connection lifecycle
_chatHubConnection = null;
chatLoginTokenLoading = false;
chatParticipantLoading = false;
userChatHistoryLoading = false;
chatLoginResponse = null;
chatParticipantModel = null;
userChatHistory = null;
sender = null;
recipient = null;
ChatApiClient().chatLoginResponse = null;
log('✅ [ChatProvider] Chat state reset (SignalR connection preserved)', name: 'ChatProvider');
}
/// OPTIMIZATION: Override dispose to ensure SignalR connection cleanup
/// This prevents connection leaks when provider is removed from widget tree
@override
void dispose() {
// CRITICAL FIX: DO NOT close SignalR connection on dispose
// The connection is a singleton and may be used by other parts of the app
// Only clear local state
_chatHubConnection = null;
if (kDebugMode) {
print('✅ ChatProvider disposed (SignalR connection preserved)');
}
super.dispose();
}
// Future<void> getUserAutoLoginToken(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber) async {
// reset();
// chatLoginTokenLoading = true;
// notifyListeners();
// chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId);
// chatLoginTokenLoading = false;
// chatParticipantLoading = true;
// notifyListeners();
// // loadParticipants(moduleId, requestId);
// if (chatLoginResponse != null) {
// await loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber);
// }
// }
Future<void> getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber, {bool isMounted = true}) async {
// OPTIMIZATION: Use async reset for proper cleanup
await reset();
receiverID = assigneeEmployeeNumber;
chatLoginTokenLoading = true;
if (isMounted) {
notifyListeners();
}
try {
chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId, assigneeEmployeeNumber);
chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber);
log('✅ Got chatParticipantModel: ${chatParticipantModel?.toJson()}');
try {
sender = chatParticipantModel?.participants?.firstWhere(
(participant) => participant.userId?.toLowerCase() == myId.toLowerCase()
);
} catch (e) {
log('⚠️ Sender NOT FOUND for myId: $myId. Error: $e');
sender = null;
}
try {
recipient = chatParticipantModel?.participants?.firstWhere(
(participant) => participant.userId?.toLowerCase() == assigneeEmployeeNumber.toLowerCase()
);
log('✅ recipient found: userId=${recipient?.userId}, userName=${recipient?.userName}, employeeNumber=${recipient?.employeeNumber}');
} catch (e) {
log('⚠️ Recipient NOT FOUND for assigneeEmployeeNumber: $assigneeEmployeeNumber. Error: $e');
recipient = null;
}
/// CRITICAL:need to remove this after testing Cache credentials in CallCoordinator for background calls
if (chatLoginResponse != null && chatParticipantModel != null && sender != null) {
final coordinator = CallCoordinator();
coordinator.cacheCredentials(
loginResponse: chatLoginResponse!,
participants: chatParticipantModel!,
myEmployeeNumber: myId,
);
}
} catch (ex) {
if (kDebugMode) {
print('⚠️ Error in getUserAutoLoginTokenSilent: $ex');
}
log('❌ EXCEPTION in getUserAutoLoginTokenSilent: $ex');
}
chatLoginTokenLoading = false;
if (isMounted) {
notifyListeners();
}
}
// Future<void> getUserLoadChatHistory(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async {
// await loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber);
// }
// Future<void> loadParticipants(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async {
// userChatHistoryLoading = true;
// notifyListeners();
// try {
// chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber);
// } catch (ex) {
// userChatHistoryLoading = false;
// notifyListeners();
// return;
// }
//
// try {
// sender = chatParticipantModel?.participants?.singleWhere((participant) => participant.employeeNumber == myId);
// recipient = chatParticipantModel?.participants?.singleWhere((participant) => participant.employeeNumber == assigneeEmployeeNumber);
// } catch (e) {}
// }
Future<void> connectToHub(int moduleId, int requestId, String myId, String assigneeEmployeeNumber, bool readOnly, {bool isMounted = true}) async {
userChatHistoryLoading = true;
if (isMounted) {
notifyListeners();
}
moduleID = moduleId;
referenceID = requestId;
// OPTIMIZATION: Wrap connection in try-catch to prevent leaks on error
if (!readOnly) {
try {
await buildHubConnection(chatParticipantModel!.id!.toString());
} catch (e) {
if (kDebugMode) {
print('⚠️ Failed to build hub connection: $e');
}
// Continue loading chat history even if hub connection fails
}
}
userChatHistory = null;
userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber);
chatResponseList = userChatHistory ?? [];
chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
userChatHistoryLoading = false;
if (isMounted) {
notifyListeners();
}
}
// Future<void> loadChatHistory(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async {
// userChatHistoryLoading = true;
// notifyListeners();
// userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber);
// chatResponseList = userChatHistory ?? [];
// chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
// userChatHistoryLoading = false;
// notifyListeners();
// }
Future<bool> invokeSendMessage(Object object) async {
messageIsSending = true;
notifyListeners();
bool returnStatus = false;
try {
await chatHubConnection!.invoke("AddChatUserAsync", args: <Object>[object]);
returnStatus = true;
} catch (ex) {}
messageIsSending = false;
notifyListeners();
return returnStatus;
}
// Future<bool> sendTextMessage(String message) async {
// messageIsSending = true;
// notifyListeners();
// bool returnStatus = false;
//
// ChatResponse? chatResponse = await ChatApiClient().sendTextMessage(message, chatParticipantModel!.id!);
// if (chatResponse != null) {
// returnStatus = true;
// // chatResponseList.add(chatResponse);
// try {
// chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
// } catch (ex) {}
// }
// messageIsSending = false;
// notifyListeners();
// return returnStatus;
// }
// List<groups.GroupResponse>? uGroups = [], searchGroups = [];
// Future<void> getUserAutoLoginToken() async {
// userLoginToken.UserAutoLoginModel userLoginResponse = await ChatApiClient().getUserLoginToken();
//
// if (userLoginResponse.StatusCode == 500) {
// disbaleChatForThisUser = true;
// notifyListeners();
// }
//
// if (userLoginResponse.response != null) {
// // AppState().setchatUserDetails = userLoginResponse;
// } else {
// // AppState().setchatUserDetails = userLoginResponse;
// userLoginResponse.errorResponses!.first.fieldName.toString() + " Erorr".showToast;
// disbaleChatForThisUser = true;
// notifyListeners();
// }
// }
/// OPTIMIZATION: Improved hub connection with error handling
/// Prevents connection leaks if initialization fails
Future<void> buildHubConnection(String conversationID) async {
try {
log('💬 Conversation ID: $conversationID', name: 'ChatProvider');
final signalRService = _signalRService;
if (!signalRService.isConnected) {
final connected = await signalRService.initialize(
userId: chatLoginResponse!.userId.toString(),
authToken: chatLoginResponse!.token ?? '',
conversationId: conversationID,
);
if (!connected) {
throw Exception('Failed to initialize SignalR connection');
}
} else {
if (conversationID.isNotEmpty) {
try {
await signalRService.invoke("JoinConversation", args: [conversationID]);
} catch (e) {
await signalRService.initialize(
userId: chatLoginResponse!.userId.toString(),
authToken: chatLoginResponse!.token ?? '',
conversationId: conversationID,
);
}
}
}
// CRITICAL FIX: Reference the singleton connection, don't create a new one
chatHubConnection = signalRService.hubConnection;
_registerChatEventHandlers();
} catch (e, stackTrace) {
if (kDebugMode) {
print('⚠️ Error building SignalR connection: $e');
}
rethrow;
}
}
/// Register chat event handlers (separate from call handlers)
void _registerChatEventHandlers() {
if (chatHubConnection == null) {
log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider');
return;
}
_signalRService.on("ReceiveMessage", onMsgReceived1);
_signalRService.on("OnMessageReceivedAsync", onMsgReceived);
_signalRService.on("OnSubmitChatAsync", onSubmitChatAsync);
_signalRService.on("OnTypingAsync", OnTypingAsync);
_signalRService.on("OnStopTypingAsync", OnStopTypingAsync);
_signalRService.on("OnSeenChatUserAsync", onSeenUserChatAsync);
_signalRService.on("OnAckSeenAsync", onAckSeenAsync);
_signalRService.on("OnCallHistoryUpdated", _onCallHistoryUpdated);
log('✅ [EVENT HANDLERS] Chat event handlers registered successfully', name: 'ChatProvider');
}
/// Start a new audio or video call - delegates to CallManager
Future<void> startCall(Participants recipient, CallType callType) async {
log('📞 [ChatProvider] startCall() - delegating to CallManager', name: 'ChatProvider');
if (sender == null) {
log('❌ [ChatProvider] Cannot start call - sender is null', name: 'ChatProvider');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Unable to start call. Please try again.');
}
return;
}
try {
await CallManager().initialize(
userId: chatLoginResponse!.userId.toString(),
authToken: chatLoginResponse!.token ?? '',
conversationId: chatParticipantModel?.id?.toString(),
moduleId: moduleID.toString(),
referenceId: referenceID?.toString(),
employeeNumber: sender!.employeeNumber,
);
log('✅ [ChatProvider] CallManager initialized successfully', name: 'ChatProvider');
} catch (e, stackTrace) {
log('❌ [ChatProvider] CallManager initialization failed: $e',
name: 'ChatProvider', error: e, stackTrace: stackTrace);
// CRITICAL: Do NOT proceed if initialization failed
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(
context,
'Unable to initialize calling system. Please try again.'
);
}
return;
}
// Delegate to CallManager
log('📤 [ChatProvider] Delegating to CallManager.startCall()', name: 'ChatProvider');
// Use employeeNumber from ChatParticipantModel for the call
final targetEmployeeNumber = recipient.employeeNumber ?? '';
await CallManager().startCall(
peerId: targetEmployeeNumber,
peerName: recipient.userName ?? 'Unknown',
callType: callType,
peerAvatar: recipient.image,
);
notifyListeners();
}
/// Toggle mute - delegates to CallManager
Future<void> toggleMute() async {
await CallManager().toggleMute();
notifyListeners();
}
/// Toggle speaker - delegates to CallManager
Future<void> toggleSpeaker() async {
await CallManager().toggleSpeaker();
notifyListeners();
}
/// Toggle camera - delegates to CallManager
Future<void> toggleCamera() async {
await CallManager().toggleCamera();
notifyListeners();
}
/// Switch camera - delegates to CallManager
Future<void> switchCamera() async {
await CallManager().switchCamera();
}
/// Hang up - delegates to CallManager
Future<void> hangUp() async {
await CallManager().hangUp();
notifyListeners();
}
/// Accept call - delegates to CallManager
Future<void> acceptCall() async {
await CallManager().acceptCall();
notifyListeners();
}
/// Decline call - delegates to CallManager
Future<void> declineCall(String reason) async {
await CallManager().declineCall(reason);
notifyListeners();
}
/// Handle call history update
void _onCallHistoryUpdated(List<Object?>? args) async {
try {
if (sender != null && recipient != null) {
userChatHistory = await ChatApiClient().loadChatHistory(
moduleID,
referenceID ?? 0,
sender!.employeeNumber ?? '',
recipient!.employeeNumber ?? '',
);
chatResponseList = userChatHistory ?? [];
chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
notifyListeners();
}
} catch (e, stackTrace) {
log('❌ [CALL HISTORY] Error reloading chat history: $e\nStack: $stackTrace', name: 'ChatProvider');
}
}
// ==================== CHAT EVENT HANDLERS ====================
Future<void> onMsgReceived1(List<Object?>? parameters) async {
print("onMsgReceived1:$parameters");
}
Future<void> onMsgReceived(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
var data = parameters[0] as Map<Object?, Object?>;
SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map<String, dynamic>.from(data));
// Add message to chat history
chatResponseList.insert(0, chatResponse);
chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
notifyListeners();
if (kDebugMode) {
print('✅ Message received: ${chatResponse.contant}'); // Fixed: contant not message
}
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in onMsgReceived: $e');
}
}
}
Future<void> onSubmitChatAsync(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
var data = parameters[0] as Map<Object?, Object?>;
SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map<String, dynamic>.from(data));
// Update existing message or add new one
int existingIndex = chatResponseList.indexWhere((msg) => msg.userChatHistoryLineId == chatResponse.userChatHistoryLineId); // Fixed: use correct property
if (existingIndex != -1) {
chatResponseList[existingIndex] = chatResponse;
} else {
chatResponseList.insert(0, chatResponse);
}
chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
notifyListeners();
if (kDebugMode) {
print('✅ Chat submitted: ${chatResponse.contant}'); // Fixed: contant not message
}
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in onSubmitChatAsync: $e');
}
}
}
Future<void> OnTypingAsync(List<Object?>? parameters) async {
try {
isTyping = true;
notifyListeners();
if (kDebugMode) {
print('✍️ User is typing...');
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in OnTypingAsync: $e');
}
}
}
Future<void> OnStopTypingAsync(List<Object?>? parameters) async {
try {
isTyping = false;
notifyListeners();
if (kDebugMode) {
print('✅ User stopped typing');
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in OnStopTypingAsync: $e');
}
}
}
Future<void> onSeenUserChatAsync(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
// Handle message seen status update
if (kDebugMode) {
print('👁️ Messages marked as seen');
}
notifyListeners();
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in onSeenUserChatAsync: $e');
}
}
}
Future<void> onAckSeenAsync(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
// Handle message acknowledgment
if (kDebugMode) {
print('✅ Message acknowledgment received');
}
notifyListeners();
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in onAckSeenAsync: $e');
}
}
}
// ==================== UTILITY METHODS ====================
/// Reset unread message count
Future<bool> resetCount({int? moduleId, int? referenceNo, String? userId}) async {
// Fixed: userId is String? not int?
try {
if (chatLoginResponse != null && sender != null && recipient != null) {
await ChatApiClient().resetCountApi(
moduleId ?? moduleID, // Use parameter or fall back to stored value
referenceNo ?? referenceID ?? 0,
sender!.employeeNumber ?? '',
);
if (kDebugMode) {
print('✅ Reset unread count');
}
return true;
}
return false;
} catch (e) {
if (kDebugMode) {
print('⚠️ resetCount error: $e');
}
log('resetCount error: $e');
return false;
}
}
/// Upload attachments
Future<List<ChatAttachment>?> uploadAttachments(String username, File file, String conversationId) async {
// Fixed: match actual usage
try {
if (chatLoginResponse == null || chatParticipantModel == null) {
if (kDebugMode) {
print('⚠️ Cannot upload - chat not initialized');
}
return null;
}
// Upload single file
final files = [file];
// TODO: Implement file upload to chat API
// The ChatApiClient doesn't have uploadAttachments method yet
// For now, return empty list
if (kDebugMode) {
print('⚠️ uploadAttachments not implemented in ChatApiClient yet');
print(' Username: $username');
print(' File: ${file.path}');
print(' Conversation: $conversationId');
}
return [];
} catch (e) {
if (kDebugMode) {
print('⚠️ Error uploading attachments: $e');
}
log('uploadAttachments error: $e');
return null;
}
}
/// Get unread messages
Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async {
// Fixed: accept employeeId and return List<UnReadMessage>
try {
if (chatLoginResponse == null) {
if (kDebugMode) {
print('⚠️ Cannot get unread messages - not logged in');
}
return [];
}
// TODO: Implement getUnreadMessages API in ChatApiClient
// The ChatApiClient doesn't have this method yet
// For now, return empty list
if (kDebugMode) {
print('⚠️ getUnreadMessages API not implemented in ChatApiClient yet');
print(' Requested for employeeId: $employeeId');
}
return [];
} catch (e) {
if (kDebugMode) {
print('⚠️ Error getting unread messages: $e');
}
log('getUnReadMessages error: $e');
return [];
}
}
}