feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage

This commit is contained in:
misha
2026-04-01 10:37:59 +03:00
parent ee2dd75a94
commit a13b623ca2
20 changed files with 949 additions and 343 deletions

View File

@@ -78,9 +78,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 8,
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -147,14 +145,18 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
onTap: () async {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
final isSent = await commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
if (!context.mounted) return;
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
@@ -237,9 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: MarkdownHtmlWidget(
markdownText: blogPostState.blogPostModel!.description,
),
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
@@ -20,45 +21,53 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
);
Future<void> getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await consultantRepository.getConsultantCertPhotos();
try {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await consultantRepository.getConsultantCertPhotos();
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
return CertificatesModel(title: course.title, date: course.date);
}).toList();
return CertificatesModel(title: course.title, date: course.date);
}).toList();
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
isUpdating: _update,
),
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
isUpdating: _update
),
);
);
} catch (e) {
log(e.toString());
}
}
Future<void> disableLoading() async {
final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos));
try {
final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos));
} catch (e) {
log(e.toString());
}
}
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {

View File

@@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
title: const Text('Стать консультантом'),
automaticallyImplyLeading: false,
centerTitle: false,
leading: IconButton(
onPressed: () => GoRouter.of(context).pop(),
icon: const Icon(Icons.close),
),
leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.',
style: TextStyle(
fontSize: 16,
),
style: TextStyle(fontSize: 16),
),
),
bottomSheet: _startRegistrationButton(),
@@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
InkWell _startRegistrationButton() {
return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 32,
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,
fontWeight: FontWeight.w600,
fontSize: 18,
),
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
),
),
),

View File

@@ -26,6 +26,17 @@ class FeedCubit extends Cubit<FeedState> {
}
}
void incrementCommentsState(int id) {
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexWhere((e) => e.id == id);
if (index == -1) return;
final current = feedBlogPostList[index];
feedBlogPostList[index] = current.copyWith(totalComments: current.totalComments + 1);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
}
}
Future<void> loadFeedBlogPostList({String? query}) async {
try {
_query = query;

View File

@@ -33,7 +33,7 @@ import '../widgets/recipes.dart';
class FeedScreen extends StatelessWidget {
const FeedScreen({super.key, this.initialTabIndex = 0});
final int initialTabIndex;
final int? initialTabIndex;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -51,7 +51,7 @@ class FeedScreen extends StatelessWidget {
class FeedScreenContent extends StatefulWidget {
const FeedScreenContent({super.key, required this.initialTabIndex});
final int initialTabIndex;
final int? initialTabIndex;
@override
State<FeedScreenContent> createState() => _FeedScreenContentState();
}
@@ -72,7 +72,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsScrollController = ScrollController();
@override
void initState() {
_selectedChipIndex = widget.initialTabIndex;
_selectedChipIndex = widget.initialTabIndex ?? 0;
chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты'];
consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context);
@@ -92,8 +92,11 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
@override
void didUpdateWidget(FeedScreenContent oldWidget) {
super.didUpdateWidget(oldWidget);
final initialTabIndex = widget.initialTabIndex;
setState(() {
_selectedChipIndex = widget.initialTabIndex;
if (initialTabIndex != null) {
_selectedChipIndex = initialTabIndex;
}
});
}

View File

@@ -13,9 +13,12 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
final PlanRepository planRepository;
AboutPlanCubit({required this.planRepository}) : super(AboutPlanState.initial());
Future<void> getPlan(String slug) async {
Future<void> getPlan({required String slug, int? index}) async {
try {
final plan = await planRepository.getPlan(slug);
if (index != null) {
planRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: plan.totalComments));
}
emit(state.copyWith(planModel: plan, error: null));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
@@ -27,13 +30,7 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
try {
emit(state.copyWith(isLoading: true));
final plan = await planRepository.getPlan(slug);
emit(
state.copyWith(
planModel: plan,
error: null,
isLoading: false,
),
);
emit(state.copyWith(planModel: plan, error: null, isLoading: false));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString(), isLoading: false));
@@ -59,21 +56,17 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
}
}
EasyDebounce.debounce(
'like_plan_$slug',
const Duration(seconds: 1),
() async {
try {
await planRepository.likePlan(slug);
} catch (e) {
log(e.toString());
if (index != null) {
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
EasyDebounce.debounce('like_plan_$slug', const Duration(seconds: 1), () async {
try {
await planRepository.likePlan(slug);
} catch (e) {
log(e.toString());
if (index != null) {
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index));
}
},
);
emit(state.copyWith(planModel: currentPlan, error: null));
}
});
}
Future<void> addToFavorite({required String slug, required int? index}) async {
@@ -84,22 +77,18 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index));
}
EasyDebounce.debounce(
'add_or_remove_plan_to_favorites',
const Duration(seconds: 1),
() async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
},
);
emit(state.copyWith(planModel: currentPlan, error: null));
}
});
}
Future<void> removeFromFavorite({required String slug, required int? index}) async {
@@ -110,22 +99,18 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index));
}
EasyDebounce.debounce(
'add_or_remove_plan_to_favorites',
const Duration(seconds: 1),
() async {
try {
await planRepository.removeFromFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async {
try {
await planRepository.removeFromFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
},
);
emit(state.copyWith(planModel: currentPlan, error: null));
}
});
}
Future<(ChosenPlanModel?, String?)> chosePlan() async {
@@ -149,8 +134,8 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
}
String _getOptionNameFromIndex(int index) => switch (index) {
1 => 'options2',
2 => 'options3',
_ => 'options1',
};
1 => 'options2',
2 => 'options3',
_ => 'options1',
};
}

View File

@@ -154,7 +154,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
const SizedBox(width: 16),
],
),
body: DataErrorWidget(onReload: () => context.read<AboutPlanCubit>().getPlan(widget.slug)),
body: DataErrorWidget(onReload: () => context.read<AboutPlanCubit>().getPlan(slug: widget.slug)),
);
}
return Scaffold(
@@ -295,7 +295,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.read<AboutPlanCubit>().getPlan(widget.slug);
context.read<AboutPlanCubit>().getPlan(slug: widget.slug, index: widget.index);
}
});
FocusManager.instance.primaryFocus?.unfocus();