Compare commits

...

2 Commits

Author SHA1 Message Date
Sikander Saleem deb8748fc1 improvements 4 weeks ago
Sikander Saleem 69fb1917fb improvement 4 weeks ago

@ -1,6 +1,5 @@
import 'package:flutter/material.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/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
@ -13,16 +12,16 @@ import '../chat_provider.dart';
import 'call_provider.dart';
class AudioCallPage extends StatefulWidget {
Participants sender;
Participants recipient;
int moduleId;
int? referenceID;
bool isAccept;
final Participants sender;
final Participants recipient;
final int moduleId;
final int? referenceID;
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
_AudioCallPageState createState() {
State<AudioCallPage> createState() {
return _AudioCallPageState();
}
}
@ -45,79 +44,67 @@ class _AudioCallPageState extends State<AudioCallPage> {
}
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) {
context.showConfirmDialog("Error while connecting to call service. Please try again later.", title: "Connection Error", onTap: () {
Navigator.pop(context);
Navigator.pop(context);
});
_showFailure("Error while connecting to call service. Please try again later.", title: "Connection Error");
return;
}
callPro.initAudioCallListeners();
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 {
callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient?.employeeNumber ?? "", false, widget.moduleId, widget.referenceID, onSuccess: (String msg) {
callPro.dispose();
Navigator.pop(context);
callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", false, widget.moduleId, widget.referenceID, onSuccess: (String msg) {
_closeCall();
}, onFailed: (String msg) {
callPro.dispose();
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;
});
_showFailure(msg, title: "Failed");
});
}
}
void _closeCall() {
callPro.endSession();
if (mounted) Navigator.pop(context);
}
Map<String, dynamic> json = {
// "callerID": AppState().chatDetails!.response!.id!.toString(),
// "callerDetails": AppState().chatDetails!.toJson(),
// "receiverID": params!.chatUser!.id.toString(),
// "receiverDetails": params!.chatUser!.toJson(),
// "title": params!.chatUser!.userName!.replaceAll(".", " "),
// "calltype": widget.isAudio ? "Audio" : "Video",
};
// CallDataModel callData = CallDataModel.fromJson(json);
// await Navigator.push(
// context,
// MaterialPageRoute(
// builder: (BuildContext context) => OutGoingCall(
// isVideoCall: callType == "VIDEO" ? true : false,
// outGoingCallData: callData,
// ),
// ),
// ).then((value) {
// print("then");
// callPro.stopListeners();
// });
void _showFailure(String message, {required String title}) {
callPro.endSession();
if (!mounted || 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: title,
message: message,
okTitle: "Go Back",
onTap: () {
Navigator.pop(cxt); // close dialog
if (mounted) Navigator.pop(context); // close call page
}),
),
).then((_) {
isDialogOpen = false;
});
}
@override
void dispose() {
callPro.stopListeners();
callPro.endSession();
super.dispose();
}

@ -38,17 +38,37 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
String currentStatus = "Connecting...";
bool _ownsConnection = false;
void _setStatus(String status) {
if (currentStatus == status) return;
currentStatus = status;
notifyListeners();
}
Future<void> _disposeConnection() async {
final HubConnection? connection = callHubConnection;
final bool owns = _ownsConnection;
callHubConnection = null;
_ownsConnection = false;
if (connection == null || !owns) return;
try {
if (callHubConnection != null) {
await callHubConnection!.stop();
}
await connection.stop();
} catch (e) {
} finally {
callHubConnection = null;
if (kDebugMode) {
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
@ -73,7 +93,6 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
final HttpConnectionOptions httpOp = HttpConnectionOptions(
transport: HttpTransportType.LongPolling,
skipNegotiation: false,
// Keep false: signalr_netcore 1.4.4 crashes when logging + LongPolling.
logMessageContent: false,
accessTokenFactory: () async => token,
requestTimeout: 30000,
@ -102,61 +121,115 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
}
Future<bool> buildHubConnection(String employeeNumber, {HubConnection? hubC}) async {
if (hubC != null) {
callHubConnection = hubC;
await callHubConnection!.start();
return true;
}
_setStatus("Connecting...");
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();
http.Response response = await ApiClient().postJsonForResponse(URLs.chatSdkToken, {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null});
if (response.statusCode == 200) {
ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
callHubConnection = await getHubConnection(chatResponse.token!);
await callHubConnection!.start();
return true;
final http.Response response = await ApiClient().postJsonForResponse(
URLs.chatSdkToken,
{"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null},
);
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) {
if (kDebugMode) {
print('⚠️ Error building call SignalR connection: $e');
}
await _disposeConnection();
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() {
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
callHubConnection!.on("OnOfferAsync", onOfferAsync);
callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
callHubConnection!.on('OnAudioToggle', onAudioToggle);
final HubConnection? hub = callHubConnection;
if (hub == null) return;
hub.on("OnCallAcceptedAsync", onCallAcceptedAsync);
hub.on("OnIceCandidateAsync", onIceCandidateAsync);
hub.on("OnOfferAsync", onOfferAsync);
hub.on("OnAnswerOffer", onAnswerOffer);
hub.on("OnHangUpAsync", onHangUpAsync);
hub.on("OnCallDeclinedAsync", onCallDeclinedAsync);
hub.on('OnAudioToggle', onAudioToggle);
}
void initCallListeners() {
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
callHubConnection!.on("OnOfferAsync", onOfferAsync);
callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
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);
final HubConnection? hub = callHubConnection;
if (hub == null) return;
initAudioCallListeners();
hub.on('OnCameraToggle', onCameraToggle);
}
//Video Constraints
@ -189,11 +262,17 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
};
Future<RTCPeerConnection> _createPeerConnection() async {
// {"url": "stun:stun.l.google.com:19302"},
Map<String, dynamic> configuration = {
"iceServers": [
{"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.onFailed = onFailed;
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 {
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 {
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}) {
invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
void endCall(String senderN, String recipientN, {required int moduleId}) async {
try {
await invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
} catch (e) {
if (kDebugMode) {
print('⚠️ HangUpAsync failed: $e');
}
}
}
void toggleMute() async {
await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!);
isMuted = !isMuted;
notifyListeners();
if (senderN == null || recipientN == null) return;
try {
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) {
@ -317,6 +438,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
void onCallAcceptedAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params");
_setStatus("Connected");
}
void onIceCandidateAsync(List<Object?>? params) {
@ -396,8 +518,12 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
} else if (invokeMethod == "AnswerOfferAsync") {
args = [targetUserID, data];
}
print("Invoking:$invokeMethod:${args}");
await callHubConnection?.invoke(invokeMethod, args: args);
print("Invoking:$invokeMethod:$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 {
@ -408,6 +534,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
callHubConnection?.off('OnOfferAsync');
callHubConnection?.off('OnHangUpAsync');
callHubConnection?.off('OnAudioToggle');
callHubConnection?.off('OnCameraToggle');
}
handleResponse(List<Object?>? params) {

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

@ -1,5 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -12,99 +10,157 @@ import 'chat_page.dart';
import 'chat_provider.dart';
class ChatWidget extends StatefulWidget {
int moduleId;
int requestId;
String? assigneeEmployeeNumber;
String? myLoginUserID;
String? contactEmployeeINumber;
bool isReadOnly;
bool isShow;
String title;
ChatWidget(
{Key? key,
this.isReadOnly = false,
this.title = "Chat",
required this.moduleId,
required this.requestId,
this.assigneeEmployeeNumber,
this.myLoginUserID,
this.contactEmployeeINumber,
this.isShow = false})
: super(key: key);
final int moduleId;
final int requestId;
final String? assigneeEmployeeNumber;
final String? myLoginUserID;
final String? contactEmployeeINumber;
final bool isReadOnly;
final bool isShow;
final String title;
const ChatWidget({
Key? key,
this.isReadOnly = false,
this.title = "Chat",
required this.moduleId,
required this.requestId,
this.assigneeEmployeeNumber,
this.myLoginUserID,
this.contactEmployeeINumber,
this.isShow = false,
}) : super(key: key);
@override
_ChatWidgetState createState() {
return _ChatWidgetState();
}
State<ChatWidget> createState() => _ChatWidgetState();
}
class _ChatWidgetState extends State<ChatWidget> {
@override
void initState() {
super.initState();
if (!widget.isShow) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<ChatProvider>(context, listen: false).reset();
getChatToken();
if (!mounted) return;
_prepareChatSession();
});
}
void getChatToken() {
// ChatProvider cProvider = Provider.of<ChatProvider>(context, listen: false);
// if (cProvider.chatLoginResponse != null && cProvider.referenceID == widget.requestId) return;
// // String assigneeEmployeeNumber = Provider.of<ServiceRequestDetailProvider>(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? "";
// String myEmployeeId = widget.myLoginUserID;
//
// 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
void didUpdateWidget(covariant ChatWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Re-prepare when the widget is pointed at a different request.
if (widget.isShow && (oldWidget.requestId != widget.requestId || oldWidget.moduleId != widget.moduleId)) {
_prepareChatSession();
}
}
@override
void dispose() {
super.dispose();
/// Fetches the chat token + participants, unless the exact same conversation
/// is already loaded in the (globally shared) [ChatProvider].
///
/// 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
Widget build(BuildContext context) {
return widget.isShow
? Consumer<ChatProvider>(builder: (pContext, chatProvider, _) {
final int unreadCount = (!chatProvider.chatLoginTokenLoading && (chatProvider.chatParticipantModel?.unreadCount ?? 0) > 0) ? chatProvider.chatParticipantModel!.unreadCount! : 0;
return CustomBadge2(
value: unreadCount,
top: 2,
right: 2,
minSize: 18,
child: IconButton(
icon: const Icon(Icons.chat_bubble),
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => ChatPage(
moduleId: widget.moduleId,
requestId: widget.requestId,
title: widget.title,
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();
if (!widget.isShow) return const SizedBox();
// Selector (instead of Consumer) so the badge does not rebuild on every
// incoming message / typing event pushed through ChatProvider.
return Selector<ChatProvider, _ChatBadgeState>(
selector: (_, provider) => _ChatBadgeState(
isLoading: provider.chatLoginTokenLoading,
unreadCount: provider.chatParticipantModel?.unreadCount ?? 0,
),
builder: (_, state, __) => CustomBadge2(
value: state.effectiveUnread,
top: 2,
right: 2,
minSize: 18,
child: IconButton(
icon: const Icon(Icons.chat_bubble),
onPressed: _openChat,
).toShimmer(context: context, isShow: state.isLoading, radius: 30, height: 30, width: 30),
),
);
}
}
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);
}

@ -103,6 +103,8 @@ class AttachmentFileViewer extends StatelessWidget {
bool isLoading = snapshot.connectionState == ConnectionState.waiting;
print("snapshot.data!:${snapshot.data}");
Widget widget;
if (isLoading) {
if (isAudio) {
@ -122,7 +124,10 @@ class AttachmentFileViewer extends StatelessWidget {
widget = const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2));
} else {
if (isImage) {
widget = Image.file(snapshot.data!, width: 48, height: 48, fit: BoxFit.contain);
widget = Image.file(snapshot.data!, width: 48, height: 48, fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48);
},);
} else {
widget = Image.asset(
"assets/images/${isPdf ? "pdf" : isExcel ? "excel" : "doc"}.png",

Loading…
Cancel
Save