From 5f0b7e026be11096e2dc1997b6b2aba1d38b1769 Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Fri, 30 Oct 2026 15:15:21 +0300 Subject: [PATCH] fix(profile): refresh bottom-nav avatar on account switch Reset ProfileAvatarCubit and user-scoped feed, chat and notification state so stale data from the previous account cannot be rendered. Refresh the avatar when the stored token changes and rebuild nav tabs when the profile identity or consultant role changes. Refs: #1113 --- lib/core/routing/main_navigation_routes.dart | 11 +-- .../new_messages_cubit.dart | 7 ++ .../profile_avatar_cubit.dart | 75 ++++++++++++++++--- .../widgets/bottom_navigation_bar_widget.dart | 13 +++- .../bloc/chats_cubit/chats_cubit.dart | 11 +++ .../bloc/feed_cubit/feed_cubit.dart | 28 +++++-- .../notifications_cubit.dart | 5 ++ .../bloc/profile_cubit/profile_cubit.dart | 4 + 8 files changed, 129 insertions(+), 25 deletions(-) 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 {