import 'package:audio_waveforms/audio_waveforms.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/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/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 'model/user_chat_history_model.dart'; enum ChatState { idle, voiceRecordingStarted, voiceRecordingCompleted } class ChatPage extends StatefulWidget { int moduleId; int requestId; String title; bool readOnly; ChatPage({Key? key, required this.moduleId, required this.requestId, this.title = "Chat", this.readOnly = false}) : super(key: key); @override _ChatPageState createState() { return _ChatPageState(); } } class _ChatPageState extends State { bool isSender = false; bool isAudioRecording = false; String? recordedFilePath; final RecorderController recorderController = RecorderController(); PlayerController playerController = PlayerController(); TextEditingController textEditingController = TextEditingController(); ChatState chatState = ChatState.idle; @override void initState() { super.initState(); getChatToken(); playerController.addListener(() async { // if (playerController.playerState == PlayerState.playing && playerController.maxDuration == await playerController.getDuration()) { // await playerController.stopPlayer(); // setState(() {}); // } }); } void getChatToken() { String assigneeEmployeeNumber = ""; Provider.of(context, listen: false).getUserAutoLoginToken(widget.moduleId, widget.requestId + 2, widget.title, context.settingProvider.username, assigneeEmployeeNumber); } @override void dispose() { playerController.dispose(); recorderController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.white10, appBar: DefaultAppBar(title: widget.title), body: Consumer(builder: (context, chatProvider, child) { if (chatProvider.chatLoginTokenLoading) return const CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 3).center; if (chatProvider.chatLoginResponse == 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: "Retry", maxWidth: true, buttonColor: AppColor.primary10, onPressed: () async { getChatToken(); }, ).paddingOnly(start: 48, end: 48) ], ).center; } return Column( children: [ Container( color: AppColor.neutral50, constraints: const BoxConstraints(maxHeight: 56), padding: const EdgeInsets.all(16), child: Row( children: [ Text( chatProvider.recipient?.userName ?? "", overflow: TextOverflow.ellipsis, maxLines: 2, style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10), ).expanded, 4.width, Text( "View All Documents", style: AppTextStyles.bodyText.copyWith( color: AppColor.white10, decoration: TextDecoration.underline, decorationColor: AppColor.white10, ), ), ], ), ), Container( color: AppColor.neutral100, child: chatProvider.userChatHistoryLoading ? 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), ], ) : chatProvider.chatResponseList.isEmpty ? Text( "Send a message to start conversation", overflow: TextOverflow.ellipsis, maxLines: 1, style: AppTextStyles.heading6.copyWith(color: AppColor.neutral50.withOpacity(.5), fontWeight: FontWeight.w500), ).center : ListView.builder( padding: const EdgeInsets.all(16), reverse: true, itemBuilder: (cxt, index) { final currentMessage = chatProvider.chatResponseList[index]; final bool showSenderName = (index == chatProvider.chatResponseList.length - 1) || (currentMessage.userId != chatProvider.chatResponseList[index + 1].userId); bool isSender = chatProvider.chatResponseList[index].userId == chatProvider.sender?.userId!; bool showDateHeader = false; if (index == chatProvider.chatResponseList.length - 1) { showDateHeader = true; } else { final nextMessage = chatProvider.chatResponseList[index + 1]; final currentDate = DateUtils.dateOnly(DateTime.parse(currentMessage.createdAt!)); final nextDate = DateUtils.dateOnly(DateTime.parse(nextMessage.createdAt!)); if (!currentDate.isAtSameMomentAs(nextDate)) { showDateHeader = true; } } return Column(mainAxisSize: MainAxisSize.min, children: [ if (showDateHeader) dateCard(currentMessage.createdAt?.chatMsgDate ?? ""), isSender ? senderMsgCard(showSenderName, chatProvider.chatResponseList[index]) : recipientMsgCard(showSenderName, chatProvider.chatResponseList[index]) ]); }, itemCount: chatProvider.chatResponseList.length)) .expanded, if (!widget.readOnly) ...[ Divider(height: 1, thickness: 1, color: const Color(0xff767676).withOpacity(.11)), SafeArea( child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 56), child: Row( children: [ if (chatState == ChatState.idle) ...[ TextFormField( controller: textEditingController, 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(), ), 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), 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: const 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: () { chatProvider.sendTextMessage(textEditingController.text).then((success) { if (success) { textEditingController.clear(); } }); }, style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded ), icon: chatProvider.messageIsSending ? const 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, ], ), ), ) ] ], ); })); } 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, ChatResponse? chatResponse, {bool loading = false, String msg = ""}) { Widget senderHeader = Row( mainAxisSize: MainAxisSize.min, children: [ Text( "${chatResponse?.userName ?? "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, Container( padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(right: 26 + 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: [ Text( chatResponse?.content ?? msg, style: AppTextStyles.bodyText2.copyWith(color: AppColor.neutral120), ).toShimmer(context: context, isShow: loading), if (loading) 4.height, Text( chatResponse?.createdAt?.chatMsgTime ?? "2:00 PM", style: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.neutral50.withOpacity(0.5)), ).toShimmer(context: context, isShow: loading), ], )), ], ), ); } Widget recipientMsgCard(bool showHeader, ChatResponse? chatResponse, {bool loading = false, String msg = ""}) { String extraSpaces = ""; int length = 0; if ((chatResponse?.content ?? "").isNotEmpty) { if (chatResponse!.content!.length < 8) { length = 8 - chatResponse.content!.length; } } String contentMsg = chatResponse?.content == null ? msg : chatResponse!.content! + extraSpaces; print("'$contentMsg'"); 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?.userName ?? "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: [ 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?.createdAt?.chatMsgTime ?? "2:00 PM", style: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.white10), ), ).toShimmer(context: context, isShow: loading), ], )), ], ), ); } }