import 'dart:io'; import 'dart:math'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.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/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_user_model.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/plan_order_message_widget.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/video_call_chat_message_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_user_online_status_line.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; import 'package:eatvio_app/features/chat/utils/chat_counterpart_utils.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String? uuid; final String? orderId; final bool? sendInitialMessage; const ChatScreen({this.uuid, this.orderId, super.key, required this.sendInitialMessage}); @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 = []; final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates(); late AppLifecycleListener _listener; List pickedFiles = []; /// Conversation uuid после успешного init (в т.ч. после resolve по orderId). String? _effectiveUuid; String? get conversationUuid => _effectiveUuid ?? widget.uuid; void _onHide() { chatRoomCubit.cancelTimer(); } void _onResume() { chatRoomCubit.updateChosenPlanState(); chatRoomCubit.setTimer(); } Future _initChatRoom() async { final uuid = await chatRoomCubit.initChat( conversationUuid, orderId: _effectiveUuid == null ? widget.orderId : null, sendInitialMessage: widget.sendInitialMessage ?? false, ); if (uuid != null && mounted) { setState(() => _effectiveUuid = uuid); } } @override void didUpdateWidget(ChatScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.uuid != widget.uuid || oldWidget.orderId != widget.orderId) { _effectiveUuid = widget.uuid; _initChatRoom(); } } @override void initState() { super.initState(); _effectiveUuid = widget.uuid; scrollController.addListener(_onScroll); _initChatRoom(); chatRoomCubit.setTimer(); _listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume); } void _onScroll() { final uuid = conversationUuid; if (uuid == null) return; if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) { chatRoomCubit.loadNextPage(uuid); } } @override void dispose() { scrollController.dispose(); _listener.dispose(); chatRoomCubit.close(); super.dispose(); } @override 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( leading: BackButton( onPressed: () { if (context.canPop()) { GoRouter.of(context).pop(); return; } GoRouter.of(context).go(RouteConstants.chatSelectScreen); }, ), ), body: DataErrorWidget(onReload: _initChatRoom), ); } else { return Scaffold( body: const Loader(), appBar: AppBar( leading: BackButton( onPressed: () { if (context.canPop()) { GoRouter.of(context).pop(); return; } GoRouter.of(context).go(RouteConstants.chatSelectScreen); }, ), ), ); } }, ), ); } Scaffold _buildChat(BuildContext context, ChatRoomStateLoaded state) { final chatCounterpart = resolveChatCounterpart(users: state.chatRoom.users, currentUserId: sl().userId); final chosenPlan = state.chatRoom.chosenPlan; final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id; final showUserDetailsButton = [ ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment, ChosenPlanStateEnum.stateInCoordination, ].contains(chosenPlan?.chosenPlanState); PacketOptions? chosenOption; if (chosenPlan?.optionType == 'options1') { chosenOption = chosenPlan?.plan.packet1Options; } else if (chosenPlan?.optionType == 'options2') { chosenOption = chosenPlan?.plan.packet2Options; } else if (chosenPlan?.optionType == 'options3') { chosenOption = chosenPlan?.plan.packet3Options; } final showAboutOrderButton = state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator; BannerType? bannerType; if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) { bannerType = null; } else if (amIConsultant && !state.hasCard && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) { bannerType = BannerType.card; } else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) { bannerType = BannerType.date; } else if (!amIConsultant && (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) { bannerType = BannerType.payment; } else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) { bannerType = BannerType.collectCharge; } else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) { bannerType = BannerType.finishPlan; } else if (chosenOption?.hasPersonalDiet == true && amIConsultant && chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) { bannerType = BannerType.setPersonalDiet; } return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( titleSpacing: 0, leading: BackButton( onPressed: () { if (context.canPop()) { GoRouter.of(context).pop(); return; } GoRouter.of(context).go(RouteConstants.chatSelectScreen); }, ), backgroundColor: appTheme.gray100, centerTitle: false, title: GestureDetector( onTap: () { GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug); }, child: Row( children: [ if (state.chatRoom.users.receiver.profilePhotoUrl != null) CircleAvatar( radius: 20, foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), if (state.chatRoom.users.receiver.profilePhotoUrl == null) Container( width: 40, height: 40, alignment: Alignment.center, decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle), child: Text( state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0], style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.chatRoom.users.receiver.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black), ), AnimatedCrossFade( firstChild: Text('Печатает...', maxLines: 1, overflow: TextOverflow.ellipsis, style: CustomTextStyles.bodySmallMulishPrimary), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, secondChild: ChatUserOnlineStatusLine( isOnline: chatCounterpart.isOnline, lastSeenText: chatCounterpart.lastSeenText, style: CustomTextStyles.bodySmallMulishPrimary, showOnlineDot: false, ), crossFadeState: state.isTyping ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 150), ), ], ), ), ], ), ), actions: [ Padding( padding: const EdgeInsetsDirectional.only(end: 8), child: _ChatHeaderVideoCallButton(onPressed: () => _openVideoCall(context, state, chatCounterpart)), ), ], ), body: SafeArea( child: Stack( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Container( color: appTheme.whiteA700, width: double.maxFinite, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), controller: scrollController, reverse: true, child: Column( children: [ const SizedBox(height: 16), ListView.builder( physics: const NeverScrollableScrollPhysics(), reverse: true, shrinkWrap: true, itemBuilder: (context, index) { if (index == state.chatMessages.length) { return const Padding(padding: EdgeInsets.all(8), child: Loader()); } 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.receiver.id != state.chatMessages[index].authorId; final message = state.chatMessages[index]; final isPlanOrderMessage = message.type == ChatMessageType.planOrder; PacketOptions? messageOption; if (isPlanOrderMessage && chosenPlan != null) { if (chosenPlan.optionType == 'options1') { messageOption = chosenPlan.plan.packet1Options; } else if (chosenPlan.optionType == 'options2') { messageOption = chosenPlan.plan.packet2Options; } else if (chosenPlan.optionType == 'options3') { messageOption = chosenPlan.plan.packet3Options; } } return Column( children: [ _messageDate(message.updatedAt), if (isPlanOrderMessage && chosenPlan != null && messageOption != null) PlanOrderMessageWidget( planName: chosenPlan.plan.name, planSlug: chosenPlan.plan.slug, price: messageOption.price, sendTime: message.updatedAt, isCurrentUser: isCurrentUserAuthor, ), VisibilityDetector( key: Key(message.id.toString()), onVisibilityChanged: (VisibilityInfo info) { if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIdsCopy.add((id: message.id, index: index)); EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async { final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy); if (!result) return; if (!context.mounted) return; context.read().reduceUnreadMessagesCount( chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length, ); unreadMessagesIdsCopy.clear(); }); } }, child: _buildMessageBubble( context: context, state: state, chatCounterpart: chatCounterpart, isCurrentUserAuthor: isCurrentUserAuthor, message: message, amIConsultant: amIConsultant, ), ), ], ); } final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; final message = state.chatMessages[index]; final isPlanOrderMessage = message.type == ChatMessageType.planOrder; PacketOptions? messageOption; if (isPlanOrderMessage && chosenPlan != null) { if (chosenPlan.optionType == 'options1') { messageOption = chosenPlan.plan.packet1Options; } else if (chosenPlan.optionType == 'options2') { messageOption = chosenPlan.plan.packet2Options; } else if (chosenPlan.optionType == 'options3') { messageOption = chosenPlan.plan.packet3Options; } } return Column( children: [ if (isPlanOrderMessage && chosenPlan != null && messageOption != null) PlanOrderMessageWidget( planName: chosenPlan.plan.name, planSlug: chosenPlan.plan.slug, price: messageOption.price, sendTime: message.updatedAt, isCurrentUser: isCurrentUserAuthor, ), VisibilityDetector( key: Key(message.id.toString()), onVisibilityChanged: (VisibilityInfo info) { if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIds.add((id: message.id, index: index)); EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async { final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds); if (!result) return; if (!context.mounted) return; context.read().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length); unreadMessagesIds.clear(); }); } }, child: _buildMessageBubble( context: context, state: state, chatCounterpart: chatCounterpart, isCurrentUserAuthor: isCurrentUserAuthor, message: message, amIConsultant: amIConsultant, ), ), ], ); }, itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0), ), const SizedBox(height: 16), if (showUserDetailsButton) ...[ // OrderCard( // plan: chosenPlan.plan, // isCustomerView: !amIConsultant, // optionType: chosenPlan.optionType, // onCancel: () async { // await context.read().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id); // }, // ), if (amIConsultant) Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomOutlinedButton( text: S.current.user_info, height: 52, buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle( side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), onPressed: () async { GoRouter.of(context).push(UserDetailsScreen.path, extra: state.chatRoom.users.receiver.slug); }, ), ), ], if (bannerType != null) Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: NotificationBanner( bannerType: bannerType, chosenPlanId: chosenPlan?.id, planName: chosenPlan?.plan.name, planPrice: chosenOption?.price.toDouble(), hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false, ), ), ], ), ), ), ), Container( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ for (int i = 0; i < pickedFiles.length; i++) ...[ Stack( clipBehavior: Clip.none, children: [ _isImageFile(pickedFiles[i].path) ? GestureDetector( onTap: () { final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList(); final imageIndex = _imageIndexAt(pickedFiles, i); context.pushTransparentRoute( PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex), ); }, child: Hero( tag: pickedFiles[i].path, transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)), ), ), ) : _buildFilePreview(pickedFiles[i]), Positioned( top: -8, right: -8, child: InkWell( customBorder: const CircleBorder(), onTap: () { setState(() { pickedFiles.remove(pickedFiles[i]); }); }, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle), child: const Icon(size: 20, Icons.close, color: Colors.black), ), ), ), ], ), const SizedBox(width: 8), ], ], ), ), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ const SizedBox(width: 8), InkWell( onTap: () => showImagePicker(state.chatRoom.users.receiver.slug), customBorder: const CircleBorder(), child: Ink( height: 40, width: 40, child: Transform.rotate( angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)), ), ), ), const SizedBox(width: 8), Expanded( child: TextFormField( textCapitalization: TextCapitalization.sentences, controller: textEditingController, maxLines: 3, minLines: 1, keyboardType: TextInputType.multiline, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), hintText: 'Сообщение', hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), ), ), ), ), const SizedBox(width: 8), InkWell( customBorder: const CircleBorder(), onTap: () async { // Проверяем, заблокирован ли пользователь final blockedUsersCubit = sl(); await blockedUsersCubit.getBlockedUsers(); final blockedState = blockedUsersCubit.state; if (blockedState is BlockedUsersStateLoaded) { final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug); if (isBlocked) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Вы не можете отправить сообщение заблокированному пользователю'), backgroundColor: Colors.red, ), ); } return; } } if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) { chatRoomCubit.sendMessage( data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(), files: pickedFiles.isEmpty ? null : pickedFiles, userSlug: state.chatRoom.users.receiver.slug, ); setState(() { textEditingController.clear(); pickedFiles.clear(); }); } }, child: Ink( decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), height: 40, width: 40, child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white), ), ), const SizedBox(width: 8), ], ), ], ), ), ], ), if (showAboutOrderButton) Positioned( top: 8, right: 15, left: 16, child: AboutOrderButton( onTap: () async { if (amIConsultant) { GoRouter.of( context, ).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id, 'customerSlug': state.chatRoom.users.receiver.slug}); return; } GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id}); }, ), ), ], ), ), ); } Widget _buildFilePreview(File file) { final fileName = file.path.split('/').last; return ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( height: 104, width: 104, color: const Color(0xFFE0E0E0), // Светло-серый фон child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.attach_file, size: 32, color: Colors.black54), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)), ), ], ), ), ); } bool _isImageFile(String path) { final lower = path.toLowerCase(); return lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.gif') || lower.endsWith('.bmp') || lower.endsWith('.webp'); } int _imageIndexAt(List allFiles, int targetIndex) { int count = 0; for (int i = 0; i < allFiles.length; i++) { if (_isImageFile(allFiles[i].path)) { if (i == targetIndex) return count; count++; } } return 0; } void showImagePicker(String slug) { showCupertinoModalPopup( context: context, builder: (context) => CupertinoActionSheet( actions: [ CupertinoActionSheetAction( onPressed: () async { Navigator.of(context).pop(); final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); setState(() { if (image != null) { pickedFiles.add(File(image.path)); } }); // if (image != null) { // await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); // } }, child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { Navigator.of(context).pop(); final picker = ImagePicker(); List images = await picker.pickMultiImage(); if (images != []) { setState(() { pickedFiles.addAll(images.map((image) => File(image.path)).toList()); }); // List files = images.map((image) => File(image.path)).toList(); // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]), ), CupertinoActionSheetAction( onPressed: () async { Navigator.of(context).pop(); 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(); setState(() { pickedFiles.addAll(result.paths.map((path) => File(path!)).toList()); }); // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]), ), ], cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')), ), ); } Center _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), ), ), ); } void _openVideoCall(BuildContext context, ChatRoomStateLoaded state, ChatUserModel chatCounterpart) { GoRouter.of(context).pushNamed( VideoCallScreen.name, extra: { 'conversationUuid': conversationUuid, 'counterpartName': chatCounterpart.name, 'counterpartAvatarUrl': chatCounterpart.profilePhotoUrl, 'counterpartUserId': chatCounterpart.id, 'counterpartIsOnline': chatCounterpart.isOnline, 'counterpartLastSeenText': chatCounterpart.lastSeenText, 'localParticipantName': state.chatRoom.users.sender.name, }, ); } Widget _buildMessageBubble({ required BuildContext context, required ChatRoomStateLoaded state, required ChatUserModel chatCounterpart, required bool isCurrentUserAuthor, required ChatMessageModel message, required bool? amIConsultant, }) { if (message.type == ChatMessageType.videoCallStarted && !isCurrentUserAuthor) { return VideoCallChatMessageWidget( text: message.data.trim(), sendTime: message.updatedAt, onVideoCallPressed: () => _openVideoCall(context, state, chatCounterpart), ); } return ChatBubbleWidget( isRead: message.isRead, sendTime: message.updatedAt, text: message.data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: message.mediaData ?? [], messageState: message.messageState, messageUuid: message.uuid ?? '', userSlug: state.chatRoom.users.receiver.slug, messageType: message.type, isConsultant: amIConsultant, urlsMediaInfo: message.urlsMediaInfo, ); } } class _ChatHeaderVideoCallButton extends StatelessWidget { const _ChatHeaderVideoCallButton({required this.onPressed}); final VoidCallback onPressed; @override Widget build(BuildContext context) { final theme = Theme.of(context); final tooltipMessage = S.maybeOf(context)?.video_call; final button = Material( color: theme.colorScheme.onPrimary, borderRadius: BorderRadius.circular(20), elevation: 0, child: InkWell( onTap: onPressed, customBorder: const CircleBorder(), child: SizedBox( width: 40, height: 40, child: Center(child: Icon(Icons.videocam_rounded, color: appTheme.blueA400, size: 18)), ), ), ); if (tooltipMessage == null || tooltipMessage.isEmpty) { return button; } return Tooltip(message: tooltipMessage, child: button); } }