import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.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/api/jwt_token.dart'; import 'package:telnow_mobile_new/src/injector/injector.dart'; import 'package:telnow_mobile_new/src/layouts/components/photo_chat.dart'; import 'package:telnow_mobile_new/src/layouts/components/template.dart'; import 'package:telnow_mobile_new/src/storage/sharedpreferences/shared_preferences_manager.dart'; import 'package:telnow_mobile_new/src/utils/U.dart'; import 'package:image/image.dart' as img; import 'package:translator/translator.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; class MobHistoryForumPage extends StatefulWidget { final Map data; const MobHistoryForumPage({required this.data, super.key}); @override State createState() => _MobHistoryForumPageState(); } class _MobHistoryForumPageState extends State { final ApiAuthProvider apiAuthProvider = ApiAuthProvider(); final JwtToken token = JwtToken(); final translator = GoogleTranslator(); final SharedPreferencesManager _sharedPreferencesManager = locator(); String? idChat; String? imagePath; TextEditingController controllerPesan = new TextEditingController(); ScrollController scrollController = ScrollController(); List messageData = []; List deletedMessage = []; int page = 0; double borderRadius = 50.0; String username = ''; bool isAfterLoad = false; bool isSend = false; bool isLoad = false; bool stopLoad = false; bool scrollBottom = false; bool isReverse = false; bool openChat = false; Uint8List? _image; @override void initState() { if(U.getInternetStatus()){ openChat = widget.data['currentState'] == 'DIMULAI' || widget.data['currentState'] == 'DISELESAIKAN'; getData(); scrollController.addListener(() => scrollListener()); } super.initState(); } getData() async { idChat = U.decodeBase64Url(_sharedPreferencesManager.getString(SharedPreferencesManager.keyAccessCode)!) + '-' + widget.data['ticketNo']; if (!kIsWeb) imagePath = await token.getPath() + '/TelNow/Images/'; var res = await token.getUserData(context); if (res != null) { username = res['userId']; getMessage(); getCollectionData(); } } setAsRead(ticketNo) async { var res = await apiAuthProvider.postData('/api/notifications/readMyForum/$ticketNo', null, null, context); if (res != null) { } } getMessage() async { if (!isLoad && !stopLoad) { setState(() => isLoad = true); setAsRead(widget.data['ticketNo']); var mymess = await apiAuthProvider.getData('/api/messages/search/myMessages/' + idChat!, {'isPaged': 'true', 'page': page.toString(), 'size': '20'}, context); if (mymess.containsKey('_embedded')) { 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])); downloadImage(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; }); } } } 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; }); } }); } 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); } downloadImage(data); } else if (result.type == DocumentChangeType.modified && isAfterLoad) { 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']; } } }); isAfterLoad = true; }); }); } Future cacheImage(String url, BuildContext context) async { bool hasNoError = true; var output = Completer(); precacheImage( NetworkImage(url), context, onError: (e, stackTrace) => hasNoError = false, ).then((_) => output.complete(hasNoError)); return output.future; } deleteCollection() { FirebaseFirestore.instance.collection("tmMessages").doc('messages').collection(idChat!).get().then((value) { for (DocumentSnapshot ds in value.docs) { ds.reference.delete(); } }); } getImageName(imageUrl) { var imgSplit = imageUrl.toString().split('/'); return imgSplit[imgSplit.length - 1]; } sendMessage(data) async { setState(() { isSend = false; controllerPesan.clear(); messageData.add({ 'msg': data['text'], 'datetime': DateTime.now().toString(), 'read': null, 'imageUrl': data['images'], 'readStatus': '', 'from': {'name': 'my name', 'user': data['userId']}, 'uniqueId': data['uniqueId'], 'selected': false, 'senderType': 'INFORMANT' }); scrollBottom = true; }); var res = await apiAuthProvider.postData('/api/messages', null, data, context); if (res != null) { int index = messageData.indexWhere((element) => element['uniqueId'] == data['uniqueId']); setState(() { messageData[index]['read'] = false; }); } else { int index = messageData.indexWhere((element) => element['uniqueId'] == data['uniqueId']); setState(() => messageData[index]['readStatus'] = 'FAILED'); } } scrollToBottom() { scrollController.animateTo(scrollController.position.minScrollExtent, duration: Duration(milliseconds: 1), curve: Curves.decelerate); scrollBottom = false; } scrollListener() { if (scrollController.offset >= scrollController.position.maxScrollExtent) { getMessage(); } } downloadImage(data) async { if (data['imageUrl'] != null && data['imageUrl'] != '' && !kIsWeb) { if (!File(imagePath! + getImageName(data['imageUrl'])).existsSync()) { int index = messageData.indexWhere((element) => element['uniqueId'] == data['uniqueId']); setState(() => messageData[index]['loadingImage'] = true); if (await cacheImage(data['imageUrl'], context)) { await apiAuthProvider.downloadImage(data['imageUrl'], imagePath! + getImageName(data['imageUrl'])); } setState(() => messageData[index]['loadingImage'] = false); } } } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { if (messageData.length > 0) { if (scrollController.position.maxScrollExtent > 0 && !isReverse) { setState(() => isReverse = true); } if (isAfterLoad && scrollBottom && isReverse) { scrollToBottom(); } } }); return Scaffold( backgroundColor: backgroundColor, appBar: appBarTemplate(context: context, title: 'forum'.tr()), body: Column( children: [ divider(), Container( alignment: Alignment.topCenter, width: U.bodyWidth(context), color: Colors.white, padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ imageTiles(imageUrl: widget.data['_requestImage'] ?? "null", width: 50, height: 40, radius: 4), SizedBox(width: 12), Expanded(child: Text(widget.data[U.langColumn(context, 'requestSubject')], style: TextStyle(color: textColor.withValues(alpha: 0.75), fontSize: 12, fontWeight: FontWeight.w300), maxLines: 2, overflow: TextOverflow.ellipsis)) ], ), ), divider(), U.getInternetStatus()?Expanded( child: Container( alignment: Alignment.topCenter, width: U.bodyWidth(context), child: messageData.length == 0 && !isAfterLoad ? loadingTemplate(() { },) : SingleChildScrollView( controller: scrollController, reverse: isReverse, padding: EdgeInsets.fromLTRB(10, 10, 10, 7), 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 ? Container(margin: EdgeInsets.only(bottom: 5), 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 = username == messageData[i]['from']['user'] && messageData[i]['senderType'] == 'INFORMANT'; var isImage = messageData[i]['imageUrl'] != null && messageData[i]['imageUrl'] != '' && messageData[i]['readStatus'] != 'DELETED'; var isTranslate = U.autoTranslate() && !isMe && messageData[i]['msg'] != messageData[i]['translate']; var widget = Row( mainAxisSize: MainAxisSize.min, children: [ messageData[i]['readStatus'] == 'FAILED' ? Padding( padding: const EdgeInsets.only(right: 15), child: PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem(value: 1, height: 40, child: Text("delete".tr(), style: TextStyle(fontSize: 14))), PopupMenuItem(value: 2, height: 40, child: Text("resend".tr(), style: TextStyle(fontSize: 14))), ], offset: Offset(MediaQuery.of(context).size.width, 0), onSelected: (value) { if (value == 1) { messageData.removeWhere((item) => item['uniqueId'] == messageData[i]['uniqueId']); } else if (value == 2) { var text = messageData[i]['msg']; var images = messageData[i]['imageUrl']; messageData.removeWhere((item) => item['uniqueId'] == messageData[i]['uniqueId']); var uuid = Uuid().v1().replaceAll('-', ''); var data = { "uniqueId": uuid, "userId": username, "recipientId": "#forum", "senderType": "INFORMANT", "text": text, "chatId": idChat, "images": images }; sendMessage(data); } }, child: Icon(Icons.refresh, color: Colors.red), ), ) : Container(), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ !isMe ? Padding( padding: const EdgeInsets.only(bottom: 6.0,), child: Text( messageData[i]['from']['name'], style: TextStyle(color: Color(U.getColor(messageData[i]['from']['user'])), fontSize: 12.0), textAlign: TextAlign.start, ), ) : SizedBox(), isImage ? Padding( padding: EdgeInsets.only(bottom: 2), child: GestureDetector( child: Container( margin: const EdgeInsets.only(bottom: 5), child: Builder(builder: (context) { if (kIsWeb) { return Image.network(messageData[i]['imageUrl']); } else if (File(imagePath! + getImageName(messageData[i]['imageUrl'])).existsSync()) { return Image.file(File(imagePath! + getImageName(messageData[i]['imageUrl'])), fit: BoxFit.cover, width: MediaQuery.of(context).size.width - 150, height: MediaQuery.of(context).size.height / 4); } else { return NoImageFound(isBase64(messageData[i]['imageUrl']) || messageData[i]['loadingImage'] ? false : true); } }), ), onTap: () => kIsWeb ? navigateTo(context, PhotoPreview('forum'.tr(), messageData[i]['imageUrl'], true)) : File(imagePath! + getImageName(messageData[i]['imageUrl'])).existsSync() ? navigateTo(context, PhotoPreview('forum'.tr(), File(imagePath! + getImageName(messageData[i]['imageUrl'])), false)) : null, ), ) : Container(width: 1), messageData[i]['readStatus'] == 'DELETED' ? Text('deletedMessage'.tr(), style: TextStyle(fontSize: 14, color: Colors.white70, fontStyle: FontStyle.italic)) : messageData[i]['msg'] != null && messageData[i]['msg'] != '' ? 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); }); }, textAlign: TextAlign.start, ) : Container(width: 1), 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(), ], ), ) ], ); var timeBubble = RichText( text: TextSpan(children: [ TextSpan(text: DateFormat('HH:mm').format(DateTime.parse(messageData[i]['datetime'])), style: TextStyle(fontSize: 12, color: Colors.black38)), WidgetSpan( child: Padding( padding: const EdgeInsets.only(left: 3), child: Icon(messageData[i]['read'] == null ? Icons.done : Icons.done_all, color: messageData[i]['read'] != null && messageData[i]['read'] ? Colors.green : Colors.black38, size: 15) ) ), ]), ); var expander = Expanded( flex: 8, child: Column( children: [ bubble_chat(widget, 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( margin: EdgeInsets.all(20), child: Text('noInternetDesc'.tr(), style: TextStyle(color: textColor.withValues(alpha: 0.65), fontStyle: FontStyle.italic, height: 1.5, fontSize: 16.0), textAlign: TextAlign.center), ), openChat ? divider() : Container(), openChat ? SafeArea( child: Container( alignment: Alignment.bottomCenter, width: U.bodyWidth(context), color: Colors.white, padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ Expanded( child: SizedBox( width: double.infinity, child: TextField( controller: controllerPesan, maxLength: 256, style: const TextStyle(fontSize: 14, color: Colors.black), keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, decoration: InputDecoration( counterText: '', hintText: 'writeMessage'.tr()+'..', hintStyle: TextStyle(color: textColor.withValues(alpha: 0.5), fontSize: 14), filled: true, fillColor: backgroundColor, hoverColor: Colors.black.withValues(alpha: 0.1), contentPadding: EdgeInsets.symmetric(horizontal: kIsWeb?15:13, vertical: kIsWeb?20:13), suffixIcon: GestureDetector( child: Padding(padding: EdgeInsets.only(left: 13, right: 13), child: U.iconsax('paperclip-2', color: textColor, size: 24)), onTap: ()async{ var uuid = Uuid().v1().replaceAll('-', ''); var data = { "uniqueId": uuid, "userId": username, "recipientId": "#forum", "senderType": "INFORMANT", "text": controllerPesan.text.trim(), "chatId": idChat, "images": null }; if (kIsWeb) { await getImage(ImageSource.gallery).then((value) { if (_image != null) { navigateTo(context, PhotoChat(data, ImageSource.gallery, false, _image!)).then((value) { if (value != null) { sendMessage(value); } }); } }); } else{ showModalBottomSheet( context: context, backgroundColor: Colors.white, builder: (BuildContext context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(top: 15, bottom: 15), child: Center( child: Text('selectPicture'.tr()), ), ), Padding( padding: const EdgeInsets.only(left: 15, right: 15), child: Divider(color: Colors.grey, height: 1.0), ), ListTile( leading: Icon(Icons.image), title: Text('fromGallery'.tr()), onTap: () { Navigator.of(context).pop(); navigateTo(context, PhotoChat(data, ImageSource.gallery, false, null)).then((value) { if (value != null) { sendMessage(value); } }); }, ), ListTile( leading: Icon(Icons.camera), title: Text('fromCamera'.tr()), onTap: () { Navigator.of(context).pop(); navigateTo(context, PhotoChat(data, ImageSource.camera, false, null)).then((value) { if (value != null) { sendMessage(value); } }); }, ) ], ), ); } ); } }, ), border: InputBorder.none, enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(borderRadius), borderSide: BorderSide(color: textColor)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(borderRadius), borderSide: BorderSide(color: primaryColor)), isDense: true ), onChanged: (val) { setState(() { if (val.trim() != '') { final tp = TextPainter(text: TextSpan(text: val), textDirection: ui.TextDirection.ltr); tp.layout(maxWidth: U.bodyWidth(context) - 100); final line = tp.computeLineMetrics().length; if (line == 1) { borderRadius = 50.0; } else { borderRadius = 25.0; } isSend = true; } else { isSend = false; } }); }, ), ), ), SizedBox(width: 12), GestureDetector( child: Container( padding: EdgeInsets.all(12), child: Image.asset('assets/image/icon/Send.png', width: 25, height: 25), decoration: BoxDecoration( color: primaryColor, borderRadius: BorderRadius.all(Radius.circular(50)) ), ), onTap: (){ if(controllerPesan.text.isNotEmpty){ var uuid = Uuid().v1().replaceAll('-', ''); var data = { "uniqueId": uuid, "userId": username, "recipientId": "#forum", "senderType": "INFORMANT", "text": controllerPesan.text.trim(), "chatId": idChat, "images": null }; sendMessage(data); } }, ) ], ), ), ):U.getInternetStatus()?Center(child: Padding( padding: EdgeInsets.symmetric(vertical: 20), child: bubble_chat(Text('chatClosed'.tr(), textAlign: TextAlign.center, style: TextStyle(fontSize: 12)), null, false, false), )):Container() ], ), ); } Future getImage(media) async { try { var pickedFile = await ImagePicker().pickImage(source: media); if (pickedFile != null) { var image = img.decodeImage(await pickedFile.readAsBytes()); var imgPercent = (1000 / (image!.width / 100)).toDouble(); if (image.width > 1000) { image = img.copyResize(image, width: ((image.width / 100) * imgPercent).toInt(), height: ((image.height / 100) * imgPercent).toInt()); } var compressed = img.encodeJpg(image, quality: 60); setState(() { _image = compressed as Uint8List?; }); } } catch (e) { print(e.toString()); } } 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)); } } 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: 75), child: Container( decoration: BoxDecoration( color: color, ), child: Padding( padding: EdgeInsets.all(8), child: isDate ? child : Stack( children: [ Padding( padding: EdgeInsets.only( bottom: 20, right: isNip && isMe ? 6 : 0, left: isNip && !isMe ? 6 : 0, ), child: child, ), Positioned(bottom: 0.0, right: isNip && isMe ? 6 : 0, 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); } }