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

1666 lines
62 KiB
Dart

9 months ago
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
9 months ago
import 'dart:io';
import 'dart:typed_data';
import 'package:audio_waveforms/audio_waveforms.dart';
// import 'package:easy_localization/easy_localization.dart';
9 months ago
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';
9 months ago
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';
9 months ago
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';
8 months ago
import 'package:test_sa/controllers/api_routes/api_manager.dart';
9 months ago
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/model/chat_login_response_model.dart';
9 months ago
import 'package:uuid/uuid.dart';
import 'package:flutter/material.dart' as Material;
import 'package:flutter/material.dart'; // NEW - for WidgetsBinding and showDialog
9 months ago
import 'api_client.dart';
import 'chat_api_client.dart';
import 'model/chat_attachment_model.dart';
import 'model/chat_participant_model.dart';
9 months ago
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'; // NEW - Import incoming call dialog
9 months ago
8 months ago
//Need to refactor this remove unused code.
HubConnection? chatHubConnection;
9 months ago
class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool isTyping = false;
bool chatLoginTokenLoading = false;
ChatLoginResponse? chatLoginResponse;
bool chatParticipantLoading = false;
ChatParticipantModel? chatParticipantModel;
bool userChatHistoryLoading = false;
// UserChatHistoryModel? userChatHistory;
List<SingleUserChatModel>? userChatHistory;
bool messageIsSending = false;
List<SingleUserChatModel> 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;
// NEW: Call handlers registration status
bool _callHandlersRegistered = false;
bool get areCallHandlersRegistered => _callHandlersRegistered;
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<void> _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<void> 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<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 {
log('i am called ...');
9 months ago
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;
}
9 months ago
} catch (ex) {
if (kDebugMode) {
print('⚠️ Error in getUserAutoLoginTokenSilent: $ex');
}
log('❌ EXCEPTION in getUserAutoLoginTokenSilent: $ex');
9 months ago
}
chatLoginTokenLoading = false;
if (isMounted) {
notifyListeners();
}
}
9 months ago
// 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) {}
// }
9 months ago
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
}
}
6 months ago
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();
}
}
9 months ago
// 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;
9 months ago
}
// 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 {
// Dispose existing connection if any
await _disposeConnection();
chatHubConnection = await getHubConnection();
await chatHubConnection!.start();
// NEW: Log connection details for diagnostics
log('═══════════════════════════════════════════', name: 'ChatProvider');
log('🔌 SignalR Hub Connection: Started', name: 'ChatProvider');
log('🔑 Connection ID: ${chatHubConnection!.connectionId ?? "NULL"}', name: 'ChatProvider');
log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider');
log('💬 Conversation ID: $conversationID', name: 'ChatProvider');
log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider');
log('═══════════════════════════════════════════', name: 'ChatProvider');
if (kDebugMode) {
print("🔌 SignalR Hub Connection: Started");
}
await chatHubConnection!.invoke("JoinConversation", args: [conversationID]);
log('✅ Joined conversation: $conversationID', name: 'ChatProvider');
// Register ALL event handlers (chat + call)
_registerAllEventHandlers();
//group On message
// chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived);
} catch (e) {
log('❌ [CONNECTION] Error building SignalR connection: $e', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Error building SignalR connection: $e');
}
// Clean up on error
await _disposeConnection();
rethrow; // Rethrow so caller knows about the error
9 months ago
}
}
/// Register all event handlers (chat + call) - called on initial connection and after reconnect
void _registerAllEventHandlers() {
if (chatHubConnection == null) {
log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider');
return;
}
log('🔧 [EVENT HANDLERS] Registering all event handlers...', name: 'ChatProvider');
// 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
_registerCallHandlers();
log('✅ [EVENT HANDLERS] All event handlers registered successfully', name: 'ChatProvider');
}
9 months ago
Future<HubConnection> getHubConnection() async {
if (kDebugMode) {
print('🔧 Creating new SignalR hub connection...');
}
9 months ago
HubConnection hub;
HttpConnectionOptions httpOp = HttpConnectionOptions(
skipNegotiation: false,
logMessageContent: true,
);
9 months ago
hub = HubConnectionBuilder()
.withUrl("${URLs.chatHubUrlChat}?UserId=${chatLoginResponse!.userId}&source=Desktop&access_token=${chatLoginResponse!.token}", options: httpOp)
9 months ago
.withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000]).build();
// NEW: Add global event logging in debug mode AND re-register handlers on reconnect
if (kDebugMode) {
// Log connection state changes
hub.onclose(({Exception? error}) {
log('🔴 [SignalR] Connection closed: $error', name: 'ChatProvider');
});
hub.onreconnecting(({Exception? error}) {
log('🟡 [SignalR] Reconnecting: $error', name: 'ChatProvider');
});
hub.onreconnected(({String? connectionId}) async {
log('🟢 [SignalR] Reconnected: $connectionId', name: 'ChatProvider');
log('🔄 [SignalR] Re-registering all event handlers after reconnection...', name: 'ChatProvider');
// CRITICAL: Re-register all event handlers after reconnection
_registerAllEventHandlers();
log('✅ [SignalR] Event handlers re-registered after reconnection', name: 'ChatProvider');
});
print('✅ SignalR hub connection created with debug logging and auto-reregister');
} else {
// Production: still need to re-register handlers on reconnect
hub.onreconnected(({String? connectionId}) async {
_registerAllEventHandlers();
});
print('✅ SignalR hub connection created');
}
9 months ago
return hub;
}
void registerEvents() {
// chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus);
9 months ago
// chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived);
// chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync);
// chatHubConnection.on("OnUserTypingAsync", onUserTyping);
chatHubConnection?.on("OnUserCountAsync", userCountAsync);
9 months ago
// chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow);
// chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered);
// chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus);
// chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus);
9 months ago
//
// {"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<void> 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();
// }
// }
9 months ago
Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async {
await chatHubConnection!.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]);
9 months ago
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<SingleUserChatModel> 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<SingleUserChatModel> 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();
}
// }
}
}
}
9 months ago
8 months ago
Future<bool> resetCount({
required int moduleId,
required int referenceNo,
String? userId,
8 months ago
}) async {
try {
return await ChatApiClient().resetCountApi(moduleId, referenceNo, userId);
8 months ago
} catch (e, stack) {
debugPrint('resetCount error: $e');
rethrow;
}
}
9 months ago
void updateUserChatHistoryStatusAsync(List data) {
try {
chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]);
9 months ago
} catch (e) {
throw e;
}
}
void updateUserChatHistoryOnMsg(List data) {
try {
chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]);
9 months ago
} catch (e) {
throw e;
}
}
// List<SingleUserChatModel> getSingleUserChatModel(String str) => List<SingleUserChatModel>.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x)));
List<SingleUserChatModel> getSingleUserChatModel(String str) {
final dynamic decodedJson = json.decode(str);
// Check if the decoded JSON is already a List
if (decodedJson is List) {
return List<SingleUserChatModel>.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<String, dynamic>) {
return [SingleUserChatModel.fromJson(decodedJson)];
}
// Handle unexpected types
else {
throw const FormatException('Expected a JSON object or a list of JSON objects.');
}
}
9 months ago
// List<groupchathistory.GetGroupChatHistoryAsync> getGroupChatHistoryAsync(String str) =>
// List<groupchathistory.GetGroupChatHistoryAsync>.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x)));
//
Future<dynamic> uploadAttachments(String userId, File file, String fileSource) async {
dynamic result;
try {
Map<String, String> 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<List<UnReadMessage>> 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<Object?>? 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();
// }
9 months ago
void getGroupUserStatus(List<Object?>? args) {
//note: need to implement this function...
print(args);
}
Future<void> markMessageAsRead(int messageId) async {
final senderId = sender?.userId;
if (senderId == null) return;
chatHubConnection?.invoke(
"SendMessageReadAsync",
args: [messageId, senderId],
);
}
9 months ago
void onChatSeen(List<Object?>? 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<Object?>? 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<Object?>? 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<Object?>? 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<Object?>? 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<ChatUser>? 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<void> OnTypingAsync(List<Object?>? 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<void> OnStopTypingAsync(List<Object?>? parameters) async {
if (timer?.isActive ?? false) {
timer!.cancel();
}
isTyping = false;
notifyListeners();
}
Future<void> onSeenUserChatAsync(List<Object?>? 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<String, dynamic>) {
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<void> onAckSeenAsync(List<Object?>? 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<void> onSubmitChatAsync(List<Object?>? 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<void> onMsgReceived1(List<Object?>? parameters) async {
print("onMsgReceived1:$parameters");
}
9 months ago
Future<void> onMsgReceived(List<Object?>? parameters) async {
List<SingleUserChatModel> data = [];
print("OnMessageReceivedAsync:$parameters");
for (dynamic msg in parameters!) {
data = getSingleUserChatModel(jsonEncode(msg));
// ...existing code...
9 months ago
}
// ...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<void> startCall(Participants recipient, CallType callType) async {
log('🔵 [CALL] startCall() invoked', name: 'ChatProvider');
log('🔵 [CALL] Parameters - recipient: ${recipient.employeeNumber}, callType: $callType', name: 'ChatProvider');
log('🔵 [CALL] Current callStatus: $callStatus', name: 'ChatProvider');
if (callStatus != CallStatus.idle) {
log('⚠️ [CALL] Cannot start call - already in a call (status: $callStatus)', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Cannot start call - already in a call');
}
return;
}
if (sender == null) {
log('❌ [CALL] Cannot start call - sender is null', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Cannot start call - sender is null');
}
return;
}
log('🔵 [CALL] Sender: ${sender!.employeeNumber}', name: 'ChatProvider');
callStatus = CallStatus.checkingPermissions;
notifyListeners();
log('🔵 [CALL] Status changed to: checkingPermissions', name: 'ChatProvider');
try {
// Generate unique call ID
final callId = const Uuid().v4();
final isVideo = callType == CallType.video;
log('🔵 [CALL] Generated callId: $callId', name: 'ChatProvider');
log('🔵 [CALL] Call type: ${isVideo ? "VIDEO" : "AUDIO"}', name: 'ChatProvider');
// Request permissions
log('🔵 [CALL] Requesting microphone permission...', name: 'ChatProvider');
final micStatus = await Permission.microphone.request();
log('🔵 [CALL] Microphone permission status: ${micStatus.name}', name: 'ChatProvider');
if (!micStatus.isGranted) {
callStatus = CallStatus.idle;
currentCall = null;
notifyListeners();
log('❌ [CALL] Microphone permission denied - aborting call', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Microphone permission denied');
}
return;
}
if (isVideo) {
log('🔵 [CALL] Requesting camera permission...', name: 'ChatProvider');
final cameraStatus = await Permission.camera.request();
log('🔵 [CALL] Camera permission status: ${cameraStatus.name}', name: 'ChatProvider');
if (!cameraStatus.isGranted) {
callStatus = CallStatus.idle;
currentCall = null;
notifyListeners();
log('❌ [CALL] Camera permission denied - aborting call', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Camera permission denied');
}
return;
}
}
// Create call session
log('🔵 [CALL] Creating CallSession...', name: 'ChatProvider');
currentCall = CallSession(
callId: callId,
type: callType,
direction: CallDirection.outgoing,
peerId: recipient.employeeNumber ?? '',
peerName: recipient.userName ?? 'Unknown',
peerAvatar: recipient.image,
startTime: DateTime.now(),
);
log('✅ [CALL] CallSession created - peerId: ${currentCall!.peerId}, peerName: ${currentCall!.peerName}', name: 'ChatProvider');
// Invoke SignalR CallUserAsync
log('🔵 [CALL] Checking SignalR connection status...', name: 'ChatProvider');
if (chatHubConnection == null) {
log('❌ [CALL] SignalR connection is null!', name: 'ChatProvider');
throw Exception('SignalR connection not established');
}
log('🔵 [CALL] SignalR connection state: ${chatHubConnection!.state}', name: 'ChatProvider');
log('🔵 [CALL] Invoking CallUserAsync with args:', name: 'ChatProvider');
log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider');
log(' - target: ${recipient.employeeNumber}', name: 'ChatProvider');
log(' - isVideoCall: $isVideo', name: 'ChatProvider');
await chatHubConnection?.invoke(
'CallUserAsync',
args: [
sender!.employeeNumber ?? '',
recipient.employeeNumber ?? '',
isVideo,
],
);
log('✅ [CALL] CallUserAsync invoked successfully', name: 'ChatProvider');
callStatus = CallStatus.outgoingRinging;
notifyListeners();
log('🔵 [CALL] Status changed to: outgoingRinging', name: 'ChatProvider');
// Start 60s timeout
_callTimeoutTimer?.cancel();
log('🔵 [CALL] Starting 60s timeout timer...', name: 'ChatProvider');
_callTimeoutTimer = Timer(const Duration(seconds: 60), () {
log('⏱️ [CALL] Timeout timer fired', name: 'ChatProvider');
if (callStatus == CallStatus.outgoingRinging) {
_handleCallTimeout();
}
});
log('✅ [CALL] Call started successfully - callId: $callId', name: 'ChatProvider');
if (kDebugMode) {
print('✅ Call started: $callId');
}
} catch (e, stackTrace) {
log('❌ [CALL] Error starting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('❌ Error starting call: $e');
}
callStatus = CallStatus.idle;
currentCall = null;
notifyListeners();
}
9 months ago
}
/// Toggle microphone mute
void toggleMute() {
log('🔵 [CALL] toggleMute() called - current state: $isMuted', name: 'ChatProvider');
isMuted = !isMuted;
notifyListeners();
log('🔵 [CALL] Microphone ${isMuted ? "muted" : "unmuted"}', name: 'ChatProvider');
// Notify peer via SignalR
if (currentCall != null && sender != null) {
log('🔵 [CALL] Invoking AudioToggle to notify peer', name: 'ChatProvider');
log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall!.peerId}', name: 'ChatProvider');
chatHubConnection?.invoke(
'AudioToggle',
args: [sender!.employeeNumber ?? '', currentCall!.peerId],
).then((_) {
log('✅ [CALL] AudioToggle invoked successfully', name: 'ChatProvider');
}).catchError((e) {
log('❌ [CALL] Error invoking AudioToggle: $e', name: 'ChatProvider');
});
} else {
log('⚠️ [CALL] Cannot notify peer - currentCall or sender is null', name: 'ChatProvider');
}
if (kDebugMode) {
print('🎤 Microphone ${isMuted ? "muted" : "unmuted"}');
9 months ago
}
}
/// Toggle speakerphone
void toggleSpeaker() {
log('🔵 [CALL] toggleSpeaker() called - current state: $isSpeakerOn', name: 'ChatProvider');
isSpeakerOn = !isSpeakerOn;
notifyListeners();
log('🔵 [CALL] Speaker ${isSpeakerOn ? "on" : "off"}', name: 'ChatProvider');
if (kDebugMode) {
print('🔊 Speaker ${isSpeakerOn ? "on" : "off"}');
}
9 months ago
}
/// Toggle camera (video only)
void toggleCamera() {
log('🔵 [CALL] toggleCamera() called - current state: $isCameraOn', name: 'ChatProvider');
isCameraOn = !isCameraOn;
notifyListeners();
log('🔵 [CALL] Camera ${isCameraOn ? "on" : "off"}', name: 'ChatProvider');
// Notify peer via SignalR
if (currentCall != null && sender != null) {
log('🔵 [CALL] Invoking CameraToggle to notify peer', name: 'ChatProvider');
log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall!.peerId}', name: 'ChatProvider');
chatHubConnection?.invoke(
'CameraToggle',
args: [sender!.employeeNumber ?? '', currentCall!.peerId],
).then((_) {
log('✅ [CALL] CameraToggle invoked successfully', name: 'ChatProvider');
}).catchError((e) {
log('❌ [CALL] Error invoking CameraToggle: $e', name: 'ChatProvider');
});
} else {
log('⚠️ [CALL] Cannot notify peer - currentCall or sender is null', name: 'ChatProvider');
}
if (kDebugMode) {
print('📹 Camera ${isCameraOn ? "on" : "off"}');
}
}
/// Switch between front and rear camera
void switchCamera() {
log('🔵 [CALL] switchCamera() called', name: 'ChatProvider');
if (kDebugMode) {
print('🔄 Switch camera requested');
}
// Implementation will be added with WebRTC service
notifyListeners();
}
/// Switch from audio call to video call
Future<void> switchToVideoCall() async {
log('🔵 [CALL] switchToVideoCall() called', name: 'ChatProvider');
log('🔵 [CALL] Current call type: ${currentCall?.type}', name: 'ChatProvider');
if (currentCall == null || currentCall!.type == CallType.video) {
log('⚠️ [CALL] Cannot switch - currentCall is null or already video', name: 'ChatProvider');
return;
}
// Request camera permission
log('🔵 [CALL] Requesting camera permission for upgrade...', name: 'ChatProvider');
final cameraStatus = await Permission.camera.request();
log('🔵 [CALL] Camera permission status: ${cameraStatus.name}', name: 'ChatProvider');
if (!cameraStatus.isGranted) {
log('❌ [CALL] Camera permission denied - cannot upgrade to video', name: 'ChatProvider');
if (kDebugMode) {
print('⚠️ Camera permission denied');
}
return;
}
// Update call type (create new session to maintain immutability)
log('🔵 [CALL] Upgrading call to video...', name: 'ChatProvider');
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();
log('✅ [CALL] Successfully switched to video call', name: 'ChatProvider');
if (kDebugMode) {
print('📹 Switched to video call');
9 months ago
}
}
/// End the current call
Future<void> hangUp() async {
log('🔵 [CALL] hangUp() called', name: 'ChatProvider');
log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider');
log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider');
if (currentCall == null || callStatus == CallStatus.idle) {
log('⚠️ [CALL] No active call to hang up', name: 'ChatProvider');
return;
}
try {
// Invoke SignalR HangUpAsync
log('🔵 [CALL] Invoking HangUpAsync with args:', name: 'ChatProvider');
log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall!.peerId}', name: 'ChatProvider');
log(' - moduleCode: $moduleID', name: 'ChatProvider');
log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider');
log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider');
await chatHubConnection?.invoke(
'HangUpAsync',
args: [
sender?.employeeNumber ?? '',
currentCall!.peerId,
moduleID.toString(),
referenceID?.toString() ?? '',
chatParticipantModel?.id?.toString() ?? '',
],
);
log('✅ [CALL] HangUpAsync invoked successfully', name: 'ChatProvider');
if (kDebugMode) {
print('📞 Call ended');
}
} catch (e, stackTrace) {
log('❌ [CALL] Error invoking HangUpAsync: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('⚠️ Error ending call: $e');
}
}
_teardownCall();
9 months ago
}
/// Handle call timeout (60s no answer)
void _handleCallTimeout() {
log('⏱️ [CALL] _handleCallTimeout() - Call timeout after 60s', name: 'ChatProvider');
log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider');
if (kDebugMode) {
print('⏱️ Call timeout - no answer');
}
// Invoke CallMissedAsync
log('🔵 [CALL] Invoking CallMissedAsync with args:', name: 'ChatProvider');
log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall?.peerId}', name: 'ChatProvider');
log(' - moduleCode: $moduleID', name: 'ChatProvider');
log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider');
log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider');
chatHubConnection?.invoke(
'CallMissedAsync',
args: [
sender?.employeeNumber ?? '',
currentCall?.peerId ?? '',
moduleID.toString(),
referenceID?.toString() ?? '',
chatParticipantModel?.id?.toString() ?? '',
],
).then((_) {
log('✅ [CALL] CallMissedAsync invoked successfully', name: 'ChatProvider');
}).catchError((e) {
log('❌ [CALL] Error invoking CallMissedAsync: $e', name: 'ChatProvider');
});
_teardownCall();
9 months ago
}
/// Clean up call resources
void _teardownCall() {
log('🧹 [CALL] _teardownCall() - Cleaning up call resources', name: 'ChatProvider');
log('🔵 [CALL] Previous status: $callStatus', name: 'ChatProvider');
log('🔵 [CALL] Call duration: ${callDuration.inSeconds}s', name: 'ChatProvider');
_callTimeoutTimer?.cancel();
log('🔵 [CALL] Timeout timer cancelled', name: 'ChatProvider');
_callDurationTimer?.cancel();
log('🔵 [CALL] Duration timer cancelled', name: 'ChatProvider');
callStatus = CallStatus.idle;
currentCall = null;
callDuration = Duration.zero;
isMuted = false;
isSpeakerOn = false;
isCameraOn = true;
isPeerMuted = false;
isPeerCameraOn = true;
notifyListeners();
log('✅ [CALL] Call resources cleaned up - status reset to idle', name: 'ChatProvider');
if (kDebugMode) {
print('🧹 Call resources cleaned up');
9 months ago
}
}
/// Start call duration timer when connected
void _startCallDurationTimer() {
log('⏱️ [CALL] _startCallDurationTimer() - Starting call duration counter', name: 'ChatProvider');
_callDurationTimer?.cancel();
callDuration = Duration.zero;
_callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
callDuration = Duration(seconds: callDuration.inSeconds + 1);
notifyListeners();
});
log('✅ [CALL] Duration timer started', name: 'ChatProvider');
if (kDebugMode) {
print('⏱️ Call duration timer started');
}
9 months ago
}
/// Accept incoming call
Future<void> acceptCall() async {
log('🔵 [CALL] acceptCall() called', name: 'ChatProvider');
log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider');
log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider');
if (currentCall == null || callStatus != CallStatus.incomingRinging) {
log('⚠️ [CALL] Cannot accept - invalid state (call: ${currentCall == null ? "null" : "exists"}, status: $callStatus)', name: 'ChatProvider');
return;
}
// Request permissions
log('🔵 [CALL] Requesting microphone permission...', name: 'ChatProvider');
final micStatus = await Permission.microphone.request();
log('🔵 [CALL] Microphone permission: ${micStatus.name}', name: 'ChatProvider');
if (!micStatus.isGranted) {
log('❌ [CALL] Microphone permission denied - declining call', name: 'ChatProvider');
await declineCall('permission_denied');
return;
}
if (currentCall!.type == CallType.video) {
log('🔵 [CALL] Video call - requesting camera permission...', name: 'ChatProvider');
final cameraStatus = await Permission.camera.request();
log('🔵 [CALL] Camera permission: ${cameraStatus.name}', name: 'ChatProvider');
if (!cameraStatus.isGranted) {
log('❌ [CALL] Camera permission denied - declining call', name: 'ChatProvider');
await declineCall('permission_denied');
return;
}
}
try {
// Invoke SignalR AnswerCallAsync
log('🔵 [CALL] Invoking AnswerCallAsync with args:', name: 'ChatProvider');
log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall!.peerId}', name: 'ChatProvider');
log(' - moduleCode: $moduleID', name: 'ChatProvider');
log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider');
log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider');
await chatHubConnection?.invoke(
'AnswerCallAsync',
args: [
sender?.employeeNumber ?? '',
currentCall!.peerId,
moduleID.toString(),
referenceID?.toString() ?? '',
chatParticipantModel?.id?.toString() ?? '',
],
);
log('✅ [CALL] AnswerCallAsync invoked successfully', name: 'ChatProvider');
callStatus = CallStatus.connecting;
notifyListeners();
log('🔵 [CALL] Status changed to: connecting', name: 'ChatProvider');
if (kDebugMode) {
print('✅ Call accepted');
}
} catch (e, stackTrace) {
log('❌ [CALL] Error accepting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('❌ Error accepting call: $e');
}
_teardownCall();
}
}
/// Decline incoming call
Future<void> declineCall(String reason) async {
log('🔵 [CALL] declineCall() called with reason: $reason', name: 'ChatProvider');
log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider');
if (currentCall == null) {
log('⚠️ [CALL] No call to decline', name: 'ChatProvider');
return;
}
try {
// Invoke SignalR CallDeclinedAsync
log('🔵 [CALL] Invoking CallDeclinedAsync with args:', name: 'ChatProvider');
log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider');
log(' - target: ${currentCall!.peerId}', name: 'ChatProvider');
log(' - moduleCode: $moduleID', name: 'ChatProvider');
log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider');
log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider');
await chatHubConnection?.invoke(
'CallDeclinedAsync',
args: [
sender?.employeeNumber ?? '',
currentCall!.peerId,
moduleID.toString(),
referenceID?.toString() ?? '',
chatParticipantModel?.id?.toString() ?? '',
],
);
log('✅ [CALL] CallDeclinedAsync invoked successfully', name: 'ChatProvider');
if (kDebugMode) {
print('📞 Call declined: $reason');
}
} catch (e, stackTrace) {
log('❌ [CALL] Error declining call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('⚠️ Error declining call: $e');
}
}
_teardownCall();
9 months ago
}
/// Register call-related SignalR event handlers
void _registerCallHandlers() {
log('═══════════════════════════════════════════', name: 'ChatProvider');
log('📞 [CALL] Starting call handler registration...', name: 'ChatProvider');
if (chatHubConnection == null) {
log('❌ [CALL] Cannot register handlers - chatHubConnection is null', name: 'ChatProvider');
return;
}
// NEW: Add a GLOBAL catch-all handler to see ALL SignalR events in debug mode
if (kDebugMode) {
log('🔍 [DEBUG] Setting up GLOBAL event interceptor to log ALL incoming SignalR messages', name: 'ChatProvider');
// Store the original method handlers
final originalHandlers = <String, Function>{};
// Register a generic listener for common call event patterns
final allPossibleCallEvents = [
'OnIncomingCallAsync',
'OnIncomingCall',
'IncomingCall',
'OnCallAcceptedAsync',
'OnCallAccepted',
'CallAccepted',
'OnCallDeclinedAsync',
'OnCallDeclined',
'CallDeclined',
'OnHangUpAsync',
'OnHangUp',
'HangUp',
'OnAudioToggle',
'AudioToggle',
'OnCameraToggle',
'CameraToggle',
'CallUserAsync', // Echo back
'OnCallStarted', // Alternative
'OnCallRinging', // Alternative
];
for (final eventName in allPossibleCallEvents) {
chatHubConnection!.on(eventName, (args) {
log('🔔🔔🔔 [GLOBAL DEBUG] Received SignalR event: "$eventName"', name: 'ChatProvider');
log(' Args: $args', name: 'ChatProvider');
log(' Args type: ${args?.runtimeType}', name: 'ChatProvider');
if (args != null && args.isNotEmpty) {
log(' First arg: ${args.first}', name: 'ChatProvider');
log(' First arg type: ${args.first?.runtimeType}', name: 'ChatProvider');
}
});
}
}
9 months ago
// Incoming call
log('🔵 [CALL] Registering: OnIncomingCallAsync', name: 'ChatProvider');
chatHubConnection!.on("OnIncomingCallAsync", _onIncomingCall);
// Call accepted
log('🔵 [CALL] Registering: OnCallAcceptedAsync', name: 'ChatProvider');
chatHubConnection!.on("OnCallAcceptedAsync", _onCallAccepted);
// Call declined
log('🔵 [CALL] Registering: OnCallDeclinedAsync', name: 'ChatProvider');
chatHubConnection!.on("OnCallDeclinedAsync", _onCallDeclined);
// Call ended
log('🔵 [CALL] Registering: OnHangUpAsync', name: 'ChatProvider');
chatHubConnection!.on("OnHangUpAsync", _onCallEnded);
9 months ago
// Peer audio toggle
log('🔵 [CALL] Registering: OnAudioToggle', name: 'ChatProvider');
chatHubConnection!.on("OnAudioToggle", _onPeerAudioToggle);
// Peer camera toggle
log('🔵 [CALL] Registering: OnCameraToggle', name: 'ChatProvider');
chatHubConnection!.on("OnCameraToggle", _onPeerCameraToggle);
log('✅ [CALL] All call handlers registered successfully', name: 'ChatProvider');
log('📞 [CALL] Ready to receive: OnIncomingCallAsync, OnCallAcceptedAsync, OnCallDeclinedAsync, OnHangUpAsync', name: 'ChatProvider');
log('📞 [CALL] My Employee Number: ${sender?.employeeNumber ?? "NOT SET YET"}', name: 'ChatProvider');
log('📞 [CALL] My User ID: ${chatLoginResponse?.userId ?? "NOT SET"}', name: 'ChatProvider');
log('📞 [CALL] SignalR Connection ID: ${chatHubConnection?.connectionId ?? "NULL"}', name: 'ChatProvider');
log('═══════════════════════════════════════════', name: 'ChatProvider');
if (kDebugMode) {
print('✅ Call handlers registered - watching for ALL call events');
}
_callHandlersRegistered = true;
9 months ago
}
/// Handle incoming call event
void _onIncomingCall(List<Object?>? args) {
log('📞 [CALL EVENT] OnIncomingCallAsync received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider');
if (args == null || args.isEmpty) {
log('⚠️ [CALL EVENT] OnIncomingCallAsync - args is null or empty', name: 'ChatProvider');
return;
}
try {
final data = args.first as Map<String, dynamic>;
log('🔵 [CALL EVENT] Parsed data: $data', name: 'ChatProvider');
final callerId = data['sourceUserId'] as String?;
final callerName = data['userName'] as String? ?? 'Unknown';
final isVideo = data['isVideoCall'] as bool? ?? false;
log('🔵 [CALL EVENT] Parsed values:', name: 'ChatProvider');
log(' - callerId: $callerId', name: 'ChatProvider');
log(' - callerName: $callerName', name: 'ChatProvider');
log(' - isVideoCall: $isVideo', name: 'ChatProvider');
// Check if already in a call
if (callStatus != CallStatus.idle) {
log('⚠️ [CALL EVENT] Already in a call (status: $callStatus) - auto-rejecting with busy', name: 'ChatProvider');
// Auto-reject with busy status
log('🔵 [CALL EVENT] Invoking CallDeclinedAsync (busy) with args:', name: 'ChatProvider');
log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider');
log(' - target: $callerId', name: 'ChatProvider');
chatHubConnection?.invoke(
'CallDeclinedAsync',
args: [
sender?.employeeNumber ?? '',
callerId ?? '',
moduleID.toString(),
referenceID?.toString() ?? '',
chatParticipantModel?.id?.toString() ?? '',
],
).then((_) {
log('✅ [CALL EVENT] Auto-rejection sent successfully', name: 'ChatProvider');
}).catchError((e) {
log('❌ [CALL EVENT] Error sending auto-rejection: $e', name: 'ChatProvider');
});
return;
}
// Create call session
log('🔵 [CALL EVENT] Creating incoming CallSession...', name: 'ChatProvider');
currentCall = CallSession(
callId: const Uuid().v4(),
type: isVideo ? CallType.video : CallType.audio,
direction: CallDirection.incoming,
peerId: callerId ?? '',
peerName: callerName,
peerAvatar: null,
startTime: DateTime.now(),
);
log('✅ [CALL EVENT] CallSession created - callId: ${currentCall!.callId}', name: 'ChatProvider');
callStatus = CallStatus.incomingRinging;
notifyListeners();
log('🔵 [CALL EVENT] Status changed to: incomingRinging', name: 'ChatProvider');
log('✅ [CALL EVENT] Incoming ${isVideo ? "video" : "audio"} call from $callerName processed', name: 'ChatProvider');
// NEW: Show incoming call dialog using global navigator
log('🔵 [CALL EVENT] Showing incoming call dialog...', name: 'ChatProvider');
final context = navigatorKey.currentContext;
if (context != null && currentCall != null) {
// Use a post-frame callback to ensure we're not in the middle of a build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => IncomingCallDialog(call: currentCall!),
);
log('✅ [CALL EVENT] Incoming call dialog shown', name: 'ChatProvider');
}
});
} else {
log('⚠️ [CALL EVENT] Cannot show dialog - context is null or call is null', name: 'ChatProvider');
}
if (kDebugMode) {
print('📞 Incoming ${isVideo ? "video" : "audio"} call from $callerName');
}
} catch (e, stackTrace) {
log('❌ [CALL EVENT] Error handling incoming call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('❌ Error handling incoming call: $e');
}
}
}
/// Handle call accepted event
void _onCallAccepted(List<Object?>? args) {
log('✅ [CALL EVENT] OnCallAcceptedAsync received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider');
if (callStatus == CallStatus.outgoingRinging) {
log('🔵 [CALL EVENT] Call was accepted by peer - stopping timeout timer', name: 'ChatProvider');
_callTimeoutTimer?.cancel();
callStatus = CallStatus.connecting;
notifyListeners();
log('🔵 [CALL EVENT] Status changed to: connecting', name: 'ChatProvider');
log('✅ [CALL EVENT] Call accepted - proceeding with connection', name: 'ChatProvider');
if (kDebugMode) {
print('✅ Call was accepted');
}
} else {
log('⚠️ [CALL EVENT] Received OnCallAcceptedAsync but status is not outgoingRinging: $callStatus', name: 'ChatProvider');
9 months ago
}
}
/// Handle call declined event
void _onCallDeclined(List<Object?>? args) {
log('📞 [CALL EVENT] OnCallDeclinedAsync received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider');
if (args == null || args.isEmpty) {
log('⚠️ [CALL EVENT] OnCallDeclinedAsync - args is null or empty', name: 'ChatProvider');
return;
}
try {
final reason = args.first as String? ?? 'declined';
log('🔵 [CALL EVENT] Decline reason: $reason', name: 'ChatProvider');
log('✅ [CALL EVENT] Call was declined by peer', name: 'ChatProvider');
if (kDebugMode) {
print('📞 Call declined: $reason');
}
_teardownCall();
} catch (e, stackTrace) {
log('❌ [CALL EVENT] Error handling call declined: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
print('❌ Error handling call declined: $e');
}
}
}
/// Handle call ended event
void _onCallEnded(List<Object?>? args) {
log('📞 [CALL EVENT] OnHangUpAsync received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider');
log('✅ [CALL EVENT] Call ended by peer - cleaning up', name: 'ChatProvider');
if (kDebugMode) {
print('📞 Call ended by peer');
}
_teardownCall();
9 months ago
}
/// Handle peer audio toggle event
void _onPeerAudioToggle(List<Object?>? args) {
log('🎤 [CALL EVENT] OnAudioToggle received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Previous peer mute state: $isPeerMuted', name: 'ChatProvider');
isPeerMuted = !isPeerMuted;
notifyListeners();
log('🔵 [CALL EVENT] New peer mute state: $isPeerMuted', name: 'ChatProvider');
log('✅ [CALL EVENT] Peer ${isPeerMuted ? "muted" : "unmuted"} their microphone', name: 'ChatProvider');
if (kDebugMode) {
print('🎤 Peer ${isPeerMuted ? "muted" : "unmuted"}');
}
9 months ago
}
/// Handle peer camera toggle event
void _onPeerCameraToggle(List<Object?>? args) {
log('📹 [CALL EVENT] OnCameraToggle received', name: 'ChatProvider');
log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider');
log('🔵 [CALL EVENT] Previous peer camera state: $isPeerCameraOn', name: 'ChatProvider');
isPeerCameraOn = !isPeerCameraOn;
notifyListeners();
log('🔵 [CALL EVENT] New peer camera state: $isPeerCameraOn', name: 'ChatProvider');
log('✅ [CALL EVENT] Peer turned camera ${isPeerCameraOn ? "on" : "off"}', name: 'ChatProvider');
}
}