improvements

merge-AudioVideo-Copilot
Sikander Saleem 2 months ago
parent 69fb1917fb
commit deb8748fc1

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
@ -13,16 +12,16 @@ import '../chat_provider.dart';
import 'call_provider.dart'; import 'call_provider.dart';
class AudioCallPage extends StatefulWidget { class AudioCallPage extends StatefulWidget {
Participants sender; final Participants sender;
Participants recipient; final Participants recipient;
int moduleId; final int moduleId;
int? referenceID; final int? referenceID;
bool isAccept; final bool isAccept;
AudioCallPage({Key? key, required this.sender, required this.recipient, this.isAccept = false, required this.moduleId, this.referenceID}) : super(key: key); const AudioCallPage({Key? key, required this.sender, required this.recipient, this.isAccept = false, required this.moduleId, this.referenceID}) : super(key: key);
@override @override
_AudioCallPageState createState() { State<AudioCallPage> createState() {
return _AudioCallPageState(); return _AudioCallPageState();
} }
} }
@ -45,79 +44,67 @@ class _AudioCallPageState extends State<AudioCallPage> {
} }
void makeCall() async { void makeCall() async {
bool isConnected = await callPro.buildHubConnection(widget.sender.employeeNumber!); bool isConnected = false;
try {
isConnected = await callPro.buildHubConnection(widget.sender.employeeNumber ?? "", hubC: chatHubConnection);
} catch (e) {
debugPrint("⚠️ Error preparing call connection: $e");
isConnected = false;
}
if (!mounted) return;
if (!isConnected) { if (!isConnected) {
context.showConfirmDialog("Error while connecting to call service. Please try again later.", title: "Connection Error", onTap: () { _showFailure("Error while connecting to call service. Please try again later.", title: "Connection Error");
Navigator.pop(context);
Navigator.pop(context);
});
return; return;
} }
callPro.initAudioCallListeners(); callPro.initAudioCallListeners();
if (widget.isAccept) { if (widget.isAccept) {
callPro.acceptCall(widget.sender.employeeNumber ?? "", widget.recipient?.employeeNumber ?? "", false, moduleId: widget.moduleId); callPro.acceptCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", false, moduleId: widget.moduleId);
} else { } else {
callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient?.employeeNumber ?? "", false, widget.moduleId, widget.referenceID, onSuccess: (String msg) { callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", false, widget.moduleId, widget.referenceID, onSuccess: (String msg) {
callPro.dispose(); _closeCall();
Navigator.pop(context);
}, onFailed: (String msg) { }, onFailed: (String msg) {
callPro.dispose(); _showFailure(msg, title: "Failed");
print("isDialogOpen:$isDialogOpen");
if (isDialogOpen) {
return;
}
isDialogOpen = true;
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (BuildContext cxt) => PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) return;
},
child: SingleBtnDialog(
title: "Failed",
message: msg,
okTitle: "Go Back",
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
}),
),
).then((val) {
isDialogOpen = false;
});
}); });
} }
}
void _closeCall() {
callPro.endSession();
if (mounted) Navigator.pop(context);
}
Map<String, dynamic> json = { void _showFailure(String message, {required String title}) {
// "callerID": AppState().chatDetails!.response!.id!.toString(), callPro.endSession();
// "callerDetails": AppState().chatDetails!.toJson(), if (!mounted || isDialogOpen) return;
// "receiverID": params!.chatUser!.id.toString(), isDialogOpen = true;
// "receiverDetails": params!.chatUser!.toJson(), showDialog(
// "title": params!.chatUser!.userName!.replaceAll(".", " "), context: context,
// "calltype": widget.isAudio ? "Audio" : "Video", barrierDismissible: false,
}; useRootNavigator: false,
builder: (BuildContext cxt) => PopScope(
// CallDataModel callData = CallDataModel.fromJson(json); canPop: false,
// await Navigator.push( onPopInvokedWithResult: (bool didPop, Object? result) {
// context, if (didPop) return;
// MaterialPageRoute( },
// builder: (BuildContext context) => OutGoingCall( child: SingleBtnDialog(
// isVideoCall: callType == "VIDEO" ? true : false, title: title,
// outGoingCallData: callData, message: message,
// ), okTitle: "Go Back",
// ), onTap: () {
// ).then((value) { Navigator.pop(cxt); // close dialog
// print("then"); if (mounted) Navigator.pop(context); // close call page
// callPro.stopListeners(); }),
// }); ),
).then((_) {
isDialogOpen = false;
});
} }
@override @override
void dispose() { void dispose() {
callPro.stopListeners(); callPro.endSession();
super.dispose(); super.dispose();
} }

