diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 70ff32b8..75372b09 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
- flutter_version: "3.32.0"
+ flutter_version: "3.32.5"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4df5da7f..a5377cc6 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
- ndkVersion = "27.0.12077973"
+ ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
diff --git a/assets/icons/blocked_users.svg b/assets/icons/blocked_users.svg
new file mode 100644
index 00000000..819b2245
--- /dev/null
+++ b/assets/icons/blocked_users.svg
@@ -0,0 +1,29 @@
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 7c47de93..648ad30f 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 34;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 34;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 34;
+ CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 4c5b3c2d..9284d469 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 32
+ 37
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart
index 746303b4..70107e0a 100644
--- a/lib/core/app_router.dart
+++ b/lib/core/app_router.dart
@@ -25,6 +25,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
+import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -193,6 +194,11 @@ class AppRouter {
),
],
),
+ GoRoute(
+ name: 'blockedUsersScreen',
+ path: '/blocked-users-screen',
+ builder: (context, state) => const BlockedUsersScreen(),
+ ),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
diff --git a/lib/core/image_compressing/image_compress_utility.dart b/lib/core/image_compressing/image_compress_utility.dart
index 6eb66f4e..e3c5fa65 100644
--- a/lib/core/image_compressing/image_compress_utility.dart
+++ b/lib/core/image_compressing/image_compress_utility.dart
@@ -7,11 +7,25 @@ abstract class ImageCompressUtility {
static Future> compressFilesList(List files) async {
final List compressedFilesList = [];
for (int i = 0; i < files.length; i++) {
- compressedFilesList.add(await compressFile(files[i]));
+ if (_isImageFile(files[i].path)) {
+ compressedFilesList.add(await compressFile(files[i]));
+ } else {
+ compressedFilesList.add(files[i]);
+ }
}
return compressedFilesList;
}
+ static 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');
+ }
+
static Future compressFile(File file) async {
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;
diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart
index 0b83e3d7..9036809a 100644
--- a/lib/core/locator_service.dart
+++ b/lib/core/locator_service.dart
@@ -37,6 +37,9 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
+import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
+import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -164,6 +167,7 @@ Future init() async {
sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory(() => BlogPostCubit(sl()));
+ sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl()));
// Repository
sl.registerLazySingleton(() => ChatRepository(chatService: sl()));
@@ -193,6 +197,7 @@ Future init() async {
sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl()));
+ sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl()));
// Services
sl.registerLazySingleton(() => ChatService(sl()));
@@ -224,6 +229,7 @@ Future init() async {
sl.registerLazySingleton(() => MyRecipesService(sl()));
sl.registerLazySingleton(() => FirebaseApi());
sl.registerLazySingleton(() => ResetPasswordService(sl()));
+ sl.registerLazySingleton(() => BlockedUserService(sl()));
// External
sl.registerLazySingleton(
diff --git a/lib/core/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart
index f10ae9ec..bcc82aeb 100644
--- a/lib/core/utils/date_time_utils.dart
+++ b/lib/core/utils/date_time_utils.dart
@@ -33,7 +33,7 @@ extension DateTimeExtension on DateTime {
}
}
-String calculateDifference(DateTime dateTime) {
+String calculateDifference(DateTime dateTime, [String? format]) {
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
if (localDateTime.isToday()) {
@@ -41,6 +41,6 @@ String calculateDifference(DateTime dateTime) {
} else if (localDateTime.isYesterday()) {
return 'Вчера';
} else {
- return localDateTime.format('d MMMM', 'ru');
+ return localDateTime.format(format ?? 'd MMMM', 'ru');
}
}
diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart
index 152f0fb9..73431542 100644
--- a/lib/core/websockets/ws_manager.dart
+++ b/lib/core/websockets/ws_manager.dart
@@ -1,9 +1,12 @@
import 'dart:developer';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
+import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
+import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
+import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
class WsManager {
final WsService wsService;
@@ -71,6 +74,11 @@ class WsManager {
}
});
client?.eventStream.listen((event) {
+ EasyDebounce.debounce(
+ 'searchChatRoom',
+ const Duration(milliseconds: 300),
+ () => sl().getChatRooms(),
+ );
log(event.rootObject.toString());
});
_subscribe();
diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart
index dbe04ec4..1ba0224e 100644
--- a/lib/core/widgets/custom_popup.dart
+++ b/lib/core/widgets/custom_popup.dart
@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -28,6 +29,7 @@ class CustomPopup extends StatelessWidget {
final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe;
final Diet? diet;
+ final String userSlug;
const CustomPopup({
super.key,
@@ -39,6 +41,7 @@ class CustomPopup extends StatelessWidget {
this.feedBlogPostModel,
this.recipe,
this.diet,
+ required this.userSlug
});
@override
@@ -164,6 +167,13 @@ class CustomPopup extends StatelessWidget {
);
}
});
+ addItem(
+ 'assets/icons/complain.svg',
+ 'Заблокировать автора',
+ () {
+ sl().blockUser(userSlug);
+ },
+ );
}
if (entries.isNotEmpty && entries.last is PopupMenuItem) {
diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
index 1dc5bd36..5541daa8 100644
--- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart
+++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
@@ -195,7 +195,8 @@ class _BlogPostScreenState extends State {
path: 'blog_post/${(widget.id)}',
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
- ),
+ userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
+ ),
const SizedBox(width: 16),
],
),
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 369993e0..f36cbbe5 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
@@ -7,11 +7,13 @@ import 'package:audioplayers/audioplayers.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';
+import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/plan_complain_model.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/repositories/chat_repository.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
@@ -167,6 +169,11 @@ 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(),
+ );
});
typingSubscription = wsManager.clientTypingStream?.listen((event) {
@@ -184,7 +191,7 @@ class ChatRoomCubit extends Cubit {
try {
await chatRepository.sendMessage(
data: data,
- files: files != null ? await ImageCompressUtility.compressFilesList(files) : null,
+ files: files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null,
slug: userSlug,
);
AudioPlayer().play((AssetSource('audio/send_message.wav')));
diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart
index e2e222cd..ab1c80c5 100644
--- a/lib/features/chat/presentation/pages/chat_screen.dart
+++ b/lib/features/chat/presentation/pages/chat_screen.dart
@@ -1,6 +1,7 @@
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/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
@@ -8,6 +9,7 @@ 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/core/locator_service.dart';
@@ -47,6 +49,7 @@ class _ChatScreenState extends State {
final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
late AppLifecycleListener _listener;
+ List pickedFiles = [];
void _onHide() {
chatRoomCubit.cancelTimer();
@@ -336,73 +339,155 @@ class _ChatScreenState extends State {
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.end,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
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))),
+ 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(width: 8),
- Expanded(
- child: SizedBox(
- height: 40,
- child: TextFormField(
- expands: true,
- textCapitalization: TextCapitalization.sentences,
- controller: textEditingController,
- maxLines: null,
- minLines: null,
- 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(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: SizedBox(
+ height: 40,
+ child: TextFormField(
+ expands: true,
+ textCapitalization: TextCapitalization.sentences,
+ controller: textEditingController,
+ maxLines: null,
+ minLines: null,
+ 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: () {
+ 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),
+ ],
),
- const SizedBox(width: 8),
- InkWell(
- customBorder: const CircleBorder(),
- onTap: () {
- if (textEditingController.text.isNotEmpty) {
- chatRoomCubit.sendMessage(
- data: textEditingController.text.trim(),
- userSlug: state.chatRoom.users.receiver.slug,
- );
- setState(() {
- textEditingController.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),
],
),
),
@@ -425,6 +510,56 @@ class _ChatScreenState extends State {
);
}
+ 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;
+ }
+
showImagePicker(String slug) {
showCupertinoModalPopup(
context: context,
@@ -435,9 +570,14 @@ class _ChatScreenState extends State {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
- if (image != null) {
- await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
- }
+ 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: [
@@ -453,8 +593,11 @@ class _ChatScreenState extends State {
final picker = ImagePicker();
List images = await picker.pickMultiImage();
if (images != []) {
- List files = images.map((image) => File(image.path)).toList();
- await chatRoomCubit.sendMessage(files: files, userSlug: slug);
+ 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(
@@ -493,8 +636,11 @@ class _ChatScreenState extends State {
],
);
if (result != null) {
- List files = result.paths.map((path) => File(path!)).toList();
- await chatRoomCubit.sendMessage(files: files, userSlug: slug);
+ // 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(
diff --git a/lib/features/chat/presentation/pages/chats_screen.dart b/lib/features/chat/presentation/pages/chats_screen.dart
index 03c3870a..6c4c2398 100644
--- a/lib/features/chat/presentation/pages/chats_screen.dart
+++ b/lib/features/chat/presentation/pages/chats_screen.dart
@@ -67,11 +67,16 @@ class _ChatsScreenState extends State {
),
];
},
- body: switch (state.state) {
- BaseState.error => DataErrorWidget(onReload: () => context.read().getChatRooms()),
- BaseState.success => _buildChatList(context, state),
- _ => const Loader(),
- },
+ body: RefreshIndicator(
+ child: switch (state.state) {
+ BaseState.error => DataErrorWidget(onReload: () => context.read().getChatRooms()),
+ BaseState.success => _buildChatList(context, state),
+ _ => const Loader(),
+ },
+ onRefresh: () async {
+ await context.read().getChatRooms();
+ },
+ ),
),
);
},
diff --git a/lib/features/chat/presentation/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/widgets/chat_entry_widget.dart
index 1338c60b..44cc67f9 100644
--- a/lib/features/chat/presentation/widgets/chat_entry_widget.dart
+++ b/lib/features/chat/presentation/widgets/chat_entry_widget.dart
@@ -20,15 +20,14 @@ class ChatEntryWidget extends StatefulWidget {
class _ChatEntryWidgetState extends State {
@override
Widget build(BuildContext context) {
- return InkWell(
- borderRadius: BorderRadius.circular(16),
+ return GestureDetector(
onTap: () => GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': widget.chatRoom.id.toString(),
},
),
- child: Ink(
+ child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
@@ -61,7 +60,13 @@ class _ChatEntryWidgetState extends State {
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.5,
child: Text(
- widget.chatRoom.lastMessage!.data.toString().trim(),
+ widget.chatRoom.lastMessage!.mediaData != null
+ ? widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
+ ? 'Вы: 📎 Вложение'
+ : '📎 Вложение'
+ : widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
+ ? 'Вы: ${widget.chatRoom.lastMessage!.data.toString().trim()}'
+ : widget.chatRoom.lastMessage!.data.toString().trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: CustomTextStyles.bodyMediumBluegray800,
@@ -77,7 +82,7 @@ class _ChatEntryWidgetState extends State {
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
- widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'),
+ calculateDifference(widget.chatRoom.lastMessage!.updatedAt, 'dd MMM'),
style: theme.textTheme.bodyMedium,
),
),
diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart
index e249c708..54924fcd 100644
--- a/lib/features/chat/repositories/chat_repository.dart
+++ b/lib/features/chat/repositories/chat_repository.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
@@ -51,7 +52,8 @@ class ChatRepository {
data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
);
- } catch (e) {
+ } catch (e, st) {
+ log(e.toString(), stackTrace: st);
rethrow;
}
}
diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart
index 3901841f..fa88e9d8 100644
--- a/lib/features/create_diet/presentation/diet_preview_screen.dart
+++ b/lib/features/create_diet/presentation/diet_preview_screen.dart
@@ -194,6 +194,7 @@ class _DietPreviewScreenState extends State {
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
+ userSlug: widget.diet.author.authorSlug,
),
const SizedBox(width: 16),
],
diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart
index a57e7580..eab3c07e 100644
--- a/lib/features/dev_tools/presentation/dev_tools_screen.dart
+++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart
@@ -51,6 +51,29 @@ class _DevToolsScreenContentState extends State {
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
+ AuthServiceTest(),
+ DiaryServiceTest(),
+ BlockedUserServiceTest(),
+ PlanServiceTest(),
+ AboutRecipeServiceTest(),
+ BlogPostServiceTest(),
+ CalendarServiceTest(),
+ CardsServiceTest(),
+ ConsultantServiceTest(),
+ ConsultationServiceTest(),
+ CreateDietServiceTest(),
+ CreatePlanServiceTest(),
+ GlossaryServiceTest(),
+ MyDietsServiceTest(),
+ MyPlansServiceTest(),
+ MyRecipesServiceTest(),
+ NotificationsServiceTest(),
+ PlansServiceTest(),
+ ProfileServiceTest(),
+ RegistrationServiceTest(),
+ ResetPasswordServiceTest(),
+ SettingsServiceTest(),
+ WsServiceTest(),
];
final controller = TextEditingController();
late final List testsStates;
@@ -132,17 +155,56 @@ class _DevToolsScreenContentState extends State {
setState(() {
testsStates[index] = TestState.loading;
});
- final result = await testCases[index].run(baseUrl: baseUrlForTest);
- if (result) {
- setState(() {
- testsStates[index] = TestState.success;
- });
- showSuccessToast('${testCases[index].name} passed');
- } else {
+ try {
+ final result = await testCases[index].run(baseUrl: baseUrlForTest);
+ if (result.success) {
+ setState(() {
+ testsStates[index] = TestState.success;
+ });
+ showSuccessToast('${testCases[index].name} passed');
+ } else {
+ setState(() {
+ testsStates[index] = TestState.fail;
+ });
+ showErrorToast('${testCases[index].name} failed');
+ // Show AlertDialog with error description
+ if (mounted) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('Test Failed'),
+ content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ }
+ }
+ } catch (e, stack) {
setState(() {
testsStates[index] = TestState.fail;
});
showErrorToast('${testCases[index].name} failed');
+ // Show AlertDialog with error description
+ if (mounted) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('Test Failed'),
+ content: SingleChildScrollView(child: Text(e.toString() + '\n' + stack.toString())),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ }
}
},
buttonStyle: const ButtonStyle(),
diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart
index 1b2f8961..59f92ce3 100644
--- a/lib/features/dev_tools/test_cases.dart
+++ b/lib/features/dev_tools/test_cases.dart
@@ -4,19 +4,48 @@ import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
+import 'package:eatvio_app/core/websockets/ws_service.dart';
+import 'package:eatvio_app/features/auth/models/credentials_model.dart';
+import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
+import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
+import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
+import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
+import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
+import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
+import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
+import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
+import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
+import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
+import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
+import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
+import 'package:eatvio_app/features/plans/services/plan_service.dart';
+import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
+import 'package:eatvio_app/features/auth/services/auth_service.dart';
+import 'package:eatvio_app/features/diary/services/diary_service.dart';
+import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
+import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
+import 'package:eatvio_app/features/registration/services/registration_service.dart';
+import 'package:eatvio_app/features/settings/services/settings_service.dart';
+
+class TestResult {
+ final bool success;
+ final String? error;
+ TestResult(this.success, [this.error]);
+}
sealed class TestCases {
String get name => throw UnimplementedError();
- Future run({required String baseUrl}) async => throw UnimplementedError();
+ Future run({required String baseUrl}) async => throw UnimplementedError();
}
class CreateRecipeTask implements TestCases {
@@ -24,7 +53,7 @@ class CreateRecipeTask implements TestCases {
String get name => 'api/recipes/create';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
@@ -41,10 +70,16 @@ class CreateRecipeTask implements TestCases {
steps: [Step(description: 'desc')],
),
);
- } catch (e) {
- return false;
+ } on DioError catch (e) {
+ final res = e.response;
+ if (res != null && (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300)) {
+ return TestResult(false, 'DioError: statusCode=${res.statusCode}\n${res.data}');
+ }
+ return TestResult(false, e.toString());
+ } catch (e, stack) {
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
- return true;
+ return TestResult(true);
}
}
@@ -53,82 +88,66 @@ class Test850task implements TestCases {
String get name => 'task #850';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
- // get all of needed services
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final profileService = ProfileService(dio, baseUrl: baseUrl);
final dietsService = DietService(dio, baseUrl: baseUrl);
-
- // get all of needed data
final dietsPaginationModel = await feedService.getDiets(page: 1);
final chosenDietsPaginationModel = await profileService.getChosenDiets();
final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull;
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
final tomorrow = today.add(const Duration(days: 1));
-
- // run test case
for (final diet in dietsPaginationModel.data) {
if (!diet.author.authorIsMe) {
if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') {
try {
await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0);
- } catch (e) {
+ } catch (e, stack) {
log('unexpected error7: $e');
- return false;
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
await Future.delayed(const Duration(seconds: 3));
}
try {
await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')});
-
- return false;
+ return TestResult(false, 'Expected error but got success');
} catch (e) {
log('expect ot have error: $e');
}
-
- // test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')});
- } catch (e) {
+ } catch (e, stack) {
log('unexpected error2: $e');
- return false;
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
-
- // test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
- } catch (e) {
+ } catch (e, stack) {
log('unexpected error3: $e');
- return false;
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
-
await Future.delayed(const Duration(seconds: 3));
-
- // test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')});
- } catch (e) {
+ } catch (e, stack) {
log('unexpected error4: $e');
- return false;
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
-
- // test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
- } catch (e) {
+ } catch (e, stack) {
log('unexpected error5: $e');
}
break;
}
}
-
- return true;
- } catch (e) {
+ return TestResult(true);
+ } catch (e, stack) {
log('unexpected error6: $e');
- return false;
+ return TestResult(false, e.toString() + '\n' + stack.toString());
}
}
}
@@ -139,16 +158,16 @@ class GetChatRoomPreviews implements TestCases {
String get name => '/api/chat/index';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
// get all of needed services
// final dio = sl();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// await chatService.getChatRooms();
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -158,16 +177,16 @@ class GetChatRoom implements TestCases {
String get name => '/api/chat/show/{id}';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
// final dio = sl();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '');
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -177,16 +196,16 @@ class SendMessage implements TestCases {
String get name => '/api/chat/{id}/message/send';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
// final dio = sl();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message');
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -196,16 +215,16 @@ class GetChatMessages implements TestCases {
String get name => '/api/chat/show/{id}/messages?page={page}';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
// final dio = sl();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -215,7 +234,7 @@ class GetChatRoomIdByUserSlug implements TestCases {
String get name => '/api/chat/get_by_user/{user}';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -230,9 +249,9 @@ class GetChatRoomIdByUserSlug implements TestCases {
break;
}
}
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -243,14 +262,14 @@ class GetBlogPostList implements TestCases {
String get name => '/api/blogPosts/index';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getBlogPostList(page: 1);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -260,7 +279,7 @@ class GetRecipes implements TestCases {
String get name => '/api/recipes/index';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -269,9 +288,9 @@ class GetRecipes implements TestCases {
final tags = tagsModel.data;
final tagsIds = tags.map((e) => e.id).toList();
await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -281,14 +300,14 @@ class GetDiets implements TestCases {
String get name => '/api/diets/index';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getDiets(page: 1, query: null);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -298,14 +317,14 @@ class GetPlans implements TestCases {
String get name => '/api/plans/index';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getPlans(page: 1, query: null);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -315,16 +334,16 @@ class GetBlogPostComments implements TestCases {
String get name => '/api/blogPosts/{id}/comments/show';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.getBlogPostComments(id: blogPosts.first.id.toString());
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -334,16 +353,16 @@ class SendBlogPostComment implements TestCases {
String get name => '/api/blogPosts/{id}/comment/create';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message');
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -353,16 +372,16 @@ class GetDietsComments implements TestCases {
String get name => '/api/diets/{id}/comments/show';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.getDietsComments(id: diets.first.id.toString());
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -372,16 +391,16 @@ class SendDietsComment implements TestCases {
String get name => '/api/diets/{id}/comment/create';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message');
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -391,16 +410,16 @@ class GetPlansComments implements TestCases {
String get name => '/api/plans/{slug}/comments/show';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.getPlansComments(slug: plans.first.slug);
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -410,16 +429,16 @@ class SendPlansComment implements TestCases {
String get name => '/api/plans/{slug}/comment/create';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message');
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
}
}
}
@@ -429,7 +448,7 @@ class GetRecipesComments implements TestCases {
String get name => '/api/recipes/{id}/comments/show';
@override
- Future run({required String baseUrl}) async {
+ Future run({required String baseUrl}) async {
try {
final dio = sl();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -440,9 +459,426 @@ class GetRecipesComments implements TestCases {
final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
final recipes = recipesModel.recipesList;
await feedService.getRecipesComments(id: recipes.first.id.toString());
- return true;
+ return TestResult(true);
} catch (e) {
- return false;
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// AuthService
+class AuthServiceTest implements TestCases {
+ @override
+ String get name => 'AuthService: getTokenByCredentials';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final authService = AuthService(dio, baseUrl: baseUrl);
+ // Use dummy credentials, expecting failure but no crash
+ await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
+ return TestResult(false, 'Should not succeed');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for dummy credentials');
+ }
+ }
+}
+
+// DiaryService
+class DiaryServiceTest implements TestCases {
+ @override
+ String get name => 'DiaryService: getDiaryIndexByDate';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final diaryService = DiaryService(dio, baseUrl: baseUrl);
+ await diaryService.getDiaryIndexByDate(date: '2023-01-01');
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// BlockedUserService
+class BlockedUserServiceTest implements TestCases {
+ @override
+ String get name => 'BlockedUserService: getBlockedUsers';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final blockedUserService = BlockedUserService(dio, baseUrl: baseUrl);
+ await blockedUserService.getBlockedUsers();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// PlanService
+class PlanServiceTest implements TestCases {
+ @override
+ String get name => 'PlanService: getPlan';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final planService = PlanService(dio, baseUrl: baseUrl);
+ // Use a dummy slug, expecting failure but no crash
+ await planService.getPlan('dummy-slug');
+ return TestResult(false, 'Should not succeed');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for dummy slug');
+ }
+ }
+}
+
+// AboutRecipeService
+class AboutRecipeServiceTest implements TestCases {
+ @override
+ String get name => 'AboutRecipeService: getRecipe';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final aboutRecipeService = AboutRecipeService(dio, baseUrl: baseUrl);
+ await aboutRecipeService.getRecipe('dummy-id');
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for dummy id');
+ }
+ }
+}
+
+// BlogPostService
+class BlogPostServiceTest implements TestCases {
+ @override
+ String get name => 'BlogPostService: createBlogPost';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final blogPostService = BlogPostService(dio, baseUrl: baseUrl);
+ // Use empty FormData, expecting failure but no crash
+ await blogPostService.createBlogPost(formData: FormData());
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for empty FormData');
+ }
+ }
+}
+
+// CalendarService
+class CalendarServiceTest implements TestCases {
+ @override
+ String get name => 'CalendarService: getCalendar';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final calendarService = CalendarService(dio, baseUrl: baseUrl);
+ await calendarService.getCalendar();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// CardsService
+class CardsServiceTest implements TestCases {
+ @override
+ String get name => 'CardsService: getCards';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final cardsService = CardsService(dio, baseUrl: baseUrl);
+ await cardsService.getCards();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// ConsultantService
+class ConsultantServiceTest implements TestCases {
+ @override
+ String get name => 'ConsultantService: getMyRequestStatus';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final consultantService = ConsultantService(dio, baseUrl: baseUrl);
+ await consultantService.getMyRequestStatus(slug: 'dummy-slug');
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for dummy slug');
+ }
+ }
+}
+
+// ConsultationService
+class ConsultationServiceTest implements TestCases {
+ @override
+ String get name => 'ConsultationService: getConsultations';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final consultationService = ConsultationService(dio);
+ await consultationService.getConsultations();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// CreateDietService
+class CreateDietServiceTest implements TestCases {
+ @override
+ String get name => 'CreateDietService: getDietPortionsNutrition';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final createDietService = CreateDietService(dio, baseUrl: baseUrl);
+ await createDietService.getDietPortionsNutrition(diet: 0);
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for diet: 0');
+ }
+ }
+}
+
+// CreatePlanService
+class CreatePlanServiceTest implements TestCases {
+ @override
+ String get name => 'CreatePlanService: createPlan';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final createPlanService = CreatePlanService(dio, baseUrl: baseUrl);
+ await createPlanService.createPlan(formData: FormData());
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for empty FormData');
+ }
+ }
+}
+
+// GlossaryService
+class GlossaryServiceTest implements TestCases {
+ @override
+ String get name => 'GlossaryService: getGlossaryByQuery';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final glossaryService = GlossaryService(dio, baseUrl: baseUrl);
+ await glossaryService.getGlossaryByQuery(query: 'test');
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// MyDietsService
+class MyDietsServiceTest implements TestCases {
+ @override
+ String get name => 'MyDietsService: getDiets';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final myDietsService = MyDietsService(dio, baseUrl: baseUrl);
+ await myDietsService.getDiets(page: 1);
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// MyPlansService
+class MyPlansServiceTest implements TestCases {
+ @override
+ String get name => 'MyPlansService: getPlans';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final myPlansService = MyPlansService(dio, baseUrl: baseUrl);
+ await myPlansService.getPlans(page: 1);
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// MyRecipesService
+class MyRecipesServiceTest implements TestCases {
+ @override
+ String get name => 'MyRecipesService: getMyRecipes';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final myRecipesService = MyRecipesService(dio, baseUrl: baseUrl);
+ await myRecipesService.getMyRecipes();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// NotificationsService
+class NotificationsServiceTest implements TestCases {
+ @override
+ String get name => 'NotificationsService: getNotifications';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final notificationsService = NotificationsService(dio);
+ await notificationsService.getNotifications(page: 1);
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// PlansService
+class PlansServiceTest implements TestCases {
+ @override
+ String get name => 'PlansService: getPlansBySlug';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final plansService = PlansService(dio, baseUrl: baseUrl);
+ await plansService.getPlansBySlug(author: 'dummy-author');
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for dummy author');
+ }
+ }
+}
+
+// ProfileService
+class ProfileServiceTest implements TestCases {
+ @override
+ String get name => 'ProfileService: getMyProfile';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final profileService = ProfileService(dio, baseUrl: baseUrl);
+ await profileService.getMyProfile();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// RegistrationService
+class RegistrationServiceTest implements TestCases {
+ @override
+ String get name => 'RegistrationService: getLocationsByAddress';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final registrationService = RegistrationService(dio, baseUrl: baseUrl);
+ await registrationService.getLocationsByAddress(query: 'test');
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// ResetPasswordService
+class ResetPasswordServiceTest implements TestCases {
+ @override
+ String get name => 'ResetPasswordService: sendEmailCodeForPasswordChange';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final resetPasswordService = ResetPasswordService(dio, baseUrl: baseUrl);
+ await resetPasswordService.sendEmailCodeForPasswordChange(data: {'email': 'test@example.com'});
+ return TestResult(false, 'Expected failure but got success');
+ } catch (e) {
+ return TestResult(true, 'Exception expected for test@example.com');
+ }
+ }
+}
+
+// SettingsService
+class SettingsServiceTest implements TestCases {
+ @override
+ String get name => 'SettingsService: getProfileSettings';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final settingsService = SettingsService(dio, baseUrl: baseUrl);
+ await settingsService.getProfileSettings();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
+ }
+ }
+}
+
+// WsService
+class WsServiceTest implements TestCases {
+ @override
+ String get name => 'WsService: getWebsocket';
+
+ @override
+ Future run({required String baseUrl}) async {
+ try {
+ final dio = sl();
+ final wsService = WsService(dio, baseUrl: baseUrl);
+ await wsService.getWebsocket();
+ return TestResult(true);
+ } catch (e) {
+ return TestResult(false, e.toString());
}
}
}
diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart
index 89fc1659..28d5292a 100644
--- a/lib/features/diets/presentation/pages/about_diet_screen.dart
+++ b/lib/features/diets/presentation/pages/about_diet_screen.dart
@@ -334,6 +334,7 @@ class _AboutDietScreenState extends State {
complainedContentType: ComplainedContent.diet,
id: dietState.diet.id,
diet: dietState.diet,
+ userSlug: dietState.diet.author.authorSlug,
),
const SizedBox(width: 16),
],
diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart
index 91edc060..d8bff7c9 100644
--- a/lib/features/feed/presentation/widgets/blog_post_widget.dart
+++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart
@@ -130,6 +130,7 @@ class _BlogPostWidgetState extends State {
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
id: state.feedBlogPostList[widget.index].id,
feedBlogPostModel: state.feedBlogPostList[widget.index],
+ userSlug: state.feedBlogPostList[widget.index].author.authorSlug,
),
],
),
diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart
index fbd4730b..dcc80666 100644
--- a/lib/features/feed/presentation/widgets/diets_widget.dart
+++ b/lib/features/feed/presentation/widgets/diets_widget.dart
@@ -94,6 +94,7 @@ class _DietsWidgetState extends State {
id: widget.diet.id,
complainedContentType: ComplainedContent.diet,
diet: widget.diet,
+ userSlug: widget.diet.author.authorSlug,
),
),
),
diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart
index 13b3b698..ea130f7e 100644
--- a/lib/features/feed/presentation/widgets/plan_widget.dart
+++ b/lib/features/feed/presentation/widgets/plan_widget.dart
@@ -100,6 +100,7 @@ class _PlanWidgetState extends State {
id: widget.plan.id,
slug: widget.plan.slug,
complainedContentType: ComplainedContent.plan,
+ userSlug: widget.plan.author.authorSlug,
),
),
),
diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart
index f9998f61..2e0ed35b 100644
--- a/lib/features/feed/presentation/widgets/recipes_widget.dart
+++ b/lib/features/feed/presentation/widgets/recipes_widget.dart
@@ -128,6 +128,7 @@ class _RecipesWidgetState extends State {
slug: widget.recipe.slug,
complainedContentType: ComplainedContent.recipe,
recipe: widget.recipe,
+ userSlug: widget.recipe.author.authorSlug,
),
),
),
diff --git a/lib/features/menu/models/blocked_users_model.dart b/lib/features/menu/models/blocked_users_model.dart
new file mode 100644
index 00000000..a7abd7f6
--- /dev/null
+++ b/lib/features/menu/models/blocked_users_model.dart
@@ -0,0 +1,14 @@
+import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'blocked_users_model.g.dart';
+part 'blocked_users_model.freezed.dart';
+
+@freezed
+abstract class BlockedUsersModel with _$BlockedUsersModel {
+ const factory BlockedUsersModel({
+ @JsonKey(name: 'data') required List profilesList,
+ }) = _BlockedUsersModel;
+
+ factory BlockedUsersModel.fromJson(Map json) => _$BlockedUsersModelFromJson(json);
+}
diff --git a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart
new file mode 100644
index 00000000..bd2a156d
--- /dev/null
+++ b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart
@@ -0,0 +1,29 @@
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
+import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BlockedUsersCubit extends Cubit {
+ final BlockedUsersRepository blockedUsersRepository;
+
+ BlockedUsersCubit({
+ required this.blockedUsersRepository,
+ }) : super(const BlockedUsersState());
+
+ getBlockedUsers() async {
+ try {
+ emit(BlockedUsersState.loaded(profilesList: await blockedUsersRepository.getBlockedUsers()));
+ } catch (e, st) {
+ emit(BlockedUsersState.error(errorMessage: '$e $st'));
+ }
+ }
+
+ blockUser(String slug) async {
+ await blockedUsersRepository.blockUserBySlug(slug);
+ await getBlockedUsers();
+ }
+
+ unblockUser(String slug) async {
+ await blockedUsersRepository.unblockUserBySlug(slug);
+ await getBlockedUsers();
+ }
+}
diff --git a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart
new file mode 100644
index 00000000..2d83cdc6
--- /dev/null
+++ b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart
@@ -0,0 +1,15 @@
+import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:flutter/foundation.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'blocked_users_state.freezed.dart';
+
+@freezed
+abstract class BlockedUsersState with _$BlockedUsersState {
+ const factory BlockedUsersState() = _BlockedUsersState;
+ const factory BlockedUsersState.loading() = BlockedUsersStateLoading;
+ const factory BlockedUsersState.loaded({required List profilesList}) = BlockedUsersStateLoaded;
+ const factory BlockedUsersState.error({
+ required String errorMessage,
+ }) = BlockedUsersStateError;
+}
diff --git a/lib/features/menu/presentation/pages/blocked_users_screen.dart b/lib/features/menu/presentation/pages/blocked_users_screen.dart
new file mode 100644
index 00000000..2f632026
--- /dev/null
+++ b/lib/features/menu/presentation/pages/blocked_users_screen.dart
@@ -0,0 +1,43 @@
+import 'package:eatvio_app/core/locator_service.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/features/menu/presentation/widgets/blocked_user_widget.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BlockedUsersScreen extends StatefulWidget {
+ const BlockedUsersScreen({super.key});
+
+ @override
+ State createState() => _BlockedUsersScreenState();
+}
+
+class _BlockedUsersScreenState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (BuildContext context) => sl()..getBlockedUsers(),
+ child: BlocBuilder(
+ builder: (BuildContext context, BlockedUsersState state) {
+ if (state is BlockedUsersStateLoaded) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Чёрный список'),
+ ),
+ body: ListView.builder(
+ itemBuilder: (context, index) => BlockedUserWidget(profile: state.profilesList[index]),
+ itemCount: state.profilesList.length,
+ ),
+ );
+ } else {
+ return const Scaffold(
+ body: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart
index c8fa2135..0449113b 100644
--- a/lib/features/menu/presentation/pages/menu_screen.dart
+++ b/lib/features/menu/presentation/pages/menu_screen.dart
@@ -53,6 +53,8 @@ class MenuScreen extends StatelessWidget {
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
+ _divider(context),
+ _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
// _divider(context),
// _tile(
// leading: ImageConstant.icnDevTools,
diff --git a/lib/features/menu/presentation/repositories/blocked_users_repository.dart b/lib/features/menu/presentation/repositories/blocked_users_repository.dart
new file mode 100644
index 00000000..96c072c4
--- /dev/null
+++ b/lib/features/menu/presentation/repositories/blocked_users_repository.dart
@@ -0,0 +1,47 @@
+import 'dart:developer';
+
+import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
+import 'package:eatvio_app/features/profile/models/profile_model.dart';
+
+class BlockedUsersRepository {
+ final BlockedUserService blockedUserService;
+
+ BlockedUsersRepository({
+ required this.blockedUserService,
+ });
+
+ Future> getBlockedUsers() async {
+ try {
+ return (await blockedUserService.getBlockedUsers()).profilesList;
+ } catch (e, st) {
+ log(e.toString(), stackTrace: st);
+ rethrow;
+ }
+ }
+
+ blockUserBySlug(String slug) async {
+ try {
+ return await blockedUserService.blockUser(
+ data: {
+ 'slug': slug,
+ },
+ );
+ } catch (e, st) {
+ log(e.toString(), stackTrace: st);
+ rethrow;
+ }
+ }
+
+ unblockUserBySlug(String slug) async {
+ try {
+ return await blockedUserService.unblockUser(
+ data: {
+ 'slug': slug,
+ },
+ );
+ } catch (e, st) {
+ log(e.toString(), stackTrace: st);
+ rethrow;
+ }
+ }
+}
diff --git a/lib/features/menu/presentation/widgets/blocked_user_widget.dart b/lib/features/menu/presentation/widgets/blocked_user_widget.dart
new file mode 100644
index 00000000..494df21c
--- /dev/null
+++ b/lib/features/menu/presentation/widgets/blocked_user_widget.dart
@@ -0,0 +1,83 @@
+import 'package:eatvio_app/core/const.dart';
+import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
+import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
+
+class BlockedUserWidget extends StatefulWidget {
+ final ProfileModel profile;
+ const BlockedUserWidget({super.key, required this.profile});
+
+ @override
+ State createState() => _BlockedUserWidgetState();
+}
+
+class _BlockedUserWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: () => GoRouter.of(context).pushNamed(
+ 'profileScreen',
+ extra: Author(
+ authorSlug: widget.profile.slug,
+ authorName: widget.profile.name,
+ authorProfileIcon: widget.profile.profilePhotoUrl,
+ authorIsMe: false,
+ isEntitySubscribedByMe: false,
+ ),
+ ),
+ child: Ink(
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+ child: Row(
+ children: [
+ CircleAvatar(
+ foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl),
+ backgroundImage: const AssetImage('assets/images/no_avatar.png'),
+ radius: 28,
+ ),
+ const SizedBox(width: 12),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ widget.profile.name,
+ style: const TextStyle(
+ color: Color(0xFF323F4B),
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ height: 1,
+ ),
+ ),
+ ],
+ ),
+ const Spacer(),
+ InkWell(
+ onTap: () {
+ context.read().unblockUser(widget.profile.slug);
+ },
+ borderRadius: BorderRadius.circular(4),
+ child: Ink(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF1F2F6),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: const Text(
+ 'Убрать',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF6D6F71),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/menu/services/blocked_users_service.dart b/lib/features/menu/services/blocked_users_service.dart
new file mode 100644
index 00000000..6a4ed987
--- /dev/null
+++ b/lib/features/menu/services/blocked_users_service.dart
@@ -0,0 +1,23 @@
+import 'package:dio/dio.dart';
+import 'package:eatvio_app/features/menu/models/blocked_users_model.dart';
+import 'package:retrofit/retrofit.dart';
+
+part 'blocked_users_service.g.dart';
+
+@RestApi()
+abstract class BlockedUserService {
+ factory BlockedUserService(Dio dio, {String baseUrl}) = _BlockedUserService;
+
+ @GET('/api/blocked_users')
+ Future getBlockedUsers();
+
+ @POST('/api/blocked_users/block')
+ Future blockUser({
+ @Body() required Map data,
+ });
+
+ @POST('/api/blocked_users/unblock')
+ Future unblockUser({
+ @Body() required Map data,
+ });
+}
diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart
index 9dc4f880..d475d34b 100644
--- a/lib/features/profile/presentation/pages/profile_screen.dart
+++ b/lib/features/profile/presentation/pages/profile_screen.dart
@@ -14,6 +14,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart';
@@ -1175,6 +1176,8 @@ class _ProfileScreenState extends State {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
+ } else if (value == 'block') {
+ sl().blockUser(state.profile.slug);
} else {
showModalBottomSheet(
context: context,
@@ -1196,6 +1199,11 @@ class _ProfileScreenState extends State {
value: 'complain',
child: Text('Пожаловаться'),
),
+ if (widget.author != null)
+ const PopupMenuItem(
+ value: 'block',
+ child: Text('Заблокировать'),
+ ),
],
child: const Icon(Icons.more_horiz_rounded),
),
diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart
index 0ad05b44..a4fc00cb 100644
--- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart
+++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart
@@ -102,6 +102,7 @@ class _ProfileBlogPostWidgetState extends State {
path: 'blog_post/${(state.blogPosts[widget.index].id)}',
id: state.blogPosts[widget.index].id,
feedBlogPostModel: state.blogPosts[widget.index],
+ userSlug: state.blogPosts[widget.index].author.authorSlug,
),
],
),
diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart
index 2a7d489f..b9328ba3 100644
--- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart
+++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart
@@ -228,6 +228,7 @@ class _AboutRecipeScreenState extends State {
complainedContentType: ComplainedContent.recipe,
slug: widget.recipeSlug,
recipe: recipeState.recipe,
+ userSlug: recipeState.recipe.author.authorSlug,
),
const SizedBox(width: 16),
],
diff --git a/pubspec.yaml b/pubspec.yaml
index 2a17b9dc..d11956ec 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
-version: 1.1.11+41
+version: 1.1.14+44
publish_to: 'none'
environment:
@@ -38,8 +38,8 @@ dependencies:
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.2.1
- syncfusion_flutter_charts: ^29.2.5
- syncfusion_flutter_gauges: ^29.2.5
+ syncfusion_flutter_charts: ^30.1.37
+ syncfusion_flutter_gauges: ^30.1.37
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
share_plus: ^11.0.0
@@ -83,7 +83,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
- flutter_lints: ^5.0.0
+ flutter_lints: ^6.0.0
flutter:
uses-material-design: true