|
|
|
|
|
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<bool> checkAudioPermissions() => Permission.microphone.isGranted;
|
|
|
|
|
|
|
|
|
|
|
|
Future<bool> checkVideoPermissions() async => (await Permission.microphone.isGranted) && (await Permission.camera.isGranted);
|
|
|
|
|
|
|
|
|
|
|
|
Future<bool> requestAudioPermissions() async {
|
|
|
|
|
|
var result = await [Permission.microphone].request();
|
|
|
|
|
|
return (result[Permission.microphone] == PermissionStatus.granted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<bool> requestVideoPermissions() async {
|
|
|
|
|
|
var result = await [Permission.microphone, Permission.camera].request();
|
|
|
|
|
|
return (result[Permission.microphone] == PermissionStatus.granted) && (result[Permission.camera] == PermissionStatus.granted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|