@ -38,17 +38,37 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
String currentStatus = "Connecting..."; String currentStatus = "Connecting...";
bool _ownsConnection = false;
void _setStatus(String status) {
if (currentStatus == status) return;
currentStatus = status;
notifyListeners();
}
Future<void> _disposeConnection() async { Future<void> _disposeConnection() async {
final HubConnection? connection = callHubConnection;
final bool owns = _ownsConnection;
callHubConnection = null;
_ownsConnection = false;
if (connection == null || !owns) return;
try { try {
if (callHubConnection != null) { await connection.stop();
await callHubConnection!.stop();
}
} catch (e) { } catch (e) {
} finally { if (kDebugMode) {
callHubConnection = null; print('⚠️ Error closing call SignalR connection: $e');
}
} }
// isLoading = false; }
// notifyListeners();
Future<void> endSession() async {
stopListeners();
onFailed = null;
onSuccess = null;
isMuted = false;
isPeerMuted = false;
currentStatus = "Connecting...";
await _disposeConnection();
} }
@override @override
@ -73,7 +93,6 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
final HttpConnectionOptions httpOp = HttpConnectionOptions( final HttpConnectionOptions httpOp = HttpConnectionOptions(
transport: HttpTransportType.LongPolling, transport: HttpTransportType.LongPolling,
skipNegotiation: false, skipNegotiation: false,
// Keep false: signalr_netcore 1.4.4 crashes when logging + LongPolling.
logMessageContent: false, logMessageContent: false,
accessTokenFactory: () async => token, accessTokenFactory: () async => token,
requestTimeout: 30000, requestTimeout: 30000,
@ -102,61 +121,115 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
} }
Future<bool> buildHubConnection(String employeeNumber, {HubConnection? hubC}) async { Future<bool> buildHubConnection(String employeeNumber, {HubConnection? hubC}) async {
if (hubC != null) { _setStatus("Connecting...");
callHubConnection = hubC;
await callHubConnection!.start();
return true;
}
try { try {
if (hubC != null) {
final bool reused = await _adoptExistingConnection(hubC);
if (reused) return true;
if (kDebugMode) {
print('ℹ️ Shared chat hub not usable (state: ${hubC.state}), opening a dedicated call connection.');
}
}
await _disposeConnection(); await _disposeConnection();
http.Response response = await ApiClient().postJsonForResponse(URLs.chatSdkToken, {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null});
if (response.statusCode == 200) { final http.Response response = await ApiClient().postJsonForResponse(
ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body)); URLs.chatSdkToken,
callHubConnection = await getHubConnection(chatResponse.token!); {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null},
await callHubConnection!.start(); );
return true; if (response.statusCode != 200) {
if (kDebugMode) {
print('⚠️ Call token request failed: HTTP ${response.statusCode}');
}
return false;
} }
return false; final ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
if (chatResponse.token == null || chatResponse.token!.isEmpty) {
if (kDebugMode) {
print('⚠️ Call token request returned an empty token.');
}
return false;
}
callHubConnection = await getHubConnection(chatResponse.token!);
_ownsConnection = true;
await callHubConnection!.start();
return callHubConnection?.state == HubConnectionState.Connected;
} catch (e) { } catch (e) {
if (kDebugMode) {
print('⚠️ Error building call SignalR connection: $e');
}
await _disposeConnection(); await _disposeConnection();
return false; return false;
} }
} }
Future<bool> _adoptExistingConnection(HubConnection hubC) async {
switch (hubC.state) {
case HubConnectionState.Connected:
callHubConnection = hubC;
_ownsConnection = false;
return true;
case HubConnectionState.Connecting:
case HubConnectionState.Reconnecting:
final bool connected = await _waitUntilConnected(hubC);
if (connected) {
callHubConnection = hubC;
_ownsConnection = false;
}
return connected;
case HubConnectionState.Disconnected:
// Chat side dropped it - we can legally restart it, but we do not own it
// so we must not stop it when the call ends.
try {
await hubC.start();
if (hubC.state == HubConnectionState.Connected) {
callHubConnection = hubC;
_ownsConnection = false;
return true;
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Could not restart shared chat hub for call: $e');
}
}
return false;
default:
// Disconnecting (or unknown) - do not touch it, build our own instead.
return false;
}
}
Future<bool> _waitUntilConnected(HubConnection hub, {Duration timeout = const Duration(seconds: 15)}) async {
final DateTime deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
final HubConnectionState? state = hub.state;
if (state == HubConnectionState.Connected) return true;
if (state == HubConnectionState.Disconnected || state == HubConnectionState.Disconnecting) return false;
await Future<void>.delayed(const Duration(milliseconds: 200));
}
return hub.state == HubConnectionState.Connected;
}
bool get isHubConnected => callHubConnection?.state == HubConnectionState.Connected;
void initAudioCallListeners() { void initAudioCallListeners() {
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync); final HubConnection? hub = callHubConnection;
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync); if (hub == null) return;
callHubConnection!.on("OnOfferAsync", onOfferAsync); hub.on("OnCallAcceptedAsync", onCallAcceptedAsync);
callHubConnection!.on("OnAnswerOffer", onAnswerOffer); hub.on("OnIceCandidateAsync", onIceCandidateAsync);
callHubConnection!.on("OnHangUpAsync", onHangUpAsync); hub.on("OnOfferAsync", onOfferAsync);
callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync); hub.on("OnAnswerOffer", onAnswerOffer);
callHubConnection!.on('OnAudioToggle', onAudioToggle); hub.on("OnHangUpAsync", onHangUpAsync);
hub.on("OnCallDeclinedAsync", onCallDeclinedAsync);
hub.on('OnAudioToggle', onAudioToggle);
} }
void initCallListeners() { void initCallListeners() {
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync); final HubConnection? hub = callHubConnection;
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync); if (hub == null) return;
callHubConnection!.on("OnOfferAsync", onOfferAsync); initAudioCallListeners();
callHubConnection!.on("OnAnswerOffer", onAnswerOffer); hub.on('OnCameraToggle', onCameraToggle);
callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
callHubConnection!.on('OnAudioToggle', onAudioToggle);
callHubConnection!.on('OnCameraToggle', onCameraToggle);
// callHubConnection!.on('OnIncomingCallAsync', onHandleIncomingCall);
// callHubConnection!.on('OnCallAcceptedAsync', _handleCallAccepted);
// callHubConnection!.on('OnCallDeclinedAsync', _handleCallDeclined);
// callHubConnection!.on('OnHangUpAsync', onHangUpAsync);
// callHubConnection!.on('OnOfferAsync', _handleOffer);
// callHubConnection!.on('OnAnswerOfferAsync', onAnswerOffer);
// callHubConnection!.on('OnIceCandidateAsync', _handleIceCandidate);
// callHubConnection!.on('OnAudioToggle', _handleAudioToggle);
// callHubConnection!.on('OnCameraToggle', _handleCameraToggle);
// callHubConnection!.on('OnUserOnlineAsync', _handleUserOnline);
// callHubConnection!.on('OnUserOfflineAsync', _handleUserOffline);
// callHubConnection!.on('OnCallHistoryUpdated', _handleCallHistoryUpdated);
// callHubConnection!.on('OnError', _handleError);
} }
//Video Constraints //Video Constraints
@ -189,11 +262,17 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
}; };
Future<RTCPeerConnection> _createPeerConnection() async { Future<RTCPeerConnection> _createPeerConnection() async {
// {"url": "stun:stun.l.google.com:19302"},
Map<String, dynamic> configuration = { Map<String, dynamic> configuration = {
"iceServers": [ "iceServers": [
{"urls": 'stun:15.185.116.59:3478'}, {"urls": 'stun:15.185.116.59:3478'},
{"urls": "turn:15.185.116.59:3479", "username": "admin", "credential": "admin"} {
"urls": "turn:15.185.116.59:3479",
"username": "admin",
"credential": "admin",
},
{"urls": "stun:stun.l.google.com:19302"},
{"urls": "stun:stun1.l.google.com:19302"},
] ]
}; };
@ -269,25 +348,67 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
this.referenceID = referenceID; this.referenceID = referenceID;
this.onFailed = onFailed; this.onFailed = onFailed;
this.onSuccess = onSuccess; this.onSuccess = onSuccess;
invoke(invokeMethod: "CallUserAsync", currentUserID: senderN!, targetUserID: recipientN!, isVideoCall: isVideo!, data: [this.moduleId, referenceID]); if (senderN == null || senderN.isEmpty || recipientN == null || recipientN.isEmpty) {
onFailed?.call("Call participants could not be resolved. Please reopen the chat and try again.");
return;
}
try {
await invoke(invokeMethod: "CallUserAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo ?? false, data: [this.moduleId, referenceID]);
_setStatus("Ringing...");
} catch (e) {
if (kDebugMode) {
print('⚠️ CallUserAsync failed: $e');
}
_setStatus("Call failed");
onFailed?.call("Could not reach the call service. Please check your connection and try again.");
}
} }
void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId}) async { void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId}) async {
invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]); this.senderN = senderN;
this.recipientN = recipientN;
try {
await invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]);
} catch (e) {
if (kDebugMode) {
print('⚠️ AnswerCallAsync failed: $e');
}
_setStatus("Call failed");
onFailed?.call("Could not join the call. Please try again.");
}
} }
Future declineCall(String senderN, String recipientN, {required int moduleId}) async { Future declineCall(String senderN, String recipientN, {required int moduleId}) async {
await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]); try {
await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]);
} catch (e) {
if (kDebugMode) {
print('⚠️ CallDeclinedAsync failed: $e');
}
}
} }
void endCall(String senderN, String recipientN, {required int moduleId}) { void endCall(String senderN, String recipientN, {required int moduleId}) async {
invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]); try {
await invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
} catch (e) {
if (kDebugMode) {
print('⚠️ HangUpAsync failed: $e');
}
}
} }
void toggleMute() async { void toggleMute() async {
await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!); if (senderN == null || recipientN == null) return;
isMuted = !isMuted; try {
notifyListeners(); await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!);
isMuted = !isMuted;
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('⚠️ AudioToggle failed: $e');
}
}
} }
void checkCall(Map<String, dynamic> message) { void checkCall(Map<String, dynamic> message) {
@ -317,6 +438,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
void onCallAcceptedAsync(List<Object?>? params) { void onCallAcceptedAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params"); print("onCallAcceptedAsync: $params");
_setStatus("Connected");
} }
void onIceCandidateAsync(List<Object?>? params) { void onIceCandidateAsync(List<Object?>? params) {
@ -396,8 +518,12 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
} else if (invokeMethod == "AnswerOfferAsync") { } else if (invokeMethod == "AnswerOfferAsync") {
args = [targetUserID, data]; args = [targetUserID, data];
} }
print("Invoking:$invokeMethod:${args}"); print("Invoking:$invokeMethod:$args");
await callHubConnection?.invoke(invokeMethod, args: args); final HubConnection? hub = callHubConnection;
if (hub == null || hub.state != HubConnectionState.Connected) {
throw StateError("Call hub is not connected (state: ${hub?.state}). Cannot invoke $invokeMethod.");
}
await hub.invoke(invokeMethod, args: args);
} }
void stopListeners() async { void stopListeners() async {
@ -408,6 +534,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
callHubConnection?.off('OnOfferAsync'); callHubConnection?.off('OnOfferAsync');
callHubConnection?.off('OnHangUpAsync'); callHubConnection?.off('OnHangUpAsync');
callHubConnection?.off('OnAudioToggle'); callHubConnection?.off('OnAudioToggle');
callHubConnection?.off('OnCameraToggle');
} }
handleResponse(List<Object?>? params) { handleResponse(List<Object?>? params) {

@ -88,6 +88,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
late int moduleID; late int moduleID;
int? referenceID; int? referenceID;
String? sessionKey;
static String buildSessionKey(int moduleId, int requestId) => '$moduleId-$requestId';
/// OPTIMIZATION: Improved connection disposal to prevent memory leaks /// OPTIMIZATION: Improved connection disposal to prevent memory leaks
/// This properly handles errors and ensures connection is always cleaned up /// This properly handles errors and ensures connection is always cleaned up
Future<void> _disposeConnection() async { Future<void> _disposeConnection() async {
@ -121,6 +125,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
userChatHistory = null; userChatHistory = null;
sender = null; sender = null;
recipient = null; recipient = null;
sessionKey = null;
ChatApiClient().chatLoginResponse = null; ChatApiClient().chatLoginResponse = null;
} }
@ -165,8 +170,14 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
try { try {
chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId, assigneeEmployeeNumber); chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId, assigneeEmployeeNumber);
chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber); chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber);
sender = chatParticipantModel?.participants?.firstWhere((participant) => participant.employeeNumber == myId); // `firstWhere` used to throw StateError when a participant was missing,
recipient = chatParticipantModel?.participants?.firstWhere((participant) => participant.employeeNumber == assigneeEmployeeNumber); // which aborted the whole setup and left the chat unusable.
final List<Participants> participants = chatParticipantModel?.participants ?? <Participants>[];
sender = participants.cast<Participants?>().firstWhere((participant) => participant?.employeeNumber == myId, orElse: () => null);
recipient = participants.cast<Participants?>().firstWhere((participant) => participant?.employeeNumber == assigneeEmployeeNumber, orElse: () => null);
if (chatLoginResponse != null) {
sessionKey = buildSessionKey(moduleId, requestId);
}
} catch (ex) { } catch (ex) {
if (kDebugMode) { if (kDebugMode) {
print('⚠️ Error in getUserAutoLoginTokenSilent: $ex'); print('⚠️ Error in getUserAutoLoginTokenSilent: $ex');

@ -1,5 +1,3 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -12,99 +10,157 @@ import 'chat_page.dart';
import 'chat_provider.dart'; import 'chat_provider.dart';
class ChatWidget extends StatefulWidget { class ChatWidget extends StatefulWidget {
int moduleId; final int moduleId;
int requestId; final int requestId;
String? assigneeEmployeeNumber; final String? assigneeEmployeeNumber;
String? myLoginUserID; final String? myLoginUserID;
String? contactEmployeeINumber; final String? contactEmployeeINumber;
bool isReadOnly; final bool isReadOnly;
bool isShow; final bool isShow;
String title; final String title;
ChatWidget( const ChatWidget({
{Key? key, Key? key,
this.isReadOnly = false, this.isReadOnly = false,
this.title = "Chat", this.title = "Chat",
required this.moduleId, required this.moduleId,
required this.requestId, required this.requestId,
this.assigneeEmployeeNumber, this.assigneeEmployeeNumber,
this.myLoginUserID, this.myLoginUserID,
this.contactEmployeeINumber, this.contactEmployeeINumber,
this.isShow = false}) this.isShow = false,
: super(key: key); }) : super(key: key);
@override @override
_ChatWidgetState createState() { State<ChatWidget> createState() => _ChatWidgetState();
return _ChatWidgetState();
}
} }
class _ChatWidgetState extends State<ChatWidget> { class _ChatWidgetState extends State<ChatWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!widget.isShow) return;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<ChatProvider>(context, listen: false).reset(); if (!mounted) return;
getChatToken(); _prepareChatSession();
}); });
} }
void getChatToken() { @override
// ChatProvider cProvider = Provider.of<ChatProvider>(context, listen: false); void didUpdateWidget(covariant ChatWidget oldWidget) {
// if (cProvider.chatLoginResponse != null && cProvider.referenceID == widget.requestId) return; super.didUpdateWidget(oldWidget);
// // String assigneeEmployeeNumber = Provider.of<ServiceRequestDetailProvider>(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? ""; // Re-prepare when the widget is pointed at a different request.
// String myEmployeeId = widget.myLoginUserID; if (widget.isShow && (oldWidget.requestId != widget.requestId || oldWidget.moduleId != widget.moduleId)) {
// _prepareChatSession();
// String receiver = context.userProvider.isNurse ? widget.assigneeEmployeeNumber : widget.myEmployeeID; }
ChatProvider cProvider = Provider.of<ChatProvider>(context, listen: false);
if (cProvider.chatLoginResponse != null && cProvider.referenceID == widget.requestId) return;
String assigneeEmployeeNumber = widget.assigneeEmployeeNumber ?? Provider.of<CMDetailProvider>(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? "";
String myEmployeeId = widget.myLoginUserID ?? context.userProvider.user!.username!;
String receiver = context.userProvider.isNurse
? assigneeEmployeeNumber
: (context.userProvider.isEngineer
? (widget.contactEmployeeINumber ?? Provider.of<CMDetailProvider>(context, listen: false).currentWorkOrder!.data!.workOrderContactPerson.first.employeeId!)
: "");
cProvider.getUserAutoLoginTokenSilent(widget.moduleId, widget.requestId, widget.title, myEmployeeId, receiver, isMounted: mounted);
} }
@override /// Fetches the chat token + participants, unless the exact same conversation
void dispose() { /// is already loaded in the (globally shared) [ChatProvider].
super.dispose(); ///
/// NOTE: `reset()` is intentionally NOT called here. `ChatProvider` is a
/// singleton, so resetting on every mount used to drop a live SignalR socket
/// and made the "already loaded" guard below dead code (it always re-fetched).
/// `getUserAutoLoginTokenSilent()` performs its own reset internally.
void _prepareChatSession() {
final ChatProvider cProvider = context.read<ChatProvider>();
final String sessionKey = ChatProvider.buildSessionKey(widget.moduleId, widget.requestId);
if (cProvider.chatLoginTokenLoading) return;
if (cProvider.chatLoginResponse != null && cProvider.sessionKey == sessionKey) return;
final String? myEmployeeId = widget.myLoginUserID ?? context.userProvider.user?.username;
final String? receiver = _resolveReceiver();
if (myEmployeeId == null || myEmployeeId.isEmpty || receiver == null || receiver.isEmpty) {
// Not enough information to open a conversation (e.g. no assignee /
// contact person yet) - keep the icon idle instead of crashing on a
// null assertion.
return;
}
cProvider.getUserAutoLoginTokenSilent(
widget.moduleId,
widget.requestId,
widget.title,
myEmployeeId,
receiver,
isMounted: mounted,
);
}
/// Nurse -> chats the assigned engineer. Engineer -> chats the contact person.
String? _resolveReceiver() {
final workOrder = context.read<CMDetailProvider>().currentWorkOrder?.data;
if (context.userProvider.isNurse) {
return widget.assigneeEmployeeNumber ?? workOrder?.assignedEmployee?.employeeId;
}
if (context.userProvider.isEngineer) {
if (widget.contactEmployeeINumber != null && widget.contactEmployeeINumber!.isNotEmpty) {
return widget.contactEmployeeINumber;
}
final contacts = workOrder?.workOrderContactPerson;
if (contacts != null && contacts.isNotEmpty) {
return contacts.first.employeeId;
}
return workOrder?.workOrderCreatedBy?.employeeId;
}
return null;
}
void _openChat() {
Navigator.push(
context,
CupertinoPageRoute<void>(
builder: (_) => ChatPage(
moduleId: widget.moduleId,
requestId: widget.requestId,
title: widget.title,
readOnly: widget.isReadOnly,
assigneeEmployeeNumber: widget.assigneeEmployeeNumber,
contactEmployeeINumber: widget.contactEmployeeINumber,
myLoginUserID: widget.myLoginUserID,
),
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.isShow if (!widget.isShow) return const SizedBox();
? Consumer<ChatProvider>(builder: (pContext, chatProvider, _) {
final int unreadCount = (!chatProvider.chatLoginTokenLoading && (chatProvider.chatParticipantModel?.unreadCount ?? 0) > 0) ? chatProvider.chatParticipantModel!.unreadCount! : 0; // Selector (instead of Consumer) so the badge does not rebuild on every
return CustomBadge2( // incoming message / typing event pushed through ChatProvider.
value: unreadCount, return Selector<ChatProvider, _ChatBadgeState>(
top: 2, selector: (_, provider) => _ChatBadgeState(
right: 2, isLoading: provider.chatLoginTokenLoading,
minSize: 18, unreadCount: provider.chatParticipantModel?.unreadCount ?? 0,
child: IconButton( ),
icon: const Icon(Icons.chat_bubble), builder: (_, state, __) => CustomBadge2(
onPressed: () { value: state.effectiveUnread,
Navigator.push( top: 2,
context, right: 2,
CupertinoPageRoute( minSize: 18,
builder: (context) => ChatPage( child: IconButton(
moduleId: widget.moduleId, icon: const Icon(Icons.chat_bubble),
requestId: widget.requestId, onPressed: _openChat,
title: widget.title, ).toShimmer(context: context, isShow: state.isLoading, radius: 30, height: 30, width: 30),
readOnly: widget.isReadOnly, ),
assigneeEmployeeNumber: widget.assigneeEmployeeNumber, );
contactEmployeeINumber: widget.contactEmployeeINumber,
myLoginUserID: widget.myLoginUserID,
)));
},
).toShimmer(context: context, isShow: chatProvider.chatLoginTokenLoading, radius: 30, height: 30, width: 30),
);
})
: const SizedBox();
} }
} }
class _ChatBadgeState {
final bool isLoading;
final int unreadCount;
const _ChatBadgeState({required this.isLoading, required this.unreadCount});
int get effectiveUnread => isLoading ? 0 : unreadCount;
@override
bool operator ==(Object other) =>
identical(this, other) || (other is _ChatBadgeState && other.isLoading == isLoading && other.effectiveUnread == effectiveUnread);
@override
int get hashCode => Object.hash(isLoading, effectiveUnread);
}

Loading…
Cancel
Save