From 48f433288f66fb8616cd908ce01a86dae3c71b62 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Mon, 11 Aug 2025 02:52:41 +0700 Subject: [PATCH] feat(chats): added message resend and deleting, feat(menu): added build number --- assets/icons/resend.svg | 3 + .../comments_actions_bottom_sheet.dart | 45 +----- .../widgets/bottom_sheet_action_widget.dart | 33 +++++ .../chat/models/chat_message_model.dart | 4 + .../bloc/chat_room_cubit/chat_room_cubit.dart | 137 ++++++++++++++---- .../chat/presentation/pages/chat_screen.dart | 118 +++++---------- .../widgets/chat_bubble_widget.dart | 89 ++++++------ .../widgets/message_action_bottom_sheet.dart | 48 ++++++ .../menu/presentation/pages/menu_screen.dart | 37 +++++ lib/generated/intl/messages_en.dart | 1 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 4 +- pubspec.yaml | 1 + 13 files changed, 333 insertions(+), 197 deletions(-) create mode 100644 assets/icons/resend.svg create mode 100644 lib/core/widgets/bottom_sheet_action_widget.dart create mode 100644 lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart diff --git a/assets/icons/resend.svg b/assets/icons/resend.svg new file mode 100644 index 00000000..245a03be --- /dev/null +++ b/assets/icons/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index 5506215d..3c1c3859 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -1,11 +1,10 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; void showCommentsActionsBottomSheet( @@ -123,7 +122,7 @@ class _CommentsActionsBottomSheetState extends State ), const BottomSheetHandlerBar(), if (widget.isMyComment) - _ActionWidget( + BottomSheetActionWidget( text: s.edit, iconPath: 'assets/icons/edit_paint.svg', needToPop: false, @@ -132,13 +131,13 @@ class _CommentsActionsBottomSheetState extends State }), ) else if (widget.reply != null) - _ActionWidget( + BottomSheetActionWidget( text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!, ), if (widget.reply != null) const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy, @@ -146,21 +145,21 @@ class _CommentsActionsBottomSheetState extends State if (widget.isMyComment) ...[ if (widget.reply != null) ...[ const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!, ), ], const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete, ), ] else ...[ const CustomDivider(), - _ActionWidget( + BottomSheetActionWidget( text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain, @@ -172,33 +171,3 @@ class _CommentsActionsBottomSheetState extends State } } -class _ActionWidget extends StatelessWidget { - const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true}); - final String text; - final String iconPath; - final void Function() onTap; - final bool needToPop; - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (needToPop) Navigator.pop(context); - onTap(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - children: [ - Expanded( - child: Text( - text, - style: TextStyles.textLMedium.copyWith(color: appTheme.black), - ), - ), - SvgPicture.asset(iconPath) - ], - ), - ), - ); - } -} diff --git a/lib/core/widgets/bottom_sheet_action_widget.dart b/lib/core/widgets/bottom_sheet_action_widget.dart new file mode 100644 index 00000000..004b5567 --- /dev/null +++ b/lib/core/widgets/bottom_sheet_action_widget.dart @@ -0,0 +1,33 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class BottomSheetActionWidget extends StatelessWidget { + const BottomSheetActionWidget({super.key, required this.text, required this.iconPath, required this.onTap, this.needToPop = true, this.color}); + final String text; + final String iconPath; + final void Function() onTap; + final bool needToPop; + final Color? color; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (needToPop) Navigator.pop(context); + onTap(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Expanded( + child: Text(text, style: TextStyles.textLMedium.copyWith(color: appTheme.black)), + ), + SvgPicture.asset(iconPath, colorFilter: color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null), + ], + ), + ), + ); + } +} diff --git a/lib/features/chat/models/chat_message_model.dart b/lib/features/chat/models/chat_message_model.dart index 7224912a..8dd8545a 100644 --- a/lib/features/chat/models/chat_message_model.dart +++ b/lib/features/chat/models/chat_message_model.dart @@ -15,7 +15,11 @@ abstract class ChatMessageModel with _$ChatMessageModel { @JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead, @JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor, @JsonKey(name: 'room_id') required String? roomId, + @Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState, + @Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid, }) = _ChatMessageModel; factory ChatMessageModel.fromJson(Map json) => _$ChatMessageModelFromJson(json); } + +enum MessageState { loading, sent, error } diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index c4d8ed45..5206889b 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; +import 'package:collection/collection.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; @@ -19,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:uuid/uuid.dart'; class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; @@ -31,12 +33,9 @@ class ChatRoomCubit extends Cubit { late int lastPage; bool isLoading = false; Timer? _timer; + var uuid = const Uuid(); - ChatRoomCubit({ - required this.chatRepository, - required this.wsManager, - required this.cardsRepository, - }) : super(const ChatRoomState.loading()); + ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading()); void setTimer() { _timer?.cancel(); @@ -89,10 +88,7 @@ class ChatRoomCubit extends Cubit { } } - Future initChat( - String uuid, { - bool sendInitialMessage = false, - }) async { + Future initChat(String uuid, {bool sendInitialMessage = false}) async { try { final chatRoom = await chatRepository.getChatRoom(uuid); ChosenPlanModel? chosenPlan; @@ -171,11 +167,7 @@ class ChatRoomCubit extends Cubit { List messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; chatRepository.emitNewMessageToChats(message); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); - EasyDebounce.debounce( - 'searchChatRoom', - const Duration(milliseconds: 300), - () => sl().getChatRooms(), - ); + EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl().getChatRooms()); }); typingSubscription = wsManager.clientTypingStream?.listen((event) { @@ -183,22 +175,14 @@ class ChatRoomCubit extends Cubit { EasyDebounce.debounce( 'client-typing', const Duration(seconds: 3), - () => emit( - (state as ChatRoomStateLoaded).copyWith( - isTyping: false, - isLoading: isLoading, - ), - ), + () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)), ); }); } - Future sendMessage({ - String? data, - List? files, - required String userSlug, - }) async { + Future sendMessage({String? data, List? files, required String userSlug}) async { try { + final messageUuid = uuid.v4(); final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null; AudioPlayer().play((AssetSource('audio/send_message.wav'))); emit( @@ -213,16 +197,38 @@ class ChatRoomCubit extends Cubit { isRead: true, isCurrentUserAuthor: true, roomId: '', + messageState: MessageState.loading, + uuid: messageUuid, ), ...(state as ChatRoomStateLoaded).chatMessages, ], ), ); - await chatRepository.sendMessage( - data: data, - files: compressedFiles, - slug: userSlug, - ); + + try { + await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == messageUuid) { + return elem.copyWith(messageState: MessageState.sent); + } + return elem; + }).toList(), + ), + ); + } catch (e) { + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == messageUuid) { + return elem.copyWith(messageState: MessageState.error); + } + return elem; + }).toList(), + ), + ); + } sl().getChatRooms(); return true; } catch (e, st) { @@ -268,7 +274,10 @@ class ChatRoomCubit extends Cubit { try { final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; if (planId == null) return false; - await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description)); + await chatRepository.complainAboutConsultation( + id: planId, + body: PlanComplainModel(description: description), + ); return true; } catch (e) { log(e.toString()); @@ -296,4 +305,70 @@ class ChatRoomCubit extends Cubit { typingSubscription?.cancel(); super.close(); } + + void deleteMessage(String uuid) { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + emit(currentState.copyWith(chatMessages: currentState.chatMessages.where((message) => message.uuid != uuid).toList())); + } + } + + Future resendMessage({required String uuid, required String userSlug}) async { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + final message = currentState.chatMessages.firstWhereOrNull((message) => message.uuid == uuid); + if (message != null) { + try { + AudioPlayer().play((AssetSource('audio/send_message.wav'))); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: [ + ChatMessageModel( + id: 1, + data: message.data, + mediaData: message.mediaData, + authorId: 1, + updatedAt: DateTime.now(), + isRead: true, + isCurrentUserAuthor: true, + roomId: '', + messageState: MessageState.loading, + uuid: uuid, + ), + ...(state as ChatRoomStateLoaded).chatMessages.where((message) => message.uuid != uuid), + ], + ), + ); + + try { + await chatRepository.sendMessage(data: message.data, files: message.mediaData as List?, slug: userSlug); + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == uuid) { + return elem.copyWith(messageState: MessageState.sent); + } + return elem; + }).toList(), + ), + ); + } catch (e) { + emit( + (state as ChatRoomStateLoaded).copyWith( + chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { + if (elem.uuid == uuid) { + return elem.copyWith(messageState: MessageState.error); + } + return elem; + }).toList(), + ), + ); + } + sl().getChatRooms(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + } + } } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index c017f047..1d7235c6 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -93,7 +93,10 @@ class _ChatScreenState extends State { if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { - return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid))); + return Scaffold( + appBar: AppBar(), + body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)), + ); } else { return const Scaffold(body: Loader()); } @@ -168,34 +171,19 @@ class _ChatScreenState extends State { width: 40, height: 40, alignment: Alignment.center, - decoration: const BoxDecoration( - color: Color(0xFFEBF4FF), - shape: BoxShape.circle, - ), + 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, - ), + style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20), ), ), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - state.chatRoom.users.receiver.name, - style: CustomTextStyles.titleSmallBold.copyWith( - color: PrimaryColors().black, - ), - ), + Text(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)), AnimatedCrossFade( - firstChild: Text( - 'Печатает...', - style: CustomTextStyles.bodySmallMulishPrimary, - ), + firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, secondChild: const SizedBox.shrink(), @@ -251,9 +239,9 @@ class _ChatScreenState extends State { if (!result) return; if (!context.mounted) return; context.read().reduceUnreadMessagesCount( - chatId: state.chatRoom.id, - count: unreadMessagesIdsCopy.length, - ); + chatId: state.chatRoom.id, + count: unreadMessagesIdsCopy.length, + ); unreadMessagesIdsCopy.clear(); }); } @@ -264,6 +252,9 @@ class _ChatScreenState extends State { text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], + messageState: state.chatMessages[index].messageState, + messageUuid: state.chatMessages[index].uuid ?? '', + userSlug: state.chatRoom.users.receiver.slug, ), ), ], @@ -290,6 +281,9 @@ class _ChatScreenState extends State { text: state.chatMessages[index].data.trim(), isCurrentUser: isCurrentUserAuthor, mediaData: state.chatMessages[index].mediaData ?? [], + messageState: state.chatMessages[index].messageState, + messageUuid: state.chatMessages[index].uuid ?? '', + userSlug: state.chatRoom.users.receiver.slug, ), ); }, @@ -361,11 +355,7 @@ class _ChatScreenState extends State { final imageIndex = _imageIndexAt(pickedFiles, i); context.pushTransparentRoute( - PhotoViewScreen( - imageUrls: imageFiles.map((e) => e.path).toList(), - isLocal: true, - index: imageIndex, - ), + PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex), ); }, child: Hero( @@ -373,14 +363,7 @@ class _ChatScreenState extends State { transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: SizedBox( - height: 104, - width: 104, - child: Image.file( - pickedFiles[i], - fit: BoxFit.cover, - ), - ), + child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)), ), ), ) @@ -397,15 +380,8 @@ class _ChatScreenState extends State { }, 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, - ), + decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle), + child: const Icon(size: 20, Icons.close, color: Colors.black), ), ), ), @@ -427,7 +403,10 @@ class _ChatScreenState extends State { child: Ink( height: 40, width: 40, - child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), + child: Transform.rotate( + angle: pi / 4, + child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)), + ), ), ), const SizedBox(width: 8), @@ -449,7 +428,9 @@ class _ChatScreenState extends State { 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)), + 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), @@ -525,13 +506,7 @@ class _ChatScreenState extends State { 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), - ), + child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)), ), ], ), @@ -579,13 +554,7 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); // } }, - child: const Row( - children: [ - Icon(Icons.photo_camera), - SizedBox(width: 8), - Text('Фото'), - ], - ), + child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -600,13 +569,7 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, - child: const Row( - children: [ - Icon(Icons.photo_library_rounded), - SizedBox(width: 8), - Text('Галерея'), - ], - ), + child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -643,20 +606,10 @@ class _ChatScreenState extends State { // await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, - child: const Row( - children: [ - Icon(Icons.file_present_rounded), - SizedBox(width: 8), - Text('Файлы'), - ], - ), + 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('Отмена'), - ), + cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')), ), ); } @@ -665,7 +618,10 @@ class _ChatScreenState extends State { 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)), + child: Text( + DateFormat.yMMMMd('RU').format(date), + style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500), + ), ), ); } diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 60a31c44..eb387691 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -5,8 +5,13 @@ import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:url_launcher/url_launcher.dart'; class ChatBubbleWidget extends StatelessWidget { @@ -17,6 +22,9 @@ class ChatBubbleWidget extends StatelessWidget { required this.isCurrentUser, required this.mediaData, required this.isRead, + required this.messageState, + required this.messageUuid, + required this.userSlug }); final DateTime sendTime; @@ -24,7 +32,9 @@ class ChatBubbleWidget extends StatelessWidget { final bool isCurrentUser; final bool isRead; final List mediaData; - + final MessageState messageState; + final String messageUuid; + final String userSlug; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); @@ -38,17 +48,33 @@ class ChatBubbleWidget extends StatelessWidget { mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ if (isCurrentUser) - Padding( - padding: const EdgeInsets.only(right: 4, bottom: 4), - child: Text( - sendTime.format('HH:mm', 'ru'), - style: const TextStyle( - color: Color(0xFF7B8794), - height: 1, - fontSize: 10, - ), + switch (messageState) { + MessageState.loading => Padding( + padding: const EdgeInsets.only(right: 4, bottom: 4), + child: SizedBox(width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: appTheme.blueA400)), ), - ), + + MessageState.sent => Padding( + padding: const EdgeInsets.only(right: 4, bottom: 4), + child: Text(sendTime.format('HH:mm', 'ru'), style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10)), + ), + + MessageState.error => GestureDetector( + onTap: () { + showMessageActionsBottomSheet( + context, + resend: () { + context.read().resendMessage(uuid: messageUuid, userSlug: userSlug); + }, + delete: () { + context.read().deleteMessage(messageUuid); + }, + ); + }, + child: Padding(padding: const EdgeInsets.only(right: 4, bottom: 4), child: SvgPicture.asset('assets/icons/error_icon.svg')), + ), + }, + Container( constraints: BoxConstraints(maxWidth: size.width * 0.7), decoration: BoxDecoration( @@ -87,33 +113,19 @@ class ChatBubbleWidget extends StatelessWidget { throw Exception('Could not launch ${mediaData[index].toString()}'); } }, - leading: const Icon( - Icons.file_copy_outlined, - color: Colors.black, - ), - trailing: const Icon( - Icons.download_outlined, - color: Colors.black, - ), + leading: const Icon(Icons.file_copy_outlined, color: Colors.black), + trailing: const Icon(Icons.download_outlined, color: Colors.black), title: Text( mediaData[index].toString().replaceAll('chat_files/', ''), maxLines: 2, - style: const TextStyle( - color: Colors.black, - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), + style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis), ), ); } return GestureDetector( onTap: () { context.pushTransparentRoute( - PhotoViewScreen( - imageUrls: mediaData, - index: index, - isLocal: mediaData[index] is File ? true : false, - ), + PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false), ); }, child: Hero( @@ -121,11 +133,7 @@ class ChatBubbleWidget extends StatelessWidget { transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(16), - child: mediaData[index] is File - ? Image.file(mediaData[index]) - : CachedNetworkImage( - imageUrl: '${mediaData[index]}', - ), + child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'), ), ), ); @@ -137,11 +145,7 @@ class ChatBubbleWidget extends StatelessWidget { ), ), if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8), - if (text.isNotEmpty) - Text( - text, - style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16), - ), + if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)), ], ), ), @@ -151,12 +155,7 @@ class ChatBubbleWidget extends StatelessWidget { padding: const EdgeInsets.only(left: 4, bottom: 4), child: Text( sendTime.format('HH:mm', 'ru'), - style: const TextStyle( - color: Color(0xFF7B8794), - height: 1, - fontSize: 10, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10, fontWeight: FontWeight.w500), ), ), ], diff --git a/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart b/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart new file mode 100644 index 00000000..1f455764 --- /dev/null +++ b/lib/features/chat/presentation/widgets/message_action_bottom_sheet.dart @@ -0,0 +1,48 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; + +void showMessageActionsBottomSheet(BuildContext context, {required void Function() resend, required void Function() delete}) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700), + child: MessageActionsBottomSheet(resend: resend, delete: delete), + ), + ); +} + +class MessageActionsBottomSheet extends StatefulWidget { + const MessageActionsBottomSheet({super.key, required this.resend, required this.delete}); + final void Function() resend; + final void Function() delete; + + @override + State createState() => _MessageActionsBottomSheet(); +} + +class _MessageActionsBottomSheet extends State { + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 7), + const BottomSheetHandlerBar(), + BottomSheetActionWidget(text: s.resend, iconPath: 'assets/icons/resend.svg', onTap: widget.resend), + const CustomDivider(), + BottomSheetActionWidget(text: s.delete, iconPath: 'assets/icons/trash_can.svg', onTap: widget.delete, color: appTheme.red), + ], + ), + ); + } +} diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 6b8b47fe..90c866a8 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -14,6 +15,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class MenuScreen extends StatelessWidget { final ProfileModel profileModel; @@ -67,6 +69,8 @@ class MenuScreen extends StatelessWidget { // ), _divider(context), _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), + const SizedBox(height: 10), + const AppInfo(), ], ), ), @@ -124,3 +128,36 @@ class MenuScreen extends StatelessWidget { }; } } + +class AppInfo extends StatefulWidget { + const AppInfo({super.key}); + + @override + State createState() => _AppInfoState(); +} + +class _AppInfoState extends State { + String version = ''; + String buildNumber = ''; + @override + void didChangeDependencies() { + PackageInfo.fromPlatform().then((PackageInfo packageInfo) { + setState(() { + version = packageInfo.version; + buildNumber = packageInfo.buildNumber; + }); + }); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsetsGeometry.only(left: 16), + child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)), + ), + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 360b4288..373b48eb 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -634,6 +634,7 @@ class MessageLookup extends MessageLookupByLibrary { "Снять с публикации", ), "reply": MessageLookupByLibrary.simpleMessage("Ответить"), + "resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 7df3cbcc..25d71d68 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3459,6 +3459,16 @@ class S { args: [], ); } + + /// `Отправить повторно` + String get resend { + return Intl.message( + 'Отправить повторно', + name: 'resend', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 452e2d5a..7424fe87 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -410,7 +410,7 @@ "youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?", "itCantBeRecovered": "Его нельзя будет восстановить", "delete": "Удалить", - "cannotDeleteAccount": "Не получилось удалить ваш аккаунт" - + "cannotDeleteAccount": "Не получилось удалить ваш аккаунт", + "resend": "Отправить повторно" } diff --git a/pubspec.yaml b/pubspec.yaml index 3895a06b..f61ae9c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 flutter_dotenv: ^5.1.0 + uuid: ^4.5.1 dev_dependencies: build_runner: ^2.6.0