import ' dart:convert ' ;
import ' dart:developer ' ;
import ' dart:io ' ;
import ' package:audio_waveforms/audio_waveforms.dart ' ;
import ' package:cached_network_image/cached_network_image.dart ' ;
import ' package:flutter/cupertino.dart ' ;
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/string_extensions.dart ' ;
import ' package:test_sa/extensions/text_extensions.dart ' ;
import ' package:test_sa/extensions/widget_extensions.dart ' ;
import ' package:test_sa/helper/utils.dart ' ;
import ' package:test_sa/models/service_request/service_request.dart ' ;
import ' package:test_sa/modules/cm_module/cm_detail_provider.dart ' ;
import ' package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart ' ;
import ' package:test_sa/modules/cx_module/chat/chat_api_client.dart ' ;
import ' package:test_sa/modules/cx_module/chat/chat_provider.dart ' ;
import ' package:test_sa/modules/cx_module/chat/view_all_attachment_page.dart ' ;
import ' package:test_sa/new_views/app_style/app_color.dart ' ;
import ' package:test_sa/new_views/common_widgets/app_filled_button.dart ' ;
import ' package:test_sa/new_views/common_widgets/default_app_bar.dart ' ;
import ' package:test_sa/views/widgets/sound/sound_player.dart ' ;
import ' helper/chat_audio_player.dart ' ;
import ' helper/chat_file_picker.dart ' ;
import ' helper/chat_file_viewer.dart ' ;
import ' model/chat_login_response_model.dart ' ;
import ' model/chat_participant_model.dart ' ;
import ' model/get_single_user_chat_list_model.dart ' ;
import ' model/user_chat_history_model.dart ' ;
//Need to refactor this ...
enum ChatState { idle , voiceRecordingStarted , voiceRecordingCompleted }
/// Helper classes for Chat Selector optimization
/// These enable granular rebuilds - only affected parts rebuild, not entire chat UI
class _ChatConnectionState {
final bool isLoading ;
final ChatLoginResponse ? loginResponse ;
const _ChatConnectionState ( {
required this . isLoading ,
required this . loginResponse ,
} ) ;
@ override
bool operator = = ( Object other ) = > identical ( this , other ) | | other is _ChatConnectionState & & isLoading = = other . isLoading & & loginResponse = = other . loginResponse ;
@ override
int get hashCode = > Object . hash ( isLoading , loginResponse ) ;
}
class _ChatHeaderState {
final Participants ? recipient ;
final bool isTyping ;
const _ChatHeaderState ( {
required this . recipient ,
required this . isTyping ,
} ) ;
@ override
bool operator = = ( Object other ) = > identical ( this , other ) | | other is _ChatHeaderState & & recipient = = other . recipient & & isTyping = = other . isTyping ;
@ override
int get hashCode = > Object . hash ( recipient , isTyping ) ;
}
class _ChatMessagesState {
final bool isLoading ;
final List < SingleUserChatModel > messages ;
final int messageCount ;
final DateTime ? lastMessageTime ;
_ChatMessagesState ( {
required this . isLoading ,
required this . messages ,
} ) : messageCount = messages . length ,
lastMessageTime = messages . isNotEmpty ? messages . first . createdDate : null ;
@ override
bool operator = = ( Object other ) = >
identical ( this , other ) | |
other is _ChatMessagesState & & isLoading = = other . isLoading & & messageCount = = other . messageCount & & lastMessageTime = = other . lastMessageTime ; // FIX: Compare timestamp to detect new messages
@ override
int get hashCode = > Object . hash ( isLoading , messageCount , lastMessageTime ) ;
}
class _ChatSendButtonState {
final bool isSending ;
const _ChatSendButtonState ( { required this . isSending } ) ;
@ override
bool operator = = ( Object other ) = > identical ( this , other ) | | other is _ChatSendButtonState & & isSending = = other . isSending ;
@ override
int get hashCode = > isSending . hashCode ;
}
class ChatPage extends StatefulWidget {
int moduleId ;
int requestId ;
String ? assigneeEmployeeNumber ;
String ? myLoginUserID ;
String ? contactEmployeeINumber ;
String title ;
bool readOnly ;
ChatPage ( { Key ? key , required this . moduleId , required this . requestId , this . title = " Chat " , this . readOnly = false , this . assigneeEmployeeNumber , this . myLoginUserID , this . contactEmployeeINumber } )
: super ( key: key ) ;
@ override
_ChatPageState createState ( ) {
return _ChatPageState ( ) ;
}
}
class _ChatPageState extends State < ChatPage > {
bool isSender = false ;
bool isAudioRecording = false ;
String ? recordedFilePath ;
final RecorderController recorderController = RecorderController ( ) ;
PlayerController playerController = PlayerController ( ) ;
TextEditingController textEditingController = TextEditingController ( ) ;
ChatState chatState = ChatState . idle ;
late String receiver ;
@ override
void initState ( ) {
super . initState ( ) ;
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
loadChatHistory ( ) ;
} ) ;
playerController . addListener ( ( ) async {
// if (playerController.playerState == PlayerState.playing && playerController.maxDuration == await playerController.getDuration()) {
// await playerController.stopPlayer();
// setState(() {});
// }
} ) ;
}
void loadChatHistory ( ) async {
// // String assigneeEmployeeNumber = Provider.of<ServiceRequestDetailProvider>(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? "";
// // String myEmployeeId = context.userProvider.user!.username!;
// //
//
// receiver = context.userProvider.isNurse ? widget.assigneeEmployeeNumber : widget.myEmployeeID;
//
// Provider.of<ChatProvider>(context, listen: false).connectToHub(widget.moduleId, widget.requestId, widget.myEmployeeID, widget.assigneeEmployeeNumber);
//
// receiver = context.userProvider.isNurse
// ? assigneeEmployeeNumber
// :myEmployeeId;
String assigneeEmployeeNumber = widget . assigneeEmployeeNumber ? ? Provider . of < CMDetailProvider > ( context , listen: false ) . currentWorkOrder ? . data ? . assignedEmployee ? . employeeId ? ? " " ;
String myEmployeeId = widget . myLoginUserID ? ? context . userProvider . user ! . username ! ;
receiver = context . userProvider . isNurse
? assigneeEmployeeNumber
: ( context . userProvider . isEngineer
? ( widget . contactEmployeeINumber ? ?
( Provider . of < CMDetailProvider > ( context , listen: false ) . currentWorkOrder ! . data ! . workOrderContactPerson . isEmpty
? Provider . of < CMDetailProvider > ( context , listen: false ) . currentWorkOrder ! . data ! . workOrderCreatedBy ! . employeeId !
: Provider . of < CMDetailProvider > ( context , listen: false ) . currentWorkOrder ! . data ! . workOrderContactPerson . first . employeeId ! ) )
: " " ) ;
final chatProvider = context . read < ChatProvider > ( ) ;
// final chatProvider = Provider.of<C;
chatProvider . connectToHub ( widget . moduleId , widget . requestId , myEmployeeId , receiver , widget . readOnly , isMounted: mounted ) ;
bool readStatus = await chatProvider . resetCount ( moduleId: widget . moduleId , referenceNo: widget . requestId , userId: context . userProvider . user ? . id ) ;
if ( readStatus ) {
chatProvider . chatParticipantModel ? . unreadCount = 0 ;
}
}
@ override
void dispose ( ) {
chatHubConnection ? . stop ( ) ;
playerController . dispose ( ) ;
recorderController . dispose ( ) ;
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
return Scaffold (
// backgroundColor: AppColor.white10,
appBar: DefaultAppBar ( title: widget . title ) ,
// OPTIMIZATION: Using Selector for connection state (rarely changes)
// This prevents rebuilding entire chat UI on every message/typing event
body: Selector < ChatProvider , _ChatConnectionState > (
selector: ( _ , provider ) = > _ChatConnectionState (
isLoading: provider . chatLoginTokenLoading ,
loginResponse: provider . chatLoginResponse ,
) ,
builder: ( context , connectionState , child ) {
if ( connectionState . isLoading ) return CircularProgressIndicator ( color: AppColor . loadingColor ( context ) , strokeWidth: 3 ) . center ;
if ( connectionState . loginResponse = = null ) {
return Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . center ,
children: [
Text (
" Failed to connect chat " ,
overflow: TextOverflow . ellipsis ,
maxLines: 1 ,
style: AppTextStyles . heading6 . copyWith ( color: AppColor . neutral50 , fontWeight: FontWeight . w500 ) ,
) ,
24. height ,
AppFilledButton (
label: " Go Back " ,
maxWidth: true ,
buttonColor: AppColor . primary10 ,
onPressed: ( ) = > Navigator . pop ( context ) ,
) . paddingOnly ( start: 48 , end: 48 )
] ,
) . center ;
}
// Chat UI - use provider directly for complex interactions
final chatProvider = Provider . of < ChatProvider > ( context , listen: false ) ;
return Column (
children: [
// OPTIMIZATION: Separate Selector for header - only rebuilds on typing/recipient change
Selector < ChatProvider , _ChatHeaderState > (
selector: ( _ , provider ) = > _ChatHeaderState (
recipient: provider . recipient ,
isTyping: provider . isTyping ,
) ,
builder: ( context , headerState , _ ) = > Container (
color: AppColor . neutral50 ,
constraints: const BoxConstraints ( maxHeight: 56 ) ,
padding: const EdgeInsets . only ( left: 16 , right: 16 , top: 8 , bottom: 8 ) ,
alignment: Alignment . center ,
child: Row (
children: [
Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
headerState . recipient ? . userName ? ? " " ,
overflow: TextOverflow . ellipsis ,
maxLines: 1 ,
style: AppTextStyles . bodyText2 . copyWith ( color: AppColor . white10 ) ,
) ,
AnimatedSize (
duration: const Duration ( milliseconds: 250 ) ,
child: SizedBox (
height: headerState . isTyping ? null : 0 ,
child: Text (
" Typing... " ,
maxLines: 1 ,
style: AppTextStyles . overline . copyWith ( color: AppColor . white10 ) ,
) ,
) ) ,
] ,
) . expanded ,
4. width ,
Text (
" View All Documents " ,
style: AppTextStyles . bodyText . copyWith (
color: AppColor . white10 ,
decoration: TextDecoration . underline ,
decorationColor: AppColor . white10
) ,
) . onPress ( ( ) {
Navigator . push ( context , CupertinoPageRoute ( builder: ( context ) = > ViewAllAttachmentPage ( moduleId: widget . moduleId , requestId: widget . requestId ) ) ) ;
} ) ,
] ,
) ,
) ,
) ,
// OPTIMIZATION: Separate Selector for message list - only rebuilds when messages change
Selector < ChatProvider , _ChatMessagesState > (
selector: ( _ , provider ) = > _ChatMessagesState (
isLoading: provider . userChatHistoryLoading ,
messages: provider . chatResponseList ,
) ,
builder: ( context , messagesState , _ ) = > Container (
// color: AppColor.neutral100,
child: messagesState . isLoading
? ListView (
padding: const EdgeInsets . all ( 16 ) ,
children: [
recipientMsgCard ( true , null , msg: " Please let me know what is the issue? Please let me know what is the issue? " , loading: true ) ,
recipientMsgCard ( false , null , msg: " testing " , loading: true ) ,
recipientMsgCard ( false , null , msg: " testing testing testing " , loading: true ) ,
senderMsgCard ( true , null , msg: " Please let me know what is the issue? Please let me know what is the issue? " , loading: true ) ,
senderMsgCard ( false , null , msg: " Please let me know what is the issue? " , loading: true ) ,
] ,
)
: messagesState . messages . isEmpty
? Text (
" Send a message to start conversation " ,
overflow: TextOverflow . ellipsis ,
maxLines: 1 ,
style: AppTextStyles . heading6 . copyWith ( color: AppColor . lightTextColor ( context ) , fontWeight: FontWeight . w500 ) ,
) . center
: ListView . builder (
padding: const EdgeInsets . all ( 16 ) ,
reverse: true ,
itemBuilder: ( cxt , index ) {
final currentMessage = messagesState . messages [ index ] ;
final bool showSenderName = ( index = = messagesState . messages . length - 1 ) | | ( currentMessage . currentUserId ! = messagesState . messages [ index + 1 ] . currentUserId ) ;
bool isSender = messagesState . messages [ index ] . currentUserId = = chatProvider . sender ? . userId ! ;
bool showDateHeader = false ;
if ( index = = messagesState . messages . length - 1 ) {
showDateHeader = true ;
} else {
final nextMessage = messagesState . messages [ index + 1 ] ;
final currentDate = DateUtils . dateOnly ( currentMessage . createdDate ! ) ;
final nextDate = DateUtils . dateOnly ( nextMessage . createdDate ! ) ;
if ( ! currentDate . isAtSameMomentAs ( nextDate ) ) {
showDateHeader = true ;
}
}
return Column ( mainAxisSize: MainAxisSize . min , children: [
if ( showDateHeader ) dateCard ( currentMessage . createdDate ? . toString ( ) . chatMsgDateWithYear ? ? " " ) ,
isSender ? senderMsgCard ( showSenderName , messagesState . messages [ index ] , index: index ) : recipientMsgCard ( showSenderName , messagesState . messages [ index ] )
] ) ;
} ,
itemCount: messagesState . messages . length ) ,
) ,
) . expanded ,
if ( ! widget . readOnly ) . . . [
const Divider ( ) . defaultStyle ( context ) ,
SafeArea (
child: ConstrainedBox (
constraints: const BoxConstraints ( minHeight: 56 ) ,
child: Row (
children: [
if ( chatState = = ChatState . idle ) . . . [
TextFormField (
controller: textEditingController ,
cursorColor: AppColor . loadingColor ( context ) ,
style: TextStyle ( fontWeight: FontWeight . w500 , fontSize: context . isTablet ( ) ? 16 : 14 , color: AppColor . textStyleColor ( context ) ) ,
minLines: 1 ,
maxLines: 3 ,
textInputAction: TextInputAction . none ,
keyboardType: TextInputType . multiline ,
onTap: ( ) {
chatHubConnection ! . invoke ( " SendTypingAsync " , args: [ receiver ] ) ;
} ,
onTapOutside: ( PointerDownEvent event ) {
chatHubConnection ! . invoke ( " SendStopTypingAsync " , args: [ receiver ] ) ;
} ,
onChanged: ( text ) {
chatHubConnection ! . invoke ( " SendTypingAsync " , args: [ receiver ] ) ;
} ,
decoration: InputDecoration (
enabledBorder: InputBorder . none ,
focusedBorder: InputBorder . none ,
border: InputBorder . none ,
errorBorder: InputBorder . none ,
contentPadding: const EdgeInsets . only ( left: 16 , top: 8 , bottom: 8 ) ,
alignLabelWithHint: true ,
// filled: true,
constraints: const BoxConstraints ( ) ,
suffixIconConstraints: const BoxConstraints ( ) ,
hintText: " Type your message here... " ,
hintStyle: TextStyle ( fontWeight: FontWeight . w500 , fontSize: context . isTablet ( ) ? 16 : 12 , color: AppColor . lightTextColor ( context ) ) ,
) ,
) . expanded ,
IconButton (
onPressed: ( ) async {
FocusScope . of ( context ) . unfocus ( ) ;
File ? file = ( await showModalBottomSheet (
context: context ,
shape: const RoundedRectangleBorder (
borderRadius: BorderRadius . vertical (
top: Radius . circular ( 20 ) ,
) ,
) ,
clipBehavior: Clip . antiAliasWithSaveLayer ,
builder: ( BuildContext context ) = > ChatFilePicker ( ) ) ) as File ? ;
if ( file ! = null ) {
Utils . showUploadingDialog ( context ) ;
await chatProvider . uploadAttachments ( context . userProvider . user ! . username ! , file , " 1 " ) ;
Utils . hideLoading ( context ) ;
}
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: " chat_attachment " . toSvgAsset ( width: 24 , height: 24 , color: AppColor . icon2Color ( context ) ) ,
constraints: const BoxConstraints ( ) ,
) ,
IconButton (
onPressed: ( ) async {
await recorderController . checkPermission ( ) ;
if ( recorderController . hasPermission ) {
chatState = ChatState . voiceRecordingStarted ;
recorderController . record ( ) ;
setState ( ( ) { } ) ;
} else {
" Audio permission denied. Please enable from setting " . showToast ;
}
// if (!isPermissionGranted) {
// "Audio permission denied. Please enable from setting".showToast;
// return;
// }
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: " chat_mic " . toSvgAsset ( width: 24 , height: 24 , color: AppColor . icon2Color ( context ) ) ,
constraints: const BoxConstraints ( ) ,
) ,
] else if ( chatState = = ChatState . voiceRecordingStarted ) . . . [
AudioWaveforms (
size: Size ( MediaQuery . of ( context ) . size . width , 56.0 ) ,
waveStyle: const WaveStyle ( waveColor: AppColor . neutral50 , extendWaveform: true , showMiddleLine: false ) ,
padding: const EdgeInsets . only ( left: 16 ) ,
recorderController: recorderController , // Customize how waveforms looks.
) . expanded ,
IconButton (
onPressed: ( ) async {
isAudioRecording = false ;
await recorderController . pause ( ) ;
recordedFilePath = await recorderController . stop ( ) ;
chatState = ChatState . voiceRecordingCompleted ;
setState ( ( ) { } ) ;
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: Icon ( Icons . stop_circle_rounded ) ,
constraints: const BoxConstraints ( ) ,
)
] else if ( chatState = = ChatState . voiceRecordingCompleted ) . . . [
if ( playerController . playerState = = PlayerState . playing )
IconButton (
onPressed: ( ) async {
await playerController . pausePlayer ( ) ;
await playerController . stopPlayer ( ) ;
setState ( ( ) { } ) ;
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: const Icon ( Icons . stop_circle_outlined , size: 20 ) ,
constraints: const BoxConstraints ( ) ,
)
else
IconButton (
onPressed: ( ) async {
await playerController . preparePlayer ( path: recordedFilePath ! ) ;
await playerController . startPlayer ( ) ;
setState ( ( ) { } ) ;
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: const Icon ( Icons . play_circle_fill_rounded , size: 20 ) ,
constraints: const BoxConstraints ( ) ,
) ,
AudioFileWaveforms (
playerController: playerController ,
waveformData: recorderController . waveData ,
enableSeekGesture: false ,
continuousWaveform: false ,
waveformType: WaveformType . long ,
playerWaveStyle: PlayerWaveStyle (
fixedWaveColor: AppColor . neutral50 ,
liveWaveColor: AppColor . primary10 ,
showSeekLine: true ,
) ,
size: Size ( MediaQuery . of ( context ) . size . width , 56.0 ) ,
) . expanded ,
IconButton (
onPressed: ( ) async {
await playerController . stopPlayer ( ) ;
recorderController . reset ( ) ;
recordedFilePath = null ;
chatState = ChatState . idle ;
setState ( ( ) { } ) ;
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
icon: " delete_icon " . toSvgAsset ( width: 24 , height: 24 ) ,
constraints: const BoxConstraints ( ) ,
) ,
] ,
// if (recordedFilePath == null) ...[
// isAudioRecording
// ? AudioWaveforms(
// size: Size(MediaQuery.of(context).size.width, 56.0),
//
// // enableGesture: true,
//
// waveStyle: const WaveStyle(waveColor: AppColor.neutral50, extendWaveform: true, showMiddleLine: false),
// padding: const EdgeInsets.only(left: 16),
// recorderController: recorderController, // Customize how waveforms looks.
// ).expanded
// : TextFormField(
// cursorColor: AppColor.neutral50,
// style: AppTextStyles.bodyText.copyWith(color: AppColor.neutral50),
// minLines: 1,
// maxLines: 3,
// textInputAction: TextInputAction.none,
// keyboardType: TextInputType.multiline,
// decoration: InputDecoration(
// enabledBorder: InputBorder.none,
// focusedBorder: InputBorder.none,
// border: InputBorder.none,
// errorBorder: InputBorder.none,
// contentPadding: const EdgeInsets.only(left: 16, top: 8, bottom: 8),
// alignLabelWithHint: true,
// filled: true,
// constraints: const BoxConstraints(),
// suffixIconConstraints: const BoxConstraints(),
// hintText: "Type your message here...",
// hintStyle: AppTextStyles.bodyText.copyWith(color: const Color(0xffCCCCCC)),
// // suffixIcon: Row(
// // mainAxisSize: MainAxisSize.min,
// // crossAxisAlignment: CrossAxisAlignment.end,
// // mainAxisAlignment: MainAxisAlignment.end,
// // children: [
// //
// // 8.width,
// // ],
// // )
// ),
// ).expanded,
// IconButton(
// onPressed: () {},
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: "chat_attachment".toSvgAsset(width: 24, height: 24),
// constraints: const BoxConstraints(),
// ),
// ],
// if (recordedFilePath == null)
// ...[]
// else ...[
// IconButton(
// onPressed: () async {
// await playerController.preparePlayer(path: recordedFilePath!);
// playerController.startPlayer();
// },
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: const Icon(Icons.play_circle_fill_rounded, size: 20),
// constraints: const BoxConstraints(),
// ),
// AudioFileWaveforms(
// playerController: playerController,
// size: Size(300, 50),
// ).expanded,
// IconButton(
// onPressed: () async {
// playerController.pausePlayer();
// },
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: const Icon(Icons.pause_circle_filled_outlined, size: 20),
// constraints: const BoxConstraints(),
// ),
// IconButton(
// onPressed: () {},
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: "delete".toSvgAsset(width: 24, height: 24),
// constraints: const BoxConstraints(),
// ),
// ],
// if (isAudioRecording && recorderController.isRecording)
// IconButton(
// onPressed: () {},
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: "chat_msg_send".toSvgAsset(width: 24, height: 24),
// constraints: const BoxConstraints(),
// ),
// if (isAudioRecording)
// IconButton(
// onPressed: () async {
// isAudioRecording = false;
// await recorderController.pause();
// recordedFilePath = await recorderController.stop();
// chatState = ChatState.voiceRecordingCompleted;
// setState(() {});
// },
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: Icon(Icons.stop_circle_rounded),
// constraints: const BoxConstraints(),
// )
// else
// IconButton(
// onPressed: () async {
// await recorderController.checkPermission();
// if (recorderController.hasPermission) {
// setState(() {
// isAudioRecording = true;
// });
// recorderController.record();
// } else {
// "Audio permission denied. Please enable from setting".showToast;
// }
// // if (!isPermissionGranted) {
// // "Audio permission denied. Please enable from setting".showToast;
// // return;
// // }
// },
// style: const ButtonStyle(
// tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
// ),
// icon: "chat_mic".toSvgAsset(width: 24, height: 24),
// constraints: const BoxConstraints(),
// ),
IconButton (
splashColor: Colors . transparent ,
highlightColor: Colors . transparent ,
hoverColor: Colors . transparent ,
onPressed: ( ) async {
if ( chatState = = ChatState . voiceRecordingCompleted ) {
Utils . showUploadingDialog ( context ) ;
try {
await chatProvider . uploadAttachments ( context . userProvider . user ! . username ! , File ( recordedFilePath ! ) , " 1 " ) ;
Utils . hideLoading ( context ) ;
await playerController . stopPlayer ( ) ;
recorderController . reset ( ) ;
recordedFilePath = null ;
chatState = ChatState . idle ;
setState ( ( ) { } ) ;
} catch ( ex ) {
Utils . hideLoading ( context ) ;
}
} else {
if ( textEditingController . text . isEmpty ) return ;
chatProvider . invokeSendMessage ( {
" Contant " : textEditingController . text ,
// "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f",
// "CreatedDate": "2025-11-09T18:58:12.502Z",
" CurrentEmployeeNumber " : context . userProvider . user ! . username ! ,
" ChatEventId " : 1 ,
" ConversationId " : chatProvider . chatParticipantModel ! . id ! . toString ( ) ,
" ModuleCode " : widget . moduleId . toString ( ) ,
" ReferenceNumber " : widget . requestId . toString ( ) ,
" UserChatHistoryLineRequestList " : [
{ " TargetEmployeeNumber " : receiver , " TargetUserStatus " : 1 , " IsSeen " : false , " IsDelivered " : true , " SeenOn " : null , " DeliveredOn " : null }
]
} ) . then ( ( success ) {
if ( success ) {
textEditingController . clear ( ) ;
}
} ) ;
}
} ,
style: const ButtonStyle (
tapTargetSize: MaterialTapTargetSize . shrinkWrap , // or .padded
) ,
// OPTIMIZATION: Separate Selector for send button - only rebuilds when sending state changes
icon: Selector < ChatProvider , _ChatSendButtonState > (
selector: ( _ , provider ) = > _ChatSendButtonState ( isSending: provider . messageIsSending ) ,
builder: ( _ , sendState , __ ) = > sendState . isSending
? SizedBox (
height: 24 ,
width: 24 ,
child: CircularProgressIndicator ( color: AppColor . primary10 , strokeWidth: 2 ) ,
)
: " chat_msg_send " . toSvgAsset ( width: 24 , height: 24 ) ,
) ,
constraints: const BoxConstraints ( ) ,
) ,
8. width ,
] ,
) ,
) ,
)
]
] ,
) ;
} , // End of connection state Selector builder
) ) ; // End of Selector and Scaffold body
}
Widget dateCard ( String date ) {
return Container (
padding: const EdgeInsets . symmetric ( vertical: 4 , horizontal: 8 ) ,
margin: const EdgeInsets . only ( top: 16 , bottom: 8 ) ,
decoration: BoxDecoration (
color: AppColor . neutral50 ,
borderRadius: BorderRadius . circular ( 6 ) ,
) ,
child: Text ( date , style: AppTextStyles . bodyText2 . copyWith ( color: AppColor . white10 ) ) )
. center ;
}
Widget senderMsgCard ( bool showHeader , SingleUserChatModel ? chatResponse , { bool loading = false , String msg = " " , int index = 0 } ) {
Widget senderHeader = Row (
mainAxisSize: MainAxisSize . min ,
children: [
Text (
" ${ chatResponse ? . currentUserName ? ? " User " } (Me) " ,
overflow: TextOverflow . ellipsis ,
maxLines: 1 ,
style: AppTextStyles . bodyText . copyWith ( color: AppColor . neutral50 , fontWeight: FontWeight . w600 ) ,
) . toShimmer ( context: context , isShow: loading ) ,
8. width ,
Container (
height: 26 ,
width: 26 ,
decoration: const BoxDecoration ( shape: BoxShape . circle , color: Colors . grey ) ,
) . toShimmer ( context: context , isShow: loading ) ,
] ,
) ;
return Align (
alignment: Alignment . centerRight ,
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
if ( showHeader ) . . . [ senderHeader , 4. height ] else 8. height ,
Row (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
Flexible (
fit: FlexFit . loose ,
child: Container (
padding: const EdgeInsets . all ( 8 ) ,
margin: const EdgeInsets . only ( right: 8 , left: 26 + 8 ) ,
decoration: BoxDecoration (
color: loading ? Colors . transparent : AppColor . white10 ,
borderRadius: BorderRadius . circular ( 6 ) ,
) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
if ( chatResponse ? . downloadUrl ! = null ) . . . [
// if (chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "audio")
// // ASoundPlayer(audio: chatResponse.downloadUrl!)
//
// ChatAudioPlayer(chatResponse!.fileTypeResponse!, chatResponse.downloadUrl!)
// // if (
// //
// // chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "image"
// //
// // )
//
// else
ChatFileViewer ( chatResponse ! . downloadUrl ! , chatResponse . fileTypeResponse ! ) ,
// Container(
// width: 250,
// height: 250,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12),
// border: Border.all(color: Colors.grey.withOpacity(.5), width: 1),
// image: DecorationImage(
// image: NetworkImage(chatResponse.downloadUrl!, headers: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"}),
// ),
// ),
// // child: CachedNetworkImage(
// // imageUrl: chatResponse.downloadUrl! ?? "",
// // // fit: boxFit ?? BoxFit.cover,
// // // alignment: Alignment.center,
// // // width: width,
// // // height: height,
// // httpHeaders: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"},
// // placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
// // // errorWidget: (context, url, error) => Icon(showDefaultIcon ? Icons.image_outlined : Icons.broken_image_rounded),
// // ),
//
// // Image.network(
// // chatResponse.downloadUrl!,
// // headers: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"},
// // ),
// )
] else
Text (
chatResponse ? . contant ? ? msg ,
style: AppTextStyles . bodyText2 . copyWith ( color: AppColor . neutral120 ) ,
) . toShimmer ( context: context , isShow: loading ) ,
if ( loading ) 4. height ,
Text (
chatResponse ? . createdDate ? . toString ( ) . chatMsgTime ? ? " 2:00 PM " ,
style: AppTextStyles . textFieldLabelStyle . copyWith ( color: AppColor . neutral50 . withOpacity ( 0.5 ) ) ,
) . toShimmer ( context: context , isShow: loading ) ,
] ,
) ) ,
) ,
if ( chatResponse ! = null )
if ( chatResponse . isSeen ! )
" chat_seen " . toSvgAsset ( width: 16 , height: 16 )
else if ( chatResponse . isDelivered ! )
" chat_delivered " . toSvgAsset ( width: 16 , height: 16 )
else
" chat_sent " . toSvgAsset ( width: 16 , height: 16 ) ,
( 26 + 8 ) . width ,
] ,
) ,
] ,
) ,
) ;
}
Widget recipientMsgCard ( bool showHeader , SingleUserChatModel ? chatResponse , { bool loading = false , String msg = " " } ) {
String extraSpaces = " " ;
int length = 0 ;
if ( ( chatResponse ? . contant ? ? " " ) . isNotEmpty ) {
if ( chatResponse ! . contant ! . length < 8 ) {
length = 8 - chatResponse . contant ! . length ;
}
}
String contentMsg = chatResponse ? . contant = = null ? msg : chatResponse ! . contant ! + extraSpaces ;
Widget recipientHeader = Row (
mainAxisSize: MainAxisSize . min ,
children: [
Container (
height: 26 ,
width: 26 ,
decoration: const BoxDecoration ( shape: BoxShape . circle , color: Colors . grey ) ,
) . toShimmer ( context: context , isShow: loading ) ,
8. width ,
Text (
chatResponse ? . currentUserName ? ? " User " ,
overflow: TextOverflow . ellipsis ,
maxLines: 1 ,
style: AppTextStyles . bodyText . copyWith ( color: AppColor . neutral50 , fontWeight: FontWeight . w600 ) ,
) . toShimmer ( context: context , isShow: loading )
] ,
) ;
return Align (
alignment: Alignment . centerLeft ,
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
if ( showHeader ) . . . [ recipientHeader , 4. height ] else 8. height ,
Container (
padding: const EdgeInsets . all ( 8 ) ,
margin: const EdgeInsets . only ( left: 26 + 8 , right: 26 + 8 ) ,
decoration: BoxDecoration (
color: loading ? Colors . transparent : AppColor . primary10 ,
borderRadius: BorderRadius . circular ( 6 ) ,
) ,
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
if ( chatResponse ? . downloadUrl ! = null ) . . . [
ChatFileViewer ( chatResponse ! . downloadUrl ! , chatResponse . fileTypeResponse ! ) ,
] else
Text (
contentMsg ,
style: AppTextStyles . bodyText2 . copyWith ( color: AppColor . white10 ) ,
) . paddingOnly ( end: 6 * length ) . toShimmer ( context: context , isShow: loading ) ,
if ( loading ) 4. height ,
Align (
alignment: Alignment . centerRight ,
widthFactor: 1 ,
child: Text (
chatResponse ? . createdDate ? . toString ( ) . chatMsgTime ? ? " 2:00 PM " ,
style: AppTextStyles . textFieldLabelStyle . copyWith ( color: AppColor . white10 ) ,
) ,
) . toShimmer ( context: context , isShow: loading ) ,
] ,
) ) ,
] ,
) ,
) ;
}
}