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/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 2e0d56e4..bbf607e7 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -396,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 67;
+ CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -546,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 67;
+ CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -584,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 67;
+ CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index a1ded5ee..d9e83314 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 67
+ 68
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
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/env_config.dart b/lib/core/env_config.dart
index 23c18f62..b363ad70 100644
--- a/lib/core/env_config.dart
+++ b/lib/core/env_config.dart
@@ -1,43 +1,43 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class EnvConfig {
- static String get baseUrl => dotenv.env['BASE_URL'] ?? 'https://eatvio.ru';
- static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? 'https://dev2.eatvio.com';
+ static String get baseUrl => dotenv.env['BASE_URL'] ?? '';
+ static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? '';
// Sentry Configuration
- static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
+ static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// Firebase Configuration
- static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? 'eatvio-3467b';
- static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app';
- static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? 'eatvio-3467b.firebasestorage.app';
- static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '613980912922';
+ static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
+ static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';
+ static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '';
+ static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '';
// Firebase Android Configuration
- static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k';
- static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '1:613980912922:android:4de95e5f2d50ba69ecc0a3';
+ static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? '';
+ static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '';
// Firebase iOS Configuration
- static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ';
- static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '1:613980912922:ios:c557d140a1212e63ecc0a3';
- static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? 'com.eatvioapp.app';
+ static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? '';
+ static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '';
+ static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? '';
// App Store URLs
- static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? 'https://apps.apple.com/app/idYOUR_APP_ID';
- static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? 'https://www.rustore.ru/catalog/app/com.eatvioapp.app';
+ static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? '';
+ static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? '';
// Telegram Support
- static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio';
- static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? 'help@eatv.io';
+ static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? '';
+ static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? '';
// Test Credentials (for development only)
- static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com';
- static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '1234567890';
+ static String get testEmail => dotenv.env['TEST_EMAIL'] ?? '';
+ static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '';
// Email Validation API
- static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? 'https://api.validate.email/validate';
+ static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? '';
// Connection Check URLs
- static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? 'https://google.com';
- static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? 'https://yandex.ru';
+ static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
+ static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
}
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/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart
index f5fd7a34..20b0e26c 100644
--- a/lib/features/auth/presentation/pages/auth_screen.dart
+++ b/lib/features/auth/presentation/pages/auth_screen.dart
@@ -73,10 +73,7 @@ class _AuthScreenState extends State {
rethrow;
}
}
- AppRouter.navigatorKey.currentContext?.go(
- '/feed-screen',
- extra: {'needToInitializeWebSocket': false},
- );
+ AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@override
@@ -102,11 +99,7 @@ class _AuthScreenState extends State {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
},
- child: SvgPicture.asset(
- ImageConstant.imgLogoEatvio,
- height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
- fit: BoxFit.contain,
- ),
+ child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
),
),
body: SafeArea(
@@ -137,10 +130,7 @@ class _AuthScreenState extends State {
hidePassword = !hidePassword;
});
},
- icon: Icon(
- hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
- color: Colors.grey,
- ),
+ icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey),
),
),
const SizedBox(height: 16),
@@ -149,11 +139,7 @@ class _AuthScreenState extends State {
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
- style: TextStyle(
- color: appTheme.blueA400,
- decorationColor: appTheme.blueA400,
- decoration: TextDecoration.underline,
- ),
+ style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
),
@@ -166,11 +152,7 @@ class _AuthScreenState extends State {
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
- style: TextStyle(
- color: appTheme.blueA400,
- decorationColor: appTheme.blueA400,
- decoration: TextDecoration.underline,
- ),
+ style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
],
@@ -181,23 +163,22 @@ class _AuthScreenState extends State {
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
- style: const TextStyle(
- fontSize: 12,
- color: Colors.black,
- ),
+ style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
- TextSpan(
- text: ' Публичной офертой',
- style: TextStyle(
- fontSize: 12,
- color: appTheme.blueA400,
- decorationColor: appTheme.blueA400,
- decoration: TextDecoration.underline,
+ if (!hidePlans) ...[
+ TextSpan(
+ text: ' Публичной офертой',
+ style: TextStyle(
+ fontSize: 12,
+ color: appTheme.blueA400,
+ decorationColor: appTheme.blueA400,
+ decoration: TextDecoration.underline,
+ ),
+ recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
- recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
- ),
- const TextSpan(text: ' и '),
+ const TextSpan(text: ' и '),
+ ],
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
@@ -223,17 +204,12 @@ class _AuthScreenState extends State {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- ),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
- final isAuthenticated = await authCubit.getTokenByCredentials(
- emailController.text,
- passwordController.text,
- );
+ final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
@@ -242,10 +218,7 @@ class _AuthScreenState extends State {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
- : Text(
- 'Войти',
- style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
- ),
+ : Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
const SizedBox(width: 8),
@@ -257,9 +230,7 @@ class _AuthScreenState extends State {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- ),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
@@ -274,14 +245,10 @@ class _AuthScreenState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
- const Text(
- 'Где взять QR-код?\n',
- style: TextStyle(
- fontSize: 18,
- ),
- ),
+ const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
- '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
+ '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
+ ),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@@ -293,15 +260,10 @@ class _AuthScreenState extends State {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(8),
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: const Text(
'Отмена',
- style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.w400,
- ),
+ style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
),
),
),
@@ -314,15 +276,10 @@ class _AuthScreenState extends State {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(8),
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text(
'Перейти',
- style: TextStyle(
- color: appTheme.blueA400,
- fontWeight: FontWeight.w600,
- ),
+ style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
),
@@ -337,10 +294,7 @@ class _AuthScreenState extends State {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
- : const Icon(
- Icons.qr_code_scanner_rounded,
- color: Colors.white,
- ),
+ : const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
),
],
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..97a574ee 100644
--- a/lib/features/chat/presentation/pages/chat_screen.dart
+++ b/lib/features/chat/presentation/pages/chat_screen.dart
@@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
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';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
+import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
@@ -93,7 +95,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 +173,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 +241,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 +254,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 +283,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 +357,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 +365,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 +382,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 +405,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 +430,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),
@@ -465,7 +448,27 @@ class _ChatScreenState extends State {
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
- onTap: () {
+ onTap: () async {
+ // Проверяем, заблокирован ли пользователь
+ final blockedUsersCubit = sl();
+ await blockedUsersCubit.getBlockedUsers();
+ final blockedState = blockedUsersCubit.state;
+
+ if (blockedState is BlockedUsersStateLoaded) {
+ final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
+ if (isBlocked) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ return;
+ }
+ }
+
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
@@ -525,13 +528,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 +576,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 +591,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 +628,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 +640,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/pages/empty_chat_screen.dart b/lib/features/chat/presentation/pages/empty_chat_screen.dart
index 7b543f86..d6a87c46 100644
--- a/lib/features/chat/presentation/pages/empty_chat_screen.dart
+++ b/lib/features/chat/presentation/pages/empty_chat_screen.dart
@@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.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';
class EmptyChatScreen extends StatefulWidget {
final Author author;
@@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State {
InkWell(
customBorder: const CircleBorder(),
onTap: () async {
+ // Проверяем, заблокирован ли пользователь
+ final blockedUsersCubit = sl();
+ await blockedUsersCubit.getBlockedUsers();
+ final blockedState = blockedUsersCubit.state;
+
+ if (blockedState is BlockedUsersStateLoaded) {
+ final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
+ if (isBlocked) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ return;
+ }
+ }
+
if (textEditingController.text.isNotEmpty) {
await sl().sendMessage(
data: textEditingController.text.trim(),
slug: widget.author.authorSlug,
);
- setState(() {
- textEditingController.clear();
- });
+ if (mounted) {
+ setState(() {
+ textEditingController.clear();
+ });
+ }
final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug);
- AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
- 'chatScreen',
- extra: {
- 'uuid': chatIdOrError,
- },
- );
+ if (context.mounted) {
+ AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
+ 'chatScreen',
+ extra: {
+ 'uuid': chatIdOrError,
+ },
+ );
+ }
}
},
child: Ink(
@@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State {
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
+
+ // Проверяем, заблокирован ли пользователь
+ final blockedUsersCubit = sl();
+ await blockedUsersCubit.getBlockedUsers();
+ final blockedState = blockedUsersCubit.state;
+
+ if (blockedState is BlockedUsersStateLoaded) {
+ final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
+ if (isBlocked) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ return;
+ }
+ }
+
final picker = ImagePicker();
- XFile? image = await picker.pickImage(source: ImageSource.camera);
- if (image != null) {
+ List? images = await picker.pickMultiImage();
+ if (images.isNotEmpty) {
+ List files = images.map((image) => File(image.path)).toList();
await sl().sendMessage(
- files: [File(image.path)],
+ files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug);
- AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
- 'chatScreen',
- extra: {
- 'uuid': chatIdOrError,
- },
- );
+ if (mounted) {
+ AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
+ 'chatScreen',
+ extra: {
+ 'uuid': chatIdOrError,
+ },
+ );
+ }
}
},
child: const Row(
@@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State {
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug);
- AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
- 'chatScreen',
- extra: {
- 'uuid': chatIdOrError,
- },
- );
+ if (mounted) {
+ AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
+ 'chatScreen',
+ extra: {
+ 'uuid': chatIdOrError,
+ },
+ );
+ }
}
},
child: const Row(
@@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State {
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug);
- AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
- 'chatScreen',
- extra: {
- 'uuid': chatIdOrError,
- },
- );
+ if (mounted) {
+ AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
+ 'chatScreen',
+ extra: {
+ 'uuid': chatIdOrError,
+ },
+ );
+ }
}
},
child: const Row(
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/consultant/become_consultant/presentation/pages/last_step_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart
index b5afd001..d44920cb 100644
--- a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart
+++ b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart
@@ -25,24 +25,16 @@ class _LastStepScreenState extends State {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'Последний шаг',
- style: TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.w700,
- height: 1.1,
- ),
- ),
+ child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
),
const SizedBox(height: 16),
CheckboxListTile(
value: checkboxState,
- title: const Text(
- 'Я ознакомлен и принимаю договор публичной офёрты',
- style: TextStyle(
- fontSize: 18,
- height: 1,
- ),
+ title: Text(
+ hidePlans
+ ? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
+ : 'Я ознакомлен и принимаю договор публичной офёрты',
+ style: const TextStyle(fontSize: 18, height: 1),
),
onChanged: (_) {
setState(() {
@@ -53,25 +45,14 @@ class _LastStepScreenState extends State {
activeColor: appTheme.blueA400,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 16,
- ),
- child: _divider(context),
- ),
+ Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton(
- onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
- child: const Text(
- 'Договор офёрты',
- style: TextStyle(
- height: 1,
- fontSize: 16,
- color: Color(0xFF323F4B),
- decoration: TextDecoration.underline,
- ),
+ onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
+ child: Text(
+ hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
+ style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
),
),
),
@@ -84,10 +65,7 @@ class _LastStepScreenState extends State {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
- decoration: BoxDecoration(
- color: Colors.black26,
- borderRadius: BorderRadius.circular(16),
- ),
+ decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
);
}
}
diff --git a/lib/features/consultations/services/consultation_service.dart b/lib/features/consultations/services/consultation_service.dart
index f0a89220..8f09e4a1 100644
--- a/lib/features/consultations/services/consultation_service.dart
+++ b/lib/features/consultations/services/consultation_service.dart
@@ -50,6 +50,7 @@ class ConsultationService {
else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
+ 'sort[]': 'created_at:desc',
},
);
return ConsultationPaginationModel.fromJson(response.data);
@@ -77,7 +78,8 @@ class ConsultationService {
final response = await dio.get(
'/api/finances/my_finances',
queryParameters: {
- if (dateComparison != null && date != null) 'filters[end_date]': date,
+ if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
+ 'sort[]': 'start_date:desc',
},
);
final data = response.data as Map;
@@ -96,7 +98,8 @@ class ConsultationService {
final response = await dio.get(
'/api/finances/my_purchases',
queryParameters: {
- if (dateComparison != null && date != null) 'filters[start_date]': date,
+ if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
+ 'sort[]': 'start_date:desc',
},
);
final data = response.data as Map;
diff --git a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart
index dd6c713e..a2f6daf0 100644
--- a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart
+++ b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart
@@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit {
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
? null
- : state.consultationFilterStatus == ConsultationFilterStatus.inProgress
- ? FiltersComparisonEnum.notInArray
- : FiltersComparisonEnum.equal,
+ : FiltersComparisonEnum.equal,
);
emit(state.copyWith(consultations: consultations, isLoading: false));
} catch (e) {
@@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit {
enum ConsultationFilterStatus {
all,
canceled,
- finished,
- inProgress;
+ finished;
String get name => switch (this) {
ConsultationFilterStatus.all => 'Все консультации',
ConsultationFilterStatus.canceled => 'Отменён',
ConsultationFilterStatus.finished => 'Завершён',
- ConsultationFilterStatus.inProgress => 'В процессе',
};
String? get value => switch (this) {
ConsultationFilterStatus.all => null,
ConsultationFilterStatus.canceled => 'canceled_by_the_initiator',
ConsultationFilterStatus.finished => 'charge_was_collected',
- ConsultationFilterStatus.inProgress => 'active',
};
}
diff --git a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart
index c54f8073..dfe84a55 100644
--- a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart
+++ b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart
@@ -14,7 +14,7 @@ abstract class ConsultationsArchiveState with _$ConsultationsArchiveState {
@Default([]) List consultations,
@Default(false) isLoading,
@Default(null) String? error,
- @Default(null) PeriodFilterStatus? periodFilterStatus,
+ @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
@Default(null) ConsultationFilterStatus? consultationFilterStatus,
required StatisticModel statisticModel,
}) = _ConsultationsArchiveState;
diff --git a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart
index fff61e45..4e1123e1 100644
--- a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart
+++ b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart
@@ -10,7 +10,7 @@ abstract class ConsultantFinancesState with _$ConsultantFinancesState {
@Default([]) List financesModels,
@Default(false) isLoading,
@Default(null) String? error,
- @Default(null) PeriodFilterStatus? periodFilterStatus,
+ @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
}) = _ConsultantFinancesState;
}
diff --git a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart
index 7ae22c03..65a53842 100644
--- a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart
+++ b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart
@@ -10,6 +10,6 @@ abstract class UserPurchasesState with _$UserPurchasesState {
@Default([]) List purchasesModels,
@Default(false) isLoading,
@Default(null) String? error,
- @Default(null) PeriodFilterStatus? periodFilterStatus,
+ @Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
}) = _UserPurchasesState;
}
diff --git a/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart b/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart
index 7e731474..085e9fa6 100644
--- a/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart
+++ b/lib/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart
@@ -96,7 +96,7 @@ class _ConsultantFinancesScreenState extends State {
if (value == null) return;
context.read().setFinancesPeriod(value);
},
- value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
+ value: state.periodFilterStatus ?? PeriodFilterStatus.all,
),
const SizedBox(height: 12),
if (state.financesModels.isNotEmpty)
@@ -129,7 +129,7 @@ class _ConsultantFinancesScreenState extends State {
);
}),
Text(
- state.financesModels[index].startDate.format('yyyy.MM.dd'),
+ state.financesModels[index].startDate.format('dd.MM.yy'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,
diff --git a/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart b/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart
index ec2d4900..c356e9fb 100644
--- a/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart
+++ b/lib/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart
@@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State
),
),
Text(
- state.consultations[index].startDate.format('yyyy.MM.dd'),
+ state.consultations[index].startDate.format('dd.MM.yy'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,
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/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart
index 96626d5d..31d74e10 100644
--- a/lib/features/notifications/models/notification_model.dart
+++ b/lib/features/notifications/models/notification_model.dart
@@ -538,6 +538,297 @@ class EntityCommented implements BaseNotificationModel {
}
}
+class PlanSubscriptionExpiredNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String planName;
+ final String customerName;
+ @override
+ final DateTime createdAt;
+
+ PlanSubscriptionExpiredNotification({
+ required this.type,
+ required this.planName,
+ required this.customerName,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory PlanSubscriptionExpiredNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return PlanSubscriptionExpiredNotification(
+ type: type,
+ planName: json['plan_name'],
+ customerName: json['user_name'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class PlanSubscriptionRenewedNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String planName;
+ final String customerName;
+ final String renewalDate;
+ @override
+ final DateTime createdAt;
+
+ PlanSubscriptionRenewedNotification({
+ required this.type,
+ required this.planName,
+ required this.customerName,
+ required this.renewalDate,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory PlanSubscriptionRenewedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return PlanSubscriptionRenewedNotification(
+ type: type,
+ planName: json['plan_name'],
+ customerName: json['user_name'],
+ renewalDate: json['renewal_date'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class ConsultantAssignedNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String consultantName;
+ final String planName;
+ final int planId;
+ @override
+ final DateTime createdAt;
+
+ ConsultantAssignedNotification({
+ required this.type,
+ required this.consultantName,
+ required this.planName,
+ required this.planId,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory ConsultantAssignedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return ConsultantAssignedNotification(
+ type: type,
+ consultantName: json['consultant_name'],
+ planName: json['plan_name'],
+ planId: json['plan_id'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class ConsultantUnassignedNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String consultantName;
+ final String planName;
+ final int planId;
+ @override
+ final DateTime createdAt;
+
+ ConsultantUnassignedNotification({
+ required this.type,
+ required this.consultantName,
+ required this.planName,
+ required this.planId,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory ConsultantUnassignedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return ConsultantUnassignedNotification(
+ type: type,
+ consultantName: json['consultant_name'],
+ planName: json['plan_name'],
+ planId: json['plan_id'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class PaymentFailedNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String planName;
+ final String errorMessage;
+ final String paymentMethod;
+ @override
+ final DateTime createdAt;
+
+ PaymentFailedNotification({
+ required this.type,
+ required this.planName,
+ required this.errorMessage,
+ required this.paymentMethod,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory PaymentFailedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return PaymentFailedNotification(
+ type: type,
+ planName: json['plan_name'],
+ errorMessage: json['error_message'],
+ paymentMethod: json['payment_method'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class PaymentSuccessfulNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String planName;
+ final double amount;
+ final String currency;
+ final String transactionId;
+ @override
+ final DateTime createdAt;
+
+ PaymentSuccessfulNotification({
+ required this.type,
+ required this.planName,
+ required this.amount,
+ required this.currency,
+ required this.transactionId,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory PaymentSuccessfulNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return PaymentSuccessfulNotification(
+ type: type,
+ planName: json['plan_name'],
+ amount: (json['amount'] as num).toDouble(),
+ currency: json['currency'],
+ transactionId: json['transaction_id'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class GoalAchievedNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String goalName;
+ final String goalType;
+ final double achievedValue;
+ final double targetValue;
+ @override
+ final DateTime createdAt;
+
+ GoalAchievedNotification({
+ required this.type,
+ required this.goalName,
+ required this.goalType,
+ required this.achievedValue,
+ required this.targetValue,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory GoalAchievedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return GoalAchievedNotification(
+ type: type,
+ goalName: json['goal_name'],
+ goalType: json['goal_type'],
+ achievedValue: (json['achieved_value'] as num).toDouble(),
+ targetValue: (json['target_value'] as num).toDouble(),
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class WorkoutReminderNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String workoutName;
+ final String workoutType;
+ final int duration;
+ @override
+ final DateTime createdAt;
+
+ WorkoutReminderNotification({
+ required this.type,
+ required this.workoutName,
+ required this.workoutType,
+ required this.duration,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory WorkoutReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return WorkoutReminderNotification(
+ type: type,
+ workoutName: json['workout_name'],
+ workoutType: json['workout_type'],
+ duration: json['duration'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
+class MealReminderNotification implements BaseNotificationModel {
+ @override
+ final NotificationType type;
+ @override
+ final bool isRead;
+ final String mealName;
+ final String mealType;
+ final int calories;
+ @override
+ final DateTime createdAt;
+
+ MealReminderNotification({
+ required this.type,
+ required this.mealName,
+ required this.mealType,
+ required this.calories,
+ required this.isRead,
+ required this.createdAt,
+ });
+
+ factory MealReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) {
+ return MealReminderNotification(
+ type: type,
+ mealName: json['meal_name'],
+ mealType: json['meal_type'],
+ calories: json['calories'],
+ isRead: isRead ?? false,
+ createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
+ );
+ }
+}
+
class UnknownNotification implements BaseNotificationModel {
@override
final NotificationType type;
@@ -570,6 +861,16 @@ enum NotificationType {
commentReplied,
scoreAdded,
entityCommented,
+ // Дополнительные типы уведомлений
+ planSubscriptionExpired,
+ planSubscriptionRenewed,
+ consultantAssigned,
+ consultantUnassigned,
+ paymentFailed,
+ paymentSuccessful,
+ goalAchieved,
+ workoutReminder,
+ mealReminder,
unknown;
static NotificationType getTypeFromString(String type) => switch (type) {
@@ -593,6 +894,16 @@ enum NotificationType {
'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied,
'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded,
'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented,
+ // Дополнительные маппинги
+ 'App\\Notifications\\Plan\\PlanSubscriptionExpired' => NotificationType.planSubscriptionExpired,
+ 'App\\Notifications\\Plan\\PlanSubscriptionRenewed' => NotificationType.planSubscriptionRenewed,
+ 'App\\Notifications\\Plan\\ConsultantAssigned' => NotificationType.consultantAssigned,
+ 'App\\Notifications\\Plan\\ConsultantUnassigned' => NotificationType.consultantUnassigned,
+ 'App\\Notifications\\Payment\\PaymentFailed' => NotificationType.paymentFailed,
+ 'App\\Notifications\\Payment\\PaymentSuccessful' => NotificationType.paymentSuccessful,
+ 'App\\Notifications\\Goal\\GoalAchieved' => NotificationType.goalAchieved,
+ 'App\\Notifications\\Workout\\WorkoutReminder' => NotificationType.workoutReminder,
+ 'App\\Notifications\\Meal\\MealReminder' => NotificationType.mealReminder,
_ => NotificationType.unknown,
};
}
diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
index d4e6e17d..df921b4a 100644
--- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
+++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
@@ -113,6 +113,16 @@ class NotificationsCubit extends Cubit {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ // Новые типы уведомлений
+ NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
+ NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {
diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart
index 18c02a13..eff047da 100644
--- a/lib/features/notifications/presentation/widgets/notification_widget.dart
+++ b/lib/features/notifications/presentation/widgets/notification_widget.dart
@@ -47,6 +47,16 @@ class NotificationWidget extends StatelessWidget {
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
+ // Новые типы уведомлений
+ PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification),
+ PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification),
+ ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification),
+ ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification),
+ PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification),
+ PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification),
+ GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification),
+ WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification),
+ MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
};
}
@@ -443,3 +453,121 @@ class _UnknownNotificationWidget extends StatelessWidget {
);
}
}
+
+// Новые виджеты для дополнительных типов уведомлений
+class _PlanSubscriptionExpiredWidget extends StatelessWidget {
+ const _PlanSubscriptionExpiredWidget({required this.notification});
+ final PlanSubscriptionExpiredNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Подписка на план "${notification.planName}" истекла',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _PlanSubscriptionRenewedWidget extends StatelessWidget {
+ const _PlanSubscriptionRenewedWidget({required this.notification});
+ final PlanSubscriptionRenewedNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _ConsultantAssignedWidget extends StatelessWidget {
+ const _ConsultantAssignedWidget({required this.notification});
+ final ConsultantAssignedNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _ConsultantUnassignedWidget extends StatelessWidget {
+ const _ConsultantUnassignedWidget({required this.notification});
+ final ConsultantUnassignedNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _PaymentFailedWidget extends StatelessWidget {
+ const _PaymentFailedWidget({required this.notification});
+ final PaymentFailedNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _PaymentSuccessfulWidget extends StatelessWidget {
+ const _PaymentSuccessfulWidget({required this.notification});
+ final PaymentSuccessfulNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _GoalAchievedWidget extends StatelessWidget {
+ const _GoalAchievedWidget({required this.notification});
+ final GoalAchievedNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _WorkoutReminderWidget extends StatelessWidget {
+ const _WorkoutReminderWidget({required this.notification});
+ final WorkoutReminderNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)',
+ createdAt: notification.createdAt,
+ );
+ }
+}
+
+class _MealReminderWidget extends StatelessWidget {
+ const _MealReminderWidget({required this.notification});
+ final MealReminderNotification notification;
+ @override
+ Widget build(BuildContext context) {
+ return _BaseNotificationWidget(
+ isRead: notification.isRead,
+ notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)',
+ createdAt: notification.createdAt,
+ );
+ }
+}
diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart
index 61f545ab..7fdcb4fe 100644
--- a/lib/features/notifications/service/notifications_service.dart
+++ b/lib/features/notifications/service/notifications_service.dart
@@ -119,6 +119,61 @@ class NotificationsService {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
+ // Новые типы уведомлений
+ NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.paymentFailed => PaymentFailedNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.goalAchieved => GoalAchievedNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
+ NotificationType.mealReminder => MealReminderNotification.fromJson(
+ notificationJson['data'],
+ type,
+ isRead: readDate != null,
+ createdAt: createdAt,
+ ),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {
diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart
index f798021f..03fd57f3 100644
--- a/lib/features/profile/models/profile_model.dart
+++ b/lib/features/profile/models/profile_model.dart
@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
+ @JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl,
diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart
index f3329074..990beed1 100644
--- a/lib/features/profile/presentation/pages/profile_screen.dart
+++ b/lib/features/profile/presentation/pages/profile_screen.dart
@@ -93,6 +93,39 @@ class _ProfileScreenState extends State {
}
isFirstFetch = false;
}
+
+ // Проверяем, заблокировал ли пользователь меня
+ if (state.profile.isBlockedByUser) {
+ return Scaffold(
+ appBar: AppBar(),
+ body: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.block,
+ size: 64,
+ color: Colors.grey[400],
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Пользователь ограничил доступ к своей странице',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w500,
+ color: Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return Scaffold(
@@ -174,10 +207,10 @@ class _ProfileScreenState extends State {
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
- if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state),
- if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state),
- if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state),
- if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
_blogPosts(state),
],
),
diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart
index af99f901..0d39d303 100644
--- a/lib/features/settings/presentation/pages/settings_screen.dart
+++ b/lib/features/settings/presentation/pages/settings_screen.dart
@@ -51,9 +51,7 @@ class _SettingsScreenState extends State {
builder: (BuildContext context, SettingsState state) {
if (state is SettingsStateLoaded) {
return Scaffold(
- appBar: AppBar(
- title: const Text('Настройки'),
- ),
+ appBar: AppBar(title: const Text('Настройки')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -64,8 +62,7 @@ class _SettingsScreenState extends State {
const SizedBox(height: 12),
_notifications(state),
const SizedBox(height: 12),
- _aboutEatvio(state),
- const SizedBox(height: 12),
+ if (!hidePlans) ...[_aboutEatvio(state), const SizedBox(height: 12)],
_becomeConsultantButton(state),
_deleteAccountButton(state, settingsCubit),
],
@@ -76,16 +73,12 @@ class _SettingsScreenState extends State {
}
if (state is SettingsStateError) {
return Scaffold(
- appBar: AppBar(
- title: const Text('Настройки'),
- ),
+ appBar: AppBar(title: const Text('Настройки')),
body: DataErrorWidget(onReload: () => settingsCubit.init()),
);
}
return Scaffold(
- appBar: AppBar(
- title: const Text('Настройки'),
- ),
+ appBar: AppBar(title: const Text('Настройки')),
body: const Loader(),
);
},
@@ -101,22 +94,13 @@ class _SettingsScreenState extends State {
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Дополнительаная информация',
- style: TextStyle(
- fontSize: 16,
- height: 1,
- color: Colors.grey,
- fontFamily: 'Mulish',
- ),
+ style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
),
),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'generalInfoScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Общая информация',
),
@@ -124,11 +108,7 @@ class _SettingsScreenState extends State {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalFormScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая форма',
),
@@ -136,11 +116,7 @@ class _SettingsScreenState extends State {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'pressureScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Давление',
),
@@ -148,11 +124,7 @@ class _SettingsScreenState extends State {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'otherInfoScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Прочая информация',
),
@@ -160,31 +132,17 @@ class _SettingsScreenState extends State {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalActivityScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая нагрузка',
),
_divider(context),
- _tile(
- onTap: () => GoRouter.of(context).pushNamed(
- 'forgetPasswordScreen',
- extra: true,
- ),
- title: 'Сменить пароль',
- ),
+ _tile(onTap: () => GoRouter.of(context).pushNamed('forgetPasswordScreen', extra: true), title: 'Сменить пароль'),
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'changeEmailScreen',
- extra: SettingsEntity(
- profileSettingsModel: state.profileSettings,
- profileDetailsModel: state.profileDetails,
- settingsCubit: settingsCubit,
- ),
+ extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Сменить Email',
),
@@ -198,14 +156,7 @@ class _SettingsScreenState extends State {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'Уведомления',
- style: TextStyle(
- fontSize: 16,
- height: 1,
- color: Colors.grey,
- ),
- ),
+ child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -213,11 +164,7 @@ class _SettingsScreenState extends State {
setState(() {
updatesNotificationsSwitchState = !updatesNotificationsSwitchState;
});
- settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- newUpdatesNotifications: updatesNotificationsSwitchState,
- ),
- );
+ settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState));
},
title: 'Уведомление о новых обновлениях',
trailing: CupertinoSwitch(
@@ -228,9 +175,7 @@ class _SettingsScreenState extends State {
updatesNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- newUpdatesNotifications: updatesNotificationsSwitchState,
- ),
+ profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState),
);
},
),
@@ -243,9 +188,7 @@ class _SettingsScreenState extends State {
fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- diaryReminderNotifications: fillDiaryNotificationsSwitchState,
- ),
+ profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
title: 'Напоминание о заполнении дневника',
@@ -257,9 +200,7 @@ class _SettingsScreenState extends State {
fillDiaryNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- diaryReminderNotifications: fillDiaryNotificationsSwitchState,
- ),
+ profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
),
@@ -271,11 +212,7 @@ class _SettingsScreenState extends State {
setState(() {
moodSwitchState = !moodSwitchState;
});
- settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- isMoodFillable: moodSwitchState,
- ),
- );
+ settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
title: 'Указывать “настроение” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -285,11 +222,7 @@ class _SettingsScreenState extends State {
setState(() {
moodSwitchState = newValue;
});
- settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- isMoodFillable: moodSwitchState,
- ),
- );
+ settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
),
),
@@ -300,11 +233,7 @@ class _SettingsScreenState extends State {
setState(() {
bedTimeSwitchState = !bedTimeSwitchState;
});
- settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- isBedtimeFillable: bedTimeSwitchState,
- ),
- );
+ settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
title: 'Указывать “отход ко сну” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -314,11 +243,7 @@ class _SettingsScreenState extends State {
setState(() {
bedTimeSwitchState = newValue;
});
- settingsCubit.updateProfileSettings(
- profileSettingsModel: state.profileSettings.copyWith(
- isBedtimeFillable: bedTimeSwitchState,
- ),
- );
+ settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
),
),
@@ -332,24 +257,11 @@ class _SettingsScreenState extends State {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'О Eatvio',
- style: TextStyle(
- fontSize: 16,
- height: 1,
- color: Colors.grey,
- ),
- ),
- ),
- _tile(
- title: 'О компании',
- onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details')),
+ child: Text('О Eatvio', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
+ _tile(title: 'О компании', onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details'))),
_divider(context),
- _tile(
- title: 'Пользовательское соглашение',
- onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
- ),
+ _tile(title: 'Пользовательское соглашение', onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf'))),
],
);
}
@@ -364,19 +276,9 @@ class _SettingsScreenState extends State {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(20),
- color: appTheme.blueA400,
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
- child: Text(
- 'Стать консультантом',
- style: TextStyle(
- color: Colors.white,
- height: 1,
- fontSize: 18,
- ),
- ),
+ child: Text('Стать консультантом', style: TextStyle(color: Colors.white, height: 1, fontSize: 18)),
),
),
),
@@ -388,19 +290,9 @@ class _SettingsScreenState extends State {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(20),
- color: const Color(0xFFEAEAEA),
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
- child: Text(
- 'Данные на проверке',
- style: TextStyle(
- color: Colors.black,
- height: 1,
- fontSize: 18,
- ),
- ),
+ child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
);
@@ -413,19 +305,9 @@ class _SettingsScreenState extends State {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(20),
- color: const Color(0x33FFCC00),
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
child: const Center(
- child: Text(
- 'Заявка обработана',
- style: TextStyle(
- color: Colors.black,
- height: 1,
- fontSize: 18,
- ),
- ),
+ child: Text('Заявка обработана', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
@@ -444,57 +326,28 @@ class _SettingsScreenState extends State {
userRootNavigator: true,
context: context,
height: null,
- child: DeleteAccountBottomSheet(
- cubit: settingsCubit,
- ),
+ child: DeleteAccountBottomSheet(cubit: settingsCubit),
);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(20),
- color: const Color(0xFFEAEAEA),
- ),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
- child: Text(
- 'Удалить аккаунт',
- style: TextStyle(
- color: Colors.black,
- height: 1,
- fontSize: 18,
- ),
- ),
+ child: Text('Удалить аккаунт', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
);
}
- ListTile _tile({
- required String title,
- required Function() onTap,
- Widget? trailing,
- EdgeInsets? padding,
- }) {
+ ListTile _tile({required String title, required Function() onTap, Widget? trailing, EdgeInsets? padding}) {
return ListTile(
contentPadding: padding,
onTap: onTap,
- title: Text(
- title,
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
- height: 1,
- ),
- ),
- trailing: trailing ??
- const Icon(
- Icons.arrow_forward_ios_rounded,
- size: 12,
- color: Colors.black38,
- ),
+ title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
+ trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
@@ -503,10 +356,7 @@ class _SettingsScreenState extends State {
child: Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
- decoration: BoxDecoration(
- color: Colors.black26,
- borderRadius: BorderRadius.circular(16),
- ),
+ decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
),
);
}
@@ -530,33 +380,23 @@ class _DeleteAccountBottomSheet extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- const SizedBox(
- height: 8,
- ),
+ const SizedBox(height: 8),
const BottomSheetHandlerBar(),
- const SizedBox(
- height: 20,
- ),
+ const SizedBox(height: 20),
Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
- const SizedBox(
- height: 16,
- ),
+ const SizedBox(height: 16),
Text(
s.youShureYouWantToDeleteAccount,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
- const SizedBox(
- height: 8,
- ),
+ const SizedBox(height: 8),
Text(
s.itCantBeRecovered,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
- const SizedBox(
- height: 8,
- ),
+ const SizedBox(height: 8),
CustomOutlinedButton(
text: s.delete,
height: 52,
@@ -573,9 +413,7 @@ class _DeleteAccountBottomSheet extends State {
showErrorDialog(context, description: s.cannotDeleteAccount);
},
),
- const SizedBox(
- height: 40,
- ),
+ const SizedBox(height: 40),
],
),
);
diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart
index c6f7719a..1fc70bb0 100644
--- a/lib/features/support/presentation/pages/support_screen.dart
+++ b/lib/features/support/presentation/pages/support_screen.dart
@@ -69,24 +69,17 @@ class SupportScreen extends StatelessWidget {
child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)),
),
const SizedBox(height: 12),
- InkWell(
- onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')),
- borderRadius: BorderRadius.circular(16),
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
- child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)),
+ if (!hidePlans) ...[
+ InkWell(
+ onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
+ borderRadius: BorderRadius.circular(16),
+ child: const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
+ child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)),
+ ),
),
- ),
- const SizedBox(height: 8),
- InkWell(
- onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
- borderRadius: BorderRadius.circular(16),
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
- child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)),
- ),
- ),
- const SizedBox(height: 8),
+ const SizedBox(height: 8),
+ ],
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')),
borderRadius: BorderRadius.circular(16),
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..d9dda30d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
-version: 1.2.8+68
+version: 1.2.9+69
publish_to: 'none'
environment:
@@ -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
diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart
index 106da096..8c666344 100644
--- a/test/features/profile/services/profile_service_test.dart
+++ b/test/features/profile/services/profile_service_test.dart
@@ -17,6 +17,7 @@ void main() {
(_) async => await Future.value(
const ProfileModel(
id: 0,
+ isBlockedByUser: false,
name: 'Alexander Avanesyan',
email: 'mso7nueh@mail.ru',
phoneConfirmationState: '',