import 'dart:io'; import 'dart:math'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; final ChatsCubit chatsCubit; const ChatScreen({ required this.chatId, required this.chatsCubit, super.key, }); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final scrollController = ScrollController(); final TextEditingController textEditingController = TextEditingController(); final ChatRoomCubit chatRoomCubit = sl(); final List<({int id, int index})> unreadMessagesIds = []; final List<({int id, int index})> unreadMessagesIdsCopy = []; @override void initState() { super.initState(); scrollController.addListener(_onScroll); chatRoomCubit.initChat(widget.chatId); } _onScroll() { if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) { chatRoomCubit.loadNextPage(widget.chatId); } } @override void dispose() { scrollController.dispose(); super.dispose(); } Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => chatRoomCubit, child: BlocBuilder( bloc: chatRoomCubit, builder: (context, state) { if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { return Scaffold( appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)), ); } else { return Scaffold( body: const Loader(), ); } }, ), ); } _buildChat(BuildContext context, ChatRoomStateLoaded state) { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( titleSpacing: 0, backgroundColor: appTheme.gray100, centerTitle: false, title: Row( children: [ if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl != null) CircleAvatar( radius: 20, foregroundImage: NetworkImage(state.chatRoom.users['to_chat_user']!.profilePhotoUrl!), ), if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl == null) Container( width: 40, height: 40, alignment: Alignment.center, decoration: BoxDecoration( color: Color(0xFFEBF4FF), shape: BoxShape.circle, ), child: Text( state.chatRoom.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoom.users['to_chat_user']!.name.split(' ')[1][0], style: TextStyle( color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20, ), ), ), SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.chatRoom.users['to_chat_user']!.name, style: CustomTextStyles.titleSmallBold.copyWith( color: PrimaryColors().black, ), ), AnimatedCrossFade( firstChild: Text( 'Печатает...', style: CustomTextStyles.bodySmallMulishPrimary, ), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, secondChild: const SizedBox.shrink(), crossFadeState: state.isTyping ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: Duration(milliseconds: 150), ), ], ), ], ), ), body: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Container( color: appTheme.whiteA700, width: double.maxFinite, child: CustomScrollView( physics: BouncingScrollPhysics(), controller: scrollController, reverse: true, slivers: [ SliverPadding( padding: EdgeInsets.symmetric(vertical: 16), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index < state.chatMessages.length) { if ((index + 1 != state.chatMessages.length && state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) || index + 1 == state.chatMessages.length) { final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; return Column( children: [ _messageDate(state.chatMessages[index].updatedAt), VisibilityDetector( key: Key(state.chatMessages[index].id.toString()), onVisibilityChanged: (VisibilityInfo info) { if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index)); EasyDebounce.debounce( 'mark_as_read', Duration(milliseconds: 500), () async { final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy); if (!result) return; widget.chatsCubit.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length); unreadMessagesIdsCopy.clear(); }, ); } }, child: ChatBubbleWidget( isRead: state.chatMessages[index].isRead, sendTime: state.chatMessages[index].updatedAt, text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], ), ), ], ); } final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; return VisibilityDetector( key: Key(state.chatMessages[index].id.toString()), onVisibilityChanged: (VisibilityInfo info) { if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIds.add((id: state.chatMessages[index].id, index: index)); EasyDebounce.debounce( 'mark_as_read', Duration(milliseconds: 500), () async { final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds); if (!result) return; widget.chatsCubit.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length); unreadMessagesIds.clear(); }, ); } }, child: ChatBubbleWidget( isRead: state.chatMessages[index].isRead, sendTime: state.chatMessages[index].updatedAt, text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], ), ); } else { return Padding( padding: const EdgeInsets.all(8), child: const Loader(), ); } }, childCount: state.chatMessages.length + (state.isLoading ? 1 : 0), ), ), ), ], ), ), ), Container( padding: EdgeInsets.symmetric(vertical: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox(width: 8), InkWell( onTap: () async { FilePickerResult? result = await FilePicker.platform.pickFiles( allowMultiple: true, type: FileType.custom, allowedExtensions: [ 'pdf', 'doc', 'docx', 'json', 'ppt', 'pptx', 'rtf', 'txt', 'xls', 'xlsx', 'csv', 'jpeg', 'jpg', 'png', 'gif', 'svg', 'xml', 'webp', ], ); if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); await chatRoomCubit.sendMessage( chatId: widget.chatId, files: files, ); } else { // User canceled the picker } }, customBorder: CircleBorder(), child: Ink( height: 40, width: 40, child: Transform.rotate( angle: pi / 4, child: Icon( Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99), ), ), ), ), SizedBox(width: 8), Expanded( child: SizedBox( height: 40, child: TextFormField( expands: true, controller: textEditingController, maxLines: null, minLines: null, keyboardType: TextInputType.multiline, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), decoration: InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), hintText: 'Сообщение', hintStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2), ), filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: BorderSide( color: Color(0xFFD1D1D6), width: 1, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: BorderSide( color: Color(0xFFD1D1D6), width: 1, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: BorderSide( color: Color(0xFFD1D1D6), width: 1, ), ), ), ), ), ), SizedBox(width: 8), InkWell( customBorder: CircleBorder(), onTap: () { if (textEditingController.text.isNotEmpty) { chatRoomCubit.sendMessage( chatId: widget.chatId, data: textEditingController.text.trim(), ); setState(() { textEditingController.clear(); }); } }, child: Ink( decoration: BoxDecoration( color: appTheme.blueA400, shape: BoxShape.circle, ), height: 40, width: 40, child: Icon( Icons.arrow_upward_rounded, size: 24, color: Colors.white, ), ), ), SizedBox(width: 8), ], ), ), ], ), ), ); } _messageDate(DateTime date) { return Center( child: Padding( padding: const EdgeInsets.only(top: 32), child: Text( DateFormat.yMMMMd('RU').format( date, ), style: const TextStyle( height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500, ), ), ), ); } }