import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:selectable_autolink_text/selectable_autolink_text.dart'; import 'package:telnow_mobile_new/src/api/api_auth_provider.dart'; import 'package:telnow_mobile_new/src/layouts/functions/message.dart'; import 'package:telnow_mobile_new/src/layouts/components/template.dart'; import 'package:telnow_mobile_new/src/utils/U.dart'; import 'package:telnow_mobile_new/src/utils/provider.dart'; import 'package:translator/translator.dart'; import 'package:url_launcher/url_launcher.dart'; class WebMessageListPage extends StatefulWidget { Map? user; WebMessageListPage(this.user, {super.key}); @override State createState() => _WebMessageListPageState(); } class _WebMessageListPageState extends State { final MessageFunction messageFunc = MessageFunction(); final ApiAuthProvider apiAuthProvider = ApiAuthProvider(); final translator = GoogleTranslator(); String? idChat; String tenant = ''; String opponent = ''; ScrollController scrollController = ScrollController(); List messageData = []; int page = 0; bool isAfterLoad = false; String username = ''; bool isLoad = false; bool stopLoad = false; bool scrollBottom = false; bool isReverse = false; bool askBroadcast = false; @override void initState() { Provider.of(context, listen: false).reset(); if(widget.user == null){ messageFunc.getUser(context); } else{ Provider.of(context, listen: false).setUser(widget.user!); messageFunc.getDataMessages(context); } // TODO: implement initState super.initState(); } selectMessage(data, isMe) async{ if(idChat != data['chatId']){ setState(() { idChat = data['chatId']; username = Provider.of(context, listen: false).user()['userId']; askBroadcast = isMe; opponent = isMe ? data['recipientName'] : data['senderName']; tenant = data['recipient']; messageData = []; page = 0; isAfterLoad = false; isLoad = false; stopLoad = false; scrollBottom = false; isReverse = false; }); scrollController.addListener(() => scrollListener()); if (!isMe) { await apiAuthProvider.patchData('/api/messages/' + data['id'].toString(), {'readStatus': 'READ'}, context); } getMessage(); getCollectionData(); } } getMessage() async { String url = 'myMessages/$idChat'; String filter = '{"f":["uniqueId","LIKE","%-%"]}'; if (askBroadcast) { url = 'myBroadcast'; filter = '{"f":["1","EQ","1"]}'; } if (!isLoad && !stopLoad) { setState(() => isLoad = true); var mymess = await apiAuthProvider.getData('/api/messages/search/$url', {'isPaged': 'true', 'page': page.toString(), 'size': '20', 'filter': filter, 'tenant': tenant}, context); if (mymess.containsKey('_embedded') && mounted) { List data = mymess['_embedded']['myMessages']; for (int i = 0; i < data.length; i++) { if (username == data[i]['from']['user']) { data[i]['selected'] = false; } else{ if(U.autoTranslate()){ data[i]['translate'] = ''; } } setState(() => messageData.insert(0, data[i])); } setState(() { if (page == 0) { scrollBottom = true; } isLoad = false; page++; if (messageData.length >= mymess['page']['totalElements']) { stopLoad = true; } }); if(U.autoTranslate()){ translateMessage(); } } else { setState(() { isLoad = false; stopLoad = true; }); } } } getCollectionData() { FirebaseFirestore.instance.collection("tmMessages").doc('messages').collection(idChat!).snapshots().listen((querySnapshot) { setState(() { querySnapshot.docChanges.forEach((result) async { var data = result.doc.data(); if (result.type == DocumentChangeType.added && isAfterLoad) { if ((username == data!['from']['user'] && messageData.where((element) => element['uniqueId'] == data['uniqueId']).length > 0) || (username != data['from']['user'] && data['readStatus'] == 'DELETED')) { int index = messageData.indexWhere((element) => element['uniqueId'] == data['uniqueId']); messageData[index]['read'] = data['read']; messageData[index]['readStatus'] = data['readStatus']; messageData[index]['imageUrl'] = data['imageUrl']; } else { if(U.autoTranslate()){ var locale = context.locale.toString() == 'zh' ? 'zh-cn' : context.locale.toString(); var translate = await translator.translate(data['msg']??'', to: locale); data['translate'] = translate.text; } messageData.add(data); } } else if (result.type == DocumentChangeType.modified && isAfterLoad) { // print("modified"); if (messageData.where((element) => element['uniqueId'] == data!['uniqueId']).length > 0) { int index = messageData.indexWhere((element) => element['uniqueId'] == data!['uniqueId']); messageData[index]['read'] = data!['read']; messageData[index]['readStatus'] = data['readStatus']; } } else if (result.type == DocumentChangeType.removed && isAfterLoad) { // print("removed"); } }); isAfterLoad = true; }); }); } translateMessage(){ var locale = context.locale.toString() == 'zh' ? 'zh-cn' : context.locale.toString(); messageData.forEach((element) async{ if (username != element['from']['user'] && element['translate'] == '') { var translate = await translator.translate(element['msg']??'', to: locale); setState(() { element['translate'] = translate.text; }); } }); } deleteCollection() { try{ FirebaseFirestore.instance.collection("tmMessages").doc('messages').collection(idChat!).get().then((value) { for (DocumentSnapshot ds in value.docs) { ds.reference.delete(); } }); } catch(e){} } scrollToBottom() { scrollController.animateTo(scrollController.position.minScrollExtent, duration: Duration(milliseconds: 1), curve: Curves.decelerate); scrollBottom = false; } scrollListener() { if (scrollController.offset >= scrollController.position.maxScrollExtent) { // print('loadMessage'); getMessage(); } } @override Widget build(BuildContext context) { if (messageData.length > 0) { if (scrollController.position.maxScrollExtent > 0 && !isReverse) { setState(() => isReverse = true); } if (isAfterLoad && scrollBottom && isReverse) { scrollToBottom(); } } return Scaffold( backgroundColor: backgroundColor, appBar: PreferredSize(preferredSize: Size.fromHeight(0), child: AppBar(elevation: 0, backgroundColor: primaryColor)), body: Column( children: [ Container( padding: EdgeInsets.symmetric(vertical: 25, horizontal: 100), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('message'.tr(), style: TextStyle(color: textColor, fontSize: 17, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis), GestureDetector( child: Text('buttonBack'.tr(), style: TextStyle(color: primaryColor, fontSize: 14)), onTap: (){ deleteCollection(); navigateBack(context); }, ) ], ), ), divider(), Expanded( child: Container( width: double.infinity, height: double.infinity, padding: EdgeInsets.symmetric(vertical: 25, horizontal: 100), child: Row( children: [ Expanded( child: Stack( children: [ Container( width: double.infinity, height: double.infinity, child: EasyRefresh( header: MaterialHeader(clamping: true, color: primaryColor), onRefresh: () => messageFunc.onRefresh(context), child: Provider.of(context).data().isEmpty && !Provider.of(context).firstLoad()? loadingTemplate(() {},) : Provider.of(context).data().isEmpty ? Center( child: Text('noMessageText').tr(), ) : SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), child: Column( mainAxisSize: MainAxisSize.max, children: List.generate(Provider.of(context).data().length, (i) { bool _isMe = Provider.of(context, listen: false).data()[i]['userId'] == Provider.of(context, listen: false).user()['userId'] ? true : false; return GestureDetector( child: Container( padding: EdgeInsets.all(10), child: Row( children: [ Provider.of(context).data()[i]['senderAvatar'] != null && Provider.of(context).data()[i]['senderAvatar'] != '' ? CircleAvatar( backgroundImage: NetworkImage(Provider.of(context).data()[i]['senderAvatar']), radius: 24, ) : Container( height: 48, width: 48, child: Center(child: Text(_isMe ? Provider.of(context).data()[i]['recipientName'] == "all_informants" ? "allInformants".tr()[0] : Provider.of(context).data()[i]['recipientName'][0] : Provider.of(context).data()[i]['senderName'][0], style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18))), decoration: BoxDecoration(shape: BoxShape.circle, color: Color(U.getColor(_isMe ? Provider.of(context).data()[i]['recipient'] : Provider.of(context).data()[i]['recipientId']))), ), SizedBox(width: 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_isMe ? Provider.of(context).data()[i]['recipientName'] == "all_informants" ? "allInformants".tr() : Provider.of(context).data()[i]['recipientName'] : Provider.of(context).data()[i]['senderName']), SizedBox(height: 5), Row( children: [ _isMe ? Padding( padding: EdgeInsets.only(right: 5), child: Icon(Icons.done_all, size: 18, color: Colors.blueGrey), ) : Container(), Provider.of(context).data()[i]['isImage'] ? Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Provider.of(context).data()[i]['fileType'] == 'pdf' ? Icons.picture_as_pdf : Icons.image, color: Colors.black45, size: 16), SizedBox(width: 6), Provider.of(context).data()[i]['lastText'] == '' ? Provider.of(context).data()[i]['fileType'] == 'pdf' ? Text('pdfFile'.tr()) : Text('photo'.tr()) : Container() ], ), ) : Container(), Expanded( child: Text(Provider.of(context).data()[i]['lastText'], style: TextStyle(fontSize: 13), maxLines: 1, overflow: TextOverflow.ellipsis), ) ], ) ], ), ), SizedBox(width: 20), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(messageFunc.timeSet(Provider.of(context).data()[i]['lastDateTimeSend']), style: TextStyle(fontSize: 11, color: Provider.of(context).data()[i]['lastReadStatus'] == 'UNREAD' && !_isMe ? Colors.green : Colors.black45)), SizedBox(height: 4), Icon(Icons.circle, color: Colors.green.withValues(alpha: Provider.of(context).data()[i]['lastReadStatus'] == 'UNREAD' && !_isMe ? 1 : 0)) ], ) ], ), decoration: BoxDecoration(color: idChat != null && idChat == Provider.of(context, listen: false).data()[i]['chatId'] ? Color(0xff26DA17).withValues(alpha: 0.2) : Colors.white, borderRadius: BorderRadius.all(Radius.circular(12))), ), onTap: ()=>selectMessage(Provider.of(context, listen: false).data()[i], _isMe), ); }), ), ), ), decoration: BoxDecoration(color: Colors.white, border: Border.all(color: textColor.withValues(alpha: 0.15)), borderRadius: BorderRadius.all(Radius.circular(12))), ), Provider.of(context).user().isNotEmpty && Provider.of(context).user()['canSendMessage'] ? Align( alignment: Alignment.bottomRight, child: GestureDetector( child: Container( margin: EdgeInsets.only(right: 20, bottom: 20), width: 50, height: 50, alignment: Alignment.center, child: U.iconsax('message', color: Colors.white), decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.all(Radius.circular(50)) ), ), onTap: ()=>messageFunc.createMessage(context), ), ) : Container() ], ), ), SizedBox(width: 30), Expanded( child: Container( child: idChat != null ? Column( children: [ Container( width: double.infinity, padding: EdgeInsets.all(20), child: Text(opponent != 'all_informants' ? opponent : "allInformants".tr(), style: TextStyle(color: textColor, fontSize: 16)), ), divider(), Expanded( child: messageData.isEmpty && !isAfterLoad ? loadingTemplate(() {},) : SingleChildScrollView( padding: const EdgeInsets.fromLTRB(10, 10, 10, 7), controller: scrollController, reverse: isReverse, child: Column( children: List.generate(messageData.length, (i) { bool hideDate = i == 0 ? false : checkDate(messageData[i]['datetime'], messageData[i - 1]['datetime']); bool isNip = i == 0 ? true : !hideDate ? true : messageData[i]['from']['user'] == messageData[i - 1]['from']['user'] ? false : true; return Column( children: [ !hideDate ? Center( child: Padding( padding: const EdgeInsets.all(10), child: bubble_chat(Text(convertDate(messageData[i]['datetime'], context.locale.toString()), textAlign: TextAlign.center, style: TextStyle(fontSize: 12)), null, false, false), ), ) : Container(), Builder(builder: (context) { var isMe = messageData[i]['from']['user'] == Provider.of(context, listen: false).user()['userId']; var isFile = messageData[i]['imageUrl'] != null && messageData[i]['imageUrl'] != '' && messageData[i]['readStatus'] != 'DELETED'; var isPdf = isFile && messageData[i]['imageUrl'].split('.').last == 'pdf'; var isImg = isFile && !isPdf; var isTranslate = U.autoTranslate() && !isMe && messageData[i]['msg'] != messageData[i]['translate']; var wdg = messageData[i]['readStatus'] == 'DELETED' ? Text('deletedMessage'.tr(), style: TextStyle(fontSize: 14, color: Colors.black54, fontStyle: FontStyle.italic)) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ isPdf ? GestureDetector( child: Container( child: Builder(builder: (context) { var pdfContainer = Container( padding: EdgeInsets.all(6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.picture_as_pdf), SizedBox(width: 6), Flexible(child: Text(getImageName(messageData[i]['imageUrl']))) ], ), decoration: BoxDecoration(color: Colors.black12.withValues(alpha: 0.1), borderRadius: BorderRadius.all(Radius.circular(4))), ); return pdfContainer; }), ), onTap: () async { await launchUrl( messageData[i]['imageUrl']); }, ) : isImg ? GestureDetector( child: Container( margin: const EdgeInsets.only(bottom: 5), child: Builder(builder: (context) { return Image.network(messageData[i]['imageUrl'], fit: BoxFit.cover, width: 200, height: 200); }) ), onTap: () => navigateTo(context, PhotoPreview(opponent, messageData[i]['imageUrl'], true)), ) : SizedBox(height: 1), messageData[i]['msg'] != null && messageData[i]['msg'] != '' ? Column( crossAxisAlignment: !isMe?CrossAxisAlignment.start:CrossAxisAlignment.end, children: [ SizedBox(height: 4), SelectableAutoLinkText( messageData[i]['msg'], style: TextStyle(fontSize: 14, color: Colors.black), linkStyle: TextStyle(color: Colors.blueAccent), highlightedLinkStyle: TextStyle(color: Colors.blueAccent, backgroundColor: Colors.blueAccent.withAlpha(0x33)), onTap: (link) async{ if (await canLaunchUrl(Uri.parse(link))) { await launchUrl(Uri.parse(link)); } }, onLongPress: (link) { Clipboard.setData(new ClipboardData(text: link)).then((value){ showSuccess('link_copied'.tr(), context); }); }, enableInteractiveSelection: false, ), isTranslate ? Container( margin: EdgeInsets.only(top: 2), decoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.2)))), child: Text(messageData[i]['translate']!=''?'(${messageData[i]['translate']})':'...', style: TextStyle(fontSize: 14, color: Colors.black.withValues(alpha: 0.65), fontStyle: FontStyle.italic)), ) : Container(), ], ) : Container() ], ); var timeBubble = Positioned( bottom: 0, right: isNip ? 6 : 0, child: Text(DateFormat('HH:mm').format(DateTime.parse(messageData[i]['datetime'])), style: TextStyle(fontSize: 10, color: Colors.black45)), ); var expander = Expanded( flex: 8, child: Column( children: [ bubble_chat(wdg, timeBubble, isNip, isMe), SizedBox(height: 5), ], crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, ) ); return isMe ? Row( children: [ Expanded(flex: 2, child: Container()), expander, ], ) : Row( children: [ expander, Expanded(flex: 2, child: Container()), ], ); }), ], ); }), ), ), ) ], ) : Container(), decoration: BoxDecoration(color: Colors.white, border: Border.all(color: textColor.withValues(alpha: 0.15)), borderRadius: BorderRadius.all(Radius.circular(12))), ), ) ], ), ), ) ], ), ); } getImageName(imageUrl) { var imgSplit = imageUrl.toString().split('/'); return imgSplit[imgSplit.length - 1]; } Widget bubble_chat(Widget child, Widget? time, bool isNip, bool isMe) { bool isDate = false; if (time == null) { isDate = true; time = Container(); } Color color = isMe ? primaryColor.withValues(alpha: 0.3) : isDate ? Color(0xffD5F5FF) : Color(0xffECECEC); var clipPath = ClipPath( child: ConstrainedBox( constraints: BoxConstraints(minWidth: 50), child: Container( decoration: BoxDecoration( color: color, ), child: Padding(padding: EdgeInsets.all(8), child: isDate ? child : Stack( children: [ Padding( padding: EdgeInsets.only( bottom: 15, right: isNip && isMe ? 6 : 0, left: isNip && !isMe ? 6 : 0, ), child: child, ), time ], ), ), ), ), clipper: isMe ? MyClipper(isNip) : YourClipper(isNip), ); return Padding( padding: EdgeInsets.only( right: isNip && isMe ? 0 : 6, left: isNip && !isMe ? 0 : 6 ), child: clipPath ); } bool checkDate(date1, date2) { final dateToCheck1 = DateTime(DateTime.parse(date1).year, DateTime.parse(date1).month, DateTime.parse(date1).day); final dateToCheck2 = DateTime(DateTime.parse(date2).year, DateTime.parse(date2).month, DateTime.parse(date2).day); return dateToCheck1 == dateToCheck2 ? true : false; } String convertDate(date, locale) { final dateToCheck = DateTime.parse(date); final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final yesterday = DateTime(now.year, now.month, now.day - 1); final aDate = DateTime(dateToCheck.year, dateToCheck.month, dateToCheck.day); if (aDate == today) { return 'today'.tr(); } else if (aDate == yesterday) { return 'yesterday'.tr(); } else { return DateFormat('dd MMMM yyyy', locale).format(DateTime.parse(date)); } } }