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:easy_localization/easy_localization.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:mohem_flutter_app/api/chat/chat_api_client.dart'; // import 'package:mohem_flutter_app/api/my_team/my_team_api_client.dart'; // import 'package:mohem_flutter_app/app_state/app_state.dart'; // import 'package:mohem_flutter_app/classes/consts.dart'; // import 'package:mohem_flutter_app/classes/encryption.dart'; // import 'package:mohem_flutter_app/classes/utils.dart'; // import 'package:mohem_flutter_app/config/routes.dart'; // import 'package:mohem_flutter_app/main.dart'; // import 'package:mohem_flutter_app/models/chat/chat_user_image_model.dart'; // import 'package:mohem_flutter_app/models/chat/create_group_request.dart' as createGroup; // import 'package:mohem_flutter_app/models/chat/get_group_chat_history.dart' as groupchathistory; // import 'package:mohem_flutter_app/models/chat/get_search_user_chat_model.dart'; // import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; // import 'package:mohem_flutter_app/models/chat/get_user_groups_by_id.dart' as groups; // import 'package:mohem_flutter_app/models/chat/get_user_groups_by_id.dart'; // import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart' as userLoginToken; // import 'package:mohem_flutter_app/models/chat/make_user_favotire_unfavorite_chat_model.dart' as fav; // import 'package:mohem_flutter_app/models/chat/target_users.dart'; // import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; // import 'package:mohem_flutter_app/ui/chat/chat_detailed_screen.dart'; // import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; // import 'package:mohem_flutter_app/widgets/image_picker.dart'; // import 'package:open_filex/open_filex.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/modules/cx_module/chat/model/chat_login_response_model.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter/material.dart' as Material; 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'; // NEW - Import call session model //Need to refactor this remove unused code. HubConnection? chatHubConnection; class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { bool isTyping = false; bool chatLoginTokenLoading = false; ChatLoginResponse? chatLoginResponse; bool chatParticipantLoading = false; ChatParticipantModel? chatParticipantModel; bool userChatHistoryLoading = false; // UserChatHistoryModel? userChatHistory; List? userChatHistory; bool messageIsSending = false; List chatResponseList = []; Participants? sender; Participants? recipient; late String receiverID; late int moduleID; int? referenceID; // === NEW: CALL STATE VARIABLES === CallStatus callStatus = CallStatus.idle; CallSession? currentCall; Duration callDuration = Duration.zero; Timer? _callDurationTimer; Timer? _callTimeoutTimer; // Call control state bool isMuted = false; bool isSpeakerOn = false; bool isCameraOn = true; bool isPeerMuted = false; bool isPeerCameraOn = true; bool get isCallInProgress => callStatus != CallStatus.idle; /// OPTIMIZATION: Improved connection disposal to prevent memory leaks /// This properly handles errors and ensures connection is always cleaned up Future _disposeConnection() async { try { if (chatHubConnection != null) { await chatHubConnection!.stop(); if (kDebugMode) { print('๐Ÿ”Œ SignalR connection closed successfully'); } } } catch (e) { if (kDebugMode) { print('โš ๏ธ Error closing SignalR connection: $e'); } // Don't rethrow - we still want to clean up } finally { chatHubConnection = null; } } /// Reset provider state and properly dispose SignalR connection Future reset() async { // OPTIMIZATION: Use async/await for proper cleanup await _disposeConnection(); chatLoginTokenLoading = false; chatParticipantLoading = false; userChatHistoryLoading = false; chatLoginResponse = null; chatParticipantModel = null; userChatHistory = null; sender = null; recipient = null; ChatApiClient().chatLoginResponse = null; } /// OPTIMIZATION: Override dispose to ensure SignalR connection cleanup /// This prevents connection leaks when provider is removed from widget tree @override void dispose() { _disposeConnection().then((_) { if (kDebugMode) { print('โœ… ChatProvider disposed'); } }).catchError((error) { if (kDebugMode) { print('โš ๏ธ Error during ChatProvider disposal: $error'); } }); super.dispose(); } // Future 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 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 { log('i am called ...'); chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId, assigneeEmployeeNumber); log('โœ… Got chatLoginResponse'); chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber); log('โœ… Got chatParticipantModel: ${chatParticipantModel?.toJson()}'); // Log available participants for debugging log('๐Ÿ“‹ Available participants: ${chatParticipantModel?.participants?.map((p) => '${p.employeeNumber} (${p.userName})').toList()}'); log('๐Ÿ” Looking for myId: $myId'); log('๐Ÿ” Looking for assigneeEmployeeNumber: $assigneeEmployeeNumber'); // Use case-insensitive matching and handle not found gracefully try { sender = chatParticipantModel?.participants?.firstWhere( (participant) => participant.employeeNumber?.toLowerCase() == myId.toLowerCase() ); log('โœ… sender i got is ${sender?.toJson()}'); } catch (e) { log('โš ๏ธ Sender NOT FOUND for myId: $myId. Error: $e'); sender = null; } try { recipient = chatParticipantModel?.participants?.firstWhere( (participant) => participant.employeeNumber?.toLowerCase() == assigneeEmployeeNumber.toLowerCase() ); log('โœ… recipient i got is ${recipient?.toJson()}'); } catch (e) { log('โš ๏ธ Recipient NOT FOUND for assigneeEmployeeNumber: $assigneeEmployeeNumber. Error: $e'); recipient = null; } } catch (ex) { if (kDebugMode) { print('โš ๏ธ Error in getUserAutoLoginTokenSilent: $ex'); } log('โŒ EXCEPTION in getUserAutoLoginTokenSilent: $ex'); } chatLoginTokenLoading = false; if (isMounted) { notifyListeners(); } } // Future getUserLoadChatHistory(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async { // await loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); // } // Future 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 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 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 invokeSendMessage(Object object) async { messageIsSending = true; notifyListeners(); bool returnStatus = false; try { await chatHubConnection!.invoke("AddChatUserAsync", args: [object]); returnStatus = true; } catch (ex) {} messageIsSending = false; notifyListeners(); return returnStatus; } // Future 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? uGroups = [], searchGroups = []; // Future 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 buildHubConnection(String conversationID) async { try { // Dispose existing connection if any await _disposeConnection(); chatHubConnection = await getHubConnection(); await chatHubConnection!.start(); if (kDebugMode) { print("๐Ÿ”Œ SignalR Hub Connection: Started"); } await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); // Register chat event handlers chatHubConnection!.on("ReceiveMessage", onMsgReceived1); chatHubConnection!.on("OnMessageReceivedAsync", onMsgReceived); chatHubConnection!.on("OnSubmitChatAsync", onSubmitChatAsync); chatHubConnection!.on("OnTypingAsync", OnTypingAsync); chatHubConnection!.on("OnStopTypingAsync", OnStopTypingAsync); chatHubConnection!.on("OnSeenChatUserAsync", onSeenUserChatAsync); chatHubConnection!.on("OnAckSeenAsync", onAckSeenAsync); // Register call event handlers (PHASE 1) _registerCallHandlers(); //group On message // chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); } catch (e) { if (kDebugMode) { print('โš ๏ธ Error building SignalR connection: $e'); } // Clean up on error await _disposeConnection(); rethrow; // Rethrow so caller knows about the error } } Future getHubConnection() async { if (kDebugMode) { print('๐Ÿ”ง Creating new SignalR hub connection...'); } HubConnection hub; HttpConnectionOptions httpOp = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); hub = HubConnectionBuilder() .withUrl("${URLs.chatHubUrlChat}?UserId=${chatLoginResponse!.userId}&source=Desktop&access_token=${chatLoginResponse!.token}", options: httpOp) .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]).build(); if (kDebugMode) { print('โœ… SignalR hub connection created'); } return hub; } void registerEvents() { // chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); // chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); // chatHubConnection.on("OnUserTypingAsync", onUserTyping); chatHubConnection?.on("OnUserCountAsync", userCountAsync); // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); // chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); // chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); // chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus); // // {"type":1,"target":"","arguments":[[{"id":217869,"userName":"Sultan.Khan","email":"Sultan.Khan@cloudsolutions.com.sa","phone":null,"title":"Sultan.Khan","userStatus":1,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":false,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null},{"id":15153,"userName":"Tamer.Fanasheh","email":"Tamer.F@cloudsolutions.com.sa","phone":null,"title":"Tamer Fanasheh","userStatus":2,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":true,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null}]]} if (kDebugMode) { // logger.i("All listeners registered"); } } // Future getUserRecentChats() async { // ChatUserModel recentChat = await ChatApiClient().getRecentChats(); // ChatUserModel favUList = await ChatApiClient().getFavUsers(); // // userGroups = await ChatApiClient().getGroupsByUserId(); // if (favUList.response != null && recentChat.response != null) { // favUsersList = favUList.response!; // favUsersList.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); // for (dynamic user in recentChat.response!) { // for (dynamic favUser in favUList.response!) { // if (user.id == favUser.id) { // user.isFav = favUser.isFav; // } // } // } // } // pChatHistory = recentChat.response ?? []; // uGroups = userGroups.groupresponse ?? []; // pChatHistory!.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); // searchedChats = pChatHistory; // isLoading = false; // await invokeUserChatHistoryNotDeliveredAsync(userId: int.parse(AppState().chatDetails!.response!.id.toString())); // sort(); // notifyListeners(); // if (searchedChats!.isNotEmpty || favUsersList.isNotEmpty) { // getUserImages(); // } // } Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async { await chatHubConnection!.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); return ""; } // void getSingleUserChatHistory({required int senderUID, required int receiverUID, required bool loadMore, bool isNewChat = false}) async { // isLoading = true; // if (isNewChat) userChatHistory = []; // if (!loadMore) paginationVal = 0; // isChatScreenActive = true; // receiverID = receiverUID; // Response response = await ChatApiClient().getSingleUserChatHistory(senderUID: senderUID, receiverUID: receiverUID, loadMore: loadMore, paginationVal: paginationVal); // if (response.statusCode == 204) { // if (isNewChat) { // userChatHistory = []; // } else if (loadMore) {} // } else { // if (loadMore) { // List temp = getSingleUserChatModel(response.body).reversed.toList(); // userChatHistory.addAll(temp); // } else { // userChatHistory = getSingleUserChatModel(response.body).reversed.toList(); // } // } // isLoading = false; // notifyListeners(); // // if (isChatScreenActive && receiverUID == receiverID) { // markRead(userChatHistory, receiverUID); // } // // generateConvId(); // } // // void generateConvId() async { // Uuid uuid = const Uuid(); // chatCID = uuid.v4(); // } void markRead(List data, String receiverID) { for (SingleUserChatModel element in data) { // if (AppState().chatDetails!.response!.id! == element.targetUserId) { if (element.isSeen != null) { if (!element.isSeen!) { element.isSeen = true; dynamic data = [ { "userChatHistoryId": element.userChatHistoryId, "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, "isDelivered": true, "isSeen": true, } ]; updateUserChatHistoryStatusAsync(data); notifyListeners(); } // } } } } Future resetCount({ required int moduleId, required int referenceNo, String? userId, }) async { try { return await ChatApiClient().resetCountApi(moduleId, referenceNo, userId); } catch (e, stack) { debugPrint('resetCount error: $e'); rethrow; } } void updateUserChatHistoryStatusAsync(List data) { try { chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } } void updateUserChatHistoryOnMsg(List data) { try { chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } } // List getSingleUserChatModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); List getSingleUserChatModel(String str) { final dynamic decodedJson = json.decode(str); // Check if the decoded JSON is already a List if (decodedJson is List) { return List.from(decodedJson.map((x) => SingleUserChatModel.fromJson(x))); } // If it's a Map (a single object), wrap it in a list else if (decodedJson is Map) { return [SingleUserChatModel.fromJson(decodedJson)]; } // Handle unexpected types else { throw const FormatException('Expected a JSON object or a list of JSON objects.'); } } // List getGroupChatHistoryAsync(String str) => // List.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x))); // Future uploadAttachments(String userId, File file, String fileSource) async { dynamic result; try { Map jsonData = { "IsContextual": true.toString(), "ModuleCode": moduleID.toString(), "ReferenceId": referenceID.toString(), "ReferenceType": "ticket", "ConversationId": chatParticipantModel!.id.toString(), "TargetUserId": receiverID, "SendMessage": true.toString(), }; Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource, jsonData: jsonData); if (response != null) { result = response; } else { result = []; } } catch (e) { throw e; } return result; } Future> getUnReadMessages(String employeeId) async { // employeeId = "FMEngineer"; try { Response response = await ApiClient().getJsonForResponse( "${URLs.unreadMessages}?retrieveAll=true", headers: {'x-api-key': URLs.chatApiKey, 'x-employee-number': employeeId}, ); if (response.statusCode == 200) { List data = jsonDecode(response.body); return data.map((elemet) => UnReadMessage.fromJson(elemet)).toList(); } else { return []; } } catch (error) { return []; } } // void updateUserChatStatus(List? args) { // dynamic items = args!.toList(); // for (var cItem in items[0]) { // for (SingleUserChatModel chat in userChatHistory) { // if (cItem["contantNo"].toString() == chat.contantNo.toString()) { // chat.isSeen = cItem["isSeen"]; // chat.isDelivered = cItem["isDelivered"]; // } // } // } // notifyListeners(); // } void getGroupUserStatus(List? args) { //note: need to implement this function... print(args); } Future markMessageAsRead(int messageId) async { final senderId = sender?.userId; if (senderId == null) return; chatHubConnection?.invoke( "SendMessageReadAsync", args: [messageId, senderId], ); } void onChatSeen(List? args) { dynamic items = args!.toList(); // for (var user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // notifyListeners(); } void userCountAsync(List? args) { dynamic items = args!.toList(); // logger.d(items); //logger.d("---------------------------------User Count Async -------------------------------------"); //logger.d(items); // for (var user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // notifyListeners(); } // void updateChatHistoryWindow(List? args) { // dynamic items = args!.toList(); // if (kDebugMode) { // logger.i("---------------------------------Update Chat History Windows Async -------------------------------------"); // } // logger.d(items); // // for (var user in searchedChats!) { // // if (user.id == items.first["id"]) { // // user.userStatus = items.first["userStatus"]; // // } // // } // // notifyListeners(); // } // void chatNotDelivered(List? args) { // dynamic items = args!.toList(); // for (dynamic item in items[0]) { // for (ChatUser element in searchedChats!) { // if (element.id == item["currentUserId"]) { // int? val = element.unreadMessageCount ?? 0; // element.unreadMessageCount = val! + 1; // } // } // } // notifyListeners(); // } // // void changeStatus(List? args) { // dynamic items = args!.toList(); // for (ChatUser user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // if (teamMembersList.isNotEmpty) { // for (ChatUser user in teamMembersList!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // } // // notifyListeners(); // } // // void filter(String value) async { // List? tmp = []; // if (value.isEmpty || value == "") { // tmp = pChatHistory; // } else { // for (ChatUser element in pChatHistory!) { // if (element.userName!.toLowerCase().contains(value.toLowerCase())) { // tmp.add(element); // } // } // } // searchedChats = tmp; // notifyListeners(); // } Timer? timer; Future OnTypingAsync(List? parameters) async { // String empId = parameters!.first as String; isTyping = true; notifyListeners(); if (timer?.isActive ?? false) { timer!.cancel(); } timer = Timer(const Duration(milliseconds: 2500), () { isTyping = false; notifyListeners(); }); } Future OnStopTypingAsync(List? parameters) async { if (timer?.isActive ?? false) { timer!.cancel(); } isTyping = false; notifyListeners(); } Future onSeenUserChatAsync(List? parameters) async { try { if (parameters == null || parameters.isEmpty) { log('onSeenUserChatAsync: parameters are null or empty'); return; } final parm = parameters.first; if (parm is! List || parm.isEmpty) { log('onSeenUserChatAsync: parm is not a valid list'); return; } final firstItem = parm.first; if (firstItem is! Map) { log('onSeenUserChatAsync: firstItem is not a Map'); return; } await chatHubConnection!.invoke( "AckSeenAsync", args: [ firstItem['currentUserId'], [firstItem['userChatHistoryId']], ], ); } catch (e, stackTrace) { log('onSeenUserChatAsync error: $e'); log('StackTrace: $stackTrace'); } } Future onAckSeenAsync(List? parameters) async { try { if (parameters == null || parameters.isEmpty) { log('onAckSeenAsync: parameters are null or empty'); return; } final parm = parameters.first; log('parm onAckSeenAsync $parm'); if (chatResponseList.isEmpty) { log('onAckSeenAsync: chatResponseList is empty'); return; } log('last list item id ${chatResponseList.first.toJson()}'); chatResponseList.first.isSeen = true; notifyListeners(); } catch (e, stackTrace) { log('onAckSeenAsync error: $e'); log('StackTrace: $stackTrace'); } } Future onSubmitChatAsync(List? parameters) async { if (kDebugMode) print("OnSubmitChatAsync:$parameters"); if (parameters == null || parameters.isEmpty) return; try { for (dynamic msg in parameters) { var data = getSingleUserChatModel(jsonEncode(msg)); if (kDebugMode) print('Parsed message: $data'); } } catch (e) { if (kDebugMode) print('Error in OnSubmitChatAsync: $e'); } } Future onMsgReceived1(List? parameters) async { print("onMsgReceived1:$parameters"); } Future onMsgReceived(List? parameters) async { List data = []; print("OnMessageReceivedAsync:$parameters"); for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); // ...existing code... } // ...existing code... userChatHistory?.insert(0, data.first); notifyListeners(); // ...existing code... } // ==================== PHASE 1: CALL INFRASTRUCTURE ==================== // All call-related methods are placed at the end to avoid modifying existing chat logic /// Start a new audio or video call Future startCall(Participants recipient, CallType callType) async { if (callStatus != CallStatus.idle) { if (kDebugMode) { print('โš ๏ธ Cannot start call - already in a call'); } return; } if (sender == null) { if (kDebugMode) { print('โš ๏ธ Cannot start call - sender is null'); } return; } callStatus = CallStatus.checkingPermissions; notifyListeners(); try { // Generate unique call ID final callId = const Uuid().v4(); final isVideo = callType == CallType.video; // Request permissions final micStatus = await Permission.microphone.request(); if (!micStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); if (kDebugMode) { print('โš ๏ธ Microphone permission denied'); } return; } if (isVideo) { final cameraStatus = await Permission.camera.request(); if (!cameraStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); if (kDebugMode) { print('โš ๏ธ Camera permission denied'); } return; } } // Create call session currentCall = CallSession( callId: callId, type: callType, direction: CallDirection.outgoing, peerId: recipient.employeeNumber ?? '', peerName: recipient.userName ?? 'Unknown', peerAvatar: recipient.image, startTime: DateTime.now(), ); // Invoke SignalR CallUserAsync await chatHubConnection?.invoke( 'CallUserAsync', args: [ sender!.employeeNumber ?? '', recipient.employeeNumber ?? '', isVideo, ], ); callStatus = CallStatus.outgoingRinging; notifyListeners(); // Start 60s timeout _callTimeoutTimer?.cancel(); _callTimeoutTimer = Timer(const Duration(seconds: 60), () { if (callStatus == CallStatus.outgoingRinging) { _handleCallTimeout(); } }); if (kDebugMode) { print('โœ… Call started: $callId'); } } catch (e) { if (kDebugMode) { print('โŒ Error starting call: $e'); } callStatus = CallStatus.idle; currentCall = null; notifyListeners(); } } /// Toggle microphone mute void toggleMute() { isMuted = !isMuted; notifyListeners(); // Notify peer via SignalR if (currentCall != null && sender != null) { chatHubConnection?.invoke( 'AudioToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], ); } if (kDebugMode) { print('๐ŸŽค Microphone ${isMuted ? "muted" : "unmuted"}'); } } /// Toggle speakerphone void toggleSpeaker() { isSpeakerOn = !isSpeakerOn; notifyListeners(); if (kDebugMode) { print('๐Ÿ”Š Speaker ${isSpeakerOn ? "on" : "off"}'); } } /// Toggle camera (video only) void toggleCamera() { isCameraOn = !isCameraOn; notifyListeners(); // Notify peer via SignalR if (currentCall != null && sender != null) { chatHubConnection?.invoke( 'CameraToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], ); } if (kDebugMode) { print('๐Ÿ“น Camera ${isCameraOn ? "on" : "off"}'); } } /// Switch between front and rear camera void switchCamera() { if (kDebugMode) { print('๐Ÿ”„ Switch camera requested'); } // Implementation will be added with WebRTC service notifyListeners(); } /// Switch from audio call to video call Future switchToVideoCall() async { if (currentCall == null || currentCall!.type == CallType.video) { return; } // Request camera permission final cameraStatus = await Permission.camera.request(); if (!cameraStatus.isGranted) { if (kDebugMode) { print('โš ๏ธ Camera permission denied'); } return; } // Update call type (create new session to maintain immutability) currentCall = CallSession( callId: currentCall!.callId, type: CallType.video, direction: currentCall!.direction, peerId: currentCall!.peerId, peerName: currentCall!.peerName, peerAvatar: currentCall!.peerAvatar, startTime: currentCall!.startTime, sessionId: currentCall!.sessionId, sdpOffer: currentCall!.sdpOffer, sdpAnswer: currentCall!.sdpAnswer, ); isCameraOn = true; notifyListeners(); if (kDebugMode) { print('๐Ÿ“น Switched to video call'); } } /// End the current call Future hangUp() async { if (currentCall == null || callStatus == CallStatus.idle) { return; } try { // Invoke SignalR HangUpAsync await chatHubConnection?.invoke( 'HangUpAsync', args: [ sender?.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); if (kDebugMode) { print('๐Ÿ“ž Call ended'); } } catch (e) { if (kDebugMode) { print('โš ๏ธ Error ending call: $e'); } } _teardownCall(); } /// Handle call timeout (60s no answer) void _handleCallTimeout() { if (kDebugMode) { print('โฑ๏ธ Call timeout - no answer'); } // Invoke CallMissedAsync chatHubConnection?.invoke( 'CallMissedAsync', args: [ sender?.employeeNumber ?? '', currentCall?.peerId ?? '', moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); _teardownCall(); } /// Clean up call resources void _teardownCall() { _callTimeoutTimer?.cancel(); _callDurationTimer?.cancel(); callStatus = CallStatus.idle; currentCall = null; callDuration = Duration.zero; isMuted = false; isSpeakerOn = false; isCameraOn = true; isPeerMuted = false; isPeerCameraOn = true; notifyListeners(); if (kDebugMode) { print('๐Ÿงน Call resources cleaned up'); } } /// Start call duration timer when connected void _startCallDurationTimer() { _callDurationTimer?.cancel(); callDuration = Duration.zero; _callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { callDuration = Duration(seconds: callDuration.inSeconds + 1); notifyListeners(); }); if (kDebugMode) { print('โฑ๏ธ Call duration timer started'); } } /// Accept incoming call Future acceptCall() async { if (currentCall == null || callStatus != CallStatus.incomingRinging) { return; } // Request permissions final micStatus = await Permission.microphone.request(); if (!micStatus.isGranted) { await declineCall('permission_denied'); return; } if (currentCall!.type == CallType.video) { final cameraStatus = await Permission.camera.request(); if (!cameraStatus.isGranted) { await declineCall('permission_denied'); return; } } try { // Invoke SignalR AnswerCallAsync await chatHubConnection?.invoke( 'AnswerCallAsync', args: [ sender?.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); callStatus = CallStatus.connecting; notifyListeners(); if (kDebugMode) { print('โœ… Call accepted'); } } catch (e) { if (kDebugMode) { print('โŒ Error accepting call: $e'); } _teardownCall(); } } /// Decline incoming call Future declineCall(String reason) async { if (currentCall == null) { return; } try { // Invoke SignalR CallDeclinedAsync await chatHubConnection?.invoke( 'CallDeclinedAsync', args: [ sender?.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); if (kDebugMode) { print('๐Ÿ“ž Call declined: $reason'); } } catch (e) { if (kDebugMode) { print('โš ๏ธ Error declining call: $e'); } } _teardownCall(); } /// Register call-related SignalR event handlers void _registerCallHandlers() { if (chatHubConnection == null) return; // Incoming call chatHubConnection!.on("OnIncomingCallAsync", _onIncomingCall); // Call accepted chatHubConnection!.on("OnCallAcceptedAsync", _onCallAccepted); // Call declined chatHubConnection!.on("OnCallDeclinedAsync", _onCallDeclined); // Call ended chatHubConnection!.on("OnHangUpAsync", _onCallEnded); // Peer audio toggle chatHubConnection!.on("OnAudioToggle", _onPeerAudioToggle); // Peer camera toggle chatHubConnection!.on("OnCameraToggle", _onPeerCameraToggle); if (kDebugMode) { print('โœ… Call handlers registered'); } } /// Handle incoming call event void _onIncomingCall(List? args) { if (args == null || args.isEmpty) return; try { final data = args.first as Map; final callerId = data['sourceUserId'] as String?; final callerName = data['userName'] as String? ?? 'Unknown'; final isVideo = data['isVideoCall'] as bool? ?? false; // Check if already in a call if (callStatus != CallStatus.idle) { // Auto-reject with busy status chatHubConnection?.invoke( 'CallDeclinedAsync', args: [ sender?.employeeNumber ?? '', callerId ?? '', moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); return; } // Create call session currentCall = CallSession( callId: const Uuid().v4(), type: isVideo ? CallType.video : CallType.audio, direction: CallDirection.incoming, peerId: callerId ?? '', peerName: callerName, peerAvatar: null, startTime: DateTime.now(), ); callStatus = CallStatus.incomingRinging; notifyListeners(); if (kDebugMode) { print('๐Ÿ“ž Incoming ${isVideo ? "video" : "audio"} call from $callerName'); } } catch (e) { if (kDebugMode) { print('โŒ Error handling incoming call: $e'); } } } /// Handle call accepted event void _onCallAccepted(List? args) { if (callStatus == CallStatus.outgoingRinging) { _callTimeoutTimer?.cancel(); callStatus = CallStatus.connecting; notifyListeners(); if (kDebugMode) { print('โœ… Call was accepted'); } } } /// Handle call declined event void _onCallDeclined(List? args) { if (args == null || args.isEmpty) return; try { final reason = args.first as String? ?? 'declined'; if (kDebugMode) { print('๐Ÿ“ž Call declined: $reason'); } _teardownCall(); } catch (e) { if (kDebugMode) { print('โŒ Error handling call declined: $e'); } } } /// Handle call ended event void _onCallEnded(List? args) { if (kDebugMode) { print('๐Ÿ“ž Call ended by peer'); } _teardownCall(); } /// Handle peer audio toggle event void _onPeerAudioToggle(List? args) { isPeerMuted = !isPeerMuted; notifyListeners(); if (kDebugMode) { print('๐ŸŽค Peer ${isPeerMuted ? "muted" : "unmuted"}'); } } /// Handle peer camera toggle event void _onPeerCameraToggle(List? args) { isPeerCameraOn = !isPeerCameraOn; notifyListeners(); if (kDebugMode) { print('๐Ÿ“น Peer camera ${isPeerCameraOn ? "on" : "off"}'); } } /// Mark call as connected and start duration timer void markCallConnected() { if (callStatus == CallStatus.connecting) { callStatus = CallStatus.connected; _startCallDurationTimer(); notifyListeners(); if (kDebugMode) { print('โœ… Call connected'); } } } }