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