diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index d0746c88..36ca1a71 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; @@ -24,14 +23,10 @@ class MainNavigationRoutes { return [ ShellRoute( builder: (context, state, child) { - // Инициализация ProfileAvatarCubit при первом использовании ShellRoute final profileAvatarCubit = context.read(); - if (profileAvatarCubit.state is ProfileAvatarStateLoading) { - // Вызываем getMyProfile() только если состояние loading (первый запуск) - WidgetsBinding.instance.addPostFrameCallback((_) { - profileAvatarCubit.getMyProfile(); - }); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + profileAvatarCubit.ensureFreshForCurrentSession(); + }); return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child); }, diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart index 8aef9871..8293bdc7 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart @@ -6,6 +6,13 @@ class NewMessagesCubit extends Cubit { NewMessagesCubit() : super(const NewMessagesState.empty()); + void reset() { + _messagesCount = 0; + if (!isClosed) { + emit(const NewMessagesState.empty()); + } + } + void incrementCounter() { _messagesCount++; emit(NewMessagesState.loaded(messagesCount: _messagesCount)); diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index 2a51eca4..ffde009e 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -1,18 +1,22 @@ import 'dart:async'; import 'package:appmetrica_plugin/appmetrica_plugin.dart'; -import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:eatvio_app/core/error_reporting/report_from.dart'; -import 'package:go_router/go_router.dart'; import 'package:permission_handler/permission_handler.dart'; class ProfileAvatarCubit extends Cubit { final ProfileRepository profileRepository; - ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading()); + ProfileAvatarCubit({required this.profileRepository}) + : super(const ProfileAvatarState.loading()); + + int _requestId = 0; + int? _sessionFingerprint; + bool _ensuring = false; int _calculateAge(String birthdate) { try { @@ -25,7 +29,8 @@ class ProfileAvatarCubit extends Cubit { final now = DateTime.now(); int age = now.year - birthYear; - if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) { + if (now.month < birthMonth || + (now.month == birthMonth && now.day < birthDay)) { age--; } @@ -38,30 +43,82 @@ class ProfileAvatarCubit extends Cubit { return 0; } + void reset() { + _requestId++; + _sessionFingerprint = null; + unawaited(AppMetrica.setUserProfileID(null).catchError((Object _) {})); + if (!isClosed) emit(const ProfileAvatarState.loading()); + } + + Future ensureFreshForCurrentSession() async { + final token = await TokenStorage.getTokenFromStorage(); + if (token == null || token.isEmpty) { + reset(); + return; + } + + final fingerprint = token.hashCode; + if (_sessionFingerprint == fingerprint && + state is ProfileAvatarStateLoaded) { + return; + } + + if (_ensuring) { + return; + } + + _ensuring = true; + try { + await getMyProfile(); + } finally { + _ensuring = false; + } + } + Future getMyProfile() async { + final requestId = ++_requestId; + final token = await TokenStorage.getTokenFromStorage(); + final fingerprint = token?.hashCode; + if (isClosed || requestId != _requestId) return; + emit(const ProfileAvatarState.loading()); try { final profile = await profileRepository.getMyProfile(); + if (isClosed || requestId != _requestId) return; + final profileDetails = await profileRepository.getMyProfileDetails(); + if (isClosed || requestId != _requestId) return; + + _sessionFingerprint = fingerprint; emit(ProfileAvatarState.loaded(profile: profile)); + try { final notificationStatus = await Permission.notification.status; + if (isClosed || requestId != _requestId) return; final isNotificationEnabled = notificationStatus.isGranted; AppMetricaUserProfile userProfile = AppMetricaUserProfile([ AppMetricaNameAttribute.withValue(profile.name), - AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female), - AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)), - AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled), + AppMetricaGenderAttribute.withValue( + profileDetails.gender == 'male' + ? AppMetricaGender.male + : AppMetricaGender.female, + ), + AppMetricaBirthDateAttribute.withAge( + _calculateAge(profileDetails.birthdate), + ), + AppMetricaNotificationEnabledAttribute.withValue( + isNotificationEnabled, + ), ]); AppMetrica.setUserProfileID(profile.slug); AppMetrica.reportUserProfile(userProfile); } catch (e, st) { await reportFrom(e, st, this); - // Игнорируем ошибки плагинов, профиль уже загружен } } catch (e, st) { + if (isClosed || requestId != _requestId) return; await reportFrom(e, st, this); - AppRouter.navigatorKey.currentContext?.go('/auth-screen'); + if (isClosed || requestId != _requestId) return; emit(ProfileAvatarState.error(errorMessage: e.toString())); } } diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index c3317d8c..b46ae479 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -39,6 +39,8 @@ class BottomNavigationBarWidget extends StatefulWidget { class _BottomNavigationBarWidgetState extends State { int currentBottomNavBarIndex = 1; List? pages; + int? _pagesProfileId; + bool? _pagesIsConsultant; int tapsCount = 0; List _getPages(bool isConsultant) { @@ -179,7 +181,16 @@ class _BottomNavigationBarWidgetState extends State { final isConsultant = profileModel.roles.contains('consultant'); // Инициализируем pages на основе profileModel - pages ??= _getPages(isConsultant); + if (pages == null || + _pagesProfileId != profileModel.id || + _pagesIsConsultant != isConsultant) { + _pagesProfileId = profileModel.id; + _pagesIsConsultant = isConsultant; + pages = _getPages(isConsultant); + if (currentBottomNavBarIndex >= pages!.length) { + currentBottomNavBarIndex = 0; + } + } // Expose tab switching to descendants (e.g. notification widget) final controller = BottomNavScope.maybeOf(context); diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart index 499766f6..363d02fd 100644 --- a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart @@ -57,6 +57,17 @@ class ChatsCubit extends Cubit { int page = 1; late int lastPage; + /// Clears all user-scoped state. WS subscriptions stay intact so that + /// `getChatRoomsWithLoading` can re-load the new user's chats. + void reset() { + _query = null; + page = 1; + lastPage = 1; + if (!isClosed) { + emit(ChatsState()); + } + } + Future connectToWebSocket() async { try { await newIncomingMessagesSubscription?.cancel(); diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index a88d4623..0aa05206 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -12,9 +12,19 @@ class FeedCubit extends Cubit { bool _hasMore = true; bool isLoading = false; String? _query; + int _requestId = 0; FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); + void reset() { + _requestId++; + _nextCursor = null; + _hasMore = true; + isLoading = false; + _query = null; + if (!isClosed) emit(const FeedState.loading()); + } + void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return; final List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; @@ -39,17 +49,18 @@ class FeedCubit extends Cubit { } Future loadFeedBlogPostList({String? query, int? currentUserId}) async { + final requestId = ++_requestId; try { _query = query; _nextCursor = null; _hasMore = true; final feed = await feedRepository.getFeed(search: query, currentUserId: currentUserId); + if (isClosed || requestId != _requestId) return; _nextCursor = feed.nextCursor; _hasMore = _nextCursor != null; - if (!isClosed) { - emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false)); - } + emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false)); } catch (e, st) { + if (isClosed || requestId != _requestId) return; await reportFrom(e, st, this); if (!isClosed) { emit(FeedState.error(errorMessage: e.toString())); @@ -62,14 +73,16 @@ class FeedCubit extends Cubit { return; } + final requestId = _requestId; isLoading = true; emit((state as FeedStateLoaded).copyWith(isLoading: true)); try { final feed = await feedRepository.getFeed(search: _query, cursor: _nextCursor, currentUserId: currentUserId); + if (isClosed || requestId != _requestId) return; _nextCursor = feed.nextCursor; _hasMore = _nextCursor != null; - if (!isClosed && state is FeedStateLoaded) { + if (state is FeedStateLoaded) { emit( FeedState.loaded( feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feed.posts], @@ -78,14 +91,15 @@ class FeedCubit extends Cubit { ); } } catch (e, st) { + if (isClosed || requestId != _requestId) return; await reportFrom(e, st, this); - if (!isClosed && state is FeedStateLoaded) { + if (state is FeedStateLoaded) { emit((state as FeedStateLoaded).copyWith(isLoading: false)); - } else if (!isClosed) { + } else { emit(FeedState.error(errorMessage: e.toString())); } } finally { - isLoading = false; + if (requestId == _requestId) isLoading = false; } } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index a3c5827d..c750c513 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -16,6 +16,11 @@ class NotificationsCubit extends Cubit { StreamSubscription? _newNotificationsSubscription; NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty()); + void reset() { + _notifications.clear(); + if (!isClosed) emit(const NotificationsState.empty()); + } + Future connectToWebSocket() async { try { await _newNotificationsSubscription?.cancel(); diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 2801c2b4..c85a7e63 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -22,6 +22,10 @@ class ProfileCubit extends Cubit { /// Bottom-nav / own profile instance, used to refresh posts after create/update/delete. static ProfileCubit? myProfileCubit; + static void clearMyProfileReference() { + myProfileCubit = null; + } + ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async {