forked from eatvio/eatvio_app
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
This commit is contained in:
@@ -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<ProfileAvatarCubit>();
|
||||
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
profileAvatarCubit.getMyProfile();
|
||||
});
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
profileAvatarCubit.ensureFreshForCurrentSession();
|
||||
});
|
||||
|
||||
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,13 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
|
||||
|
||||
NewMessagesCubit() : super(const NewMessagesState.empty());
|
||||
|
||||
void reset() {
|
||||
_messagesCount = 0;
|
||||
if (!isClosed) {
|
||||
emit(const NewMessagesState.empty());
|
||||
}
|
||||
}
|
||||
|
||||
void incrementCounter() {
|
||||
_messagesCount++;
|
||||
emit(NewMessagesState.loaded(messagesCount: _messagesCount));
|
||||
|
||||
@@ -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<ProfileAvatarState> {
|
||||
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<ProfileAvatarState> {
|
||||
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<ProfileAvatarState> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_requestId++;
|
||||
_sessionFingerprint = null;
|
||||
unawaited(AppMetrica.setUserProfileID(null).catchError((Object _) {}));
|
||||
if (!isClosed) emit(const ProfileAvatarState.loading());
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ class BottomNavigationBarWidget extends StatefulWidget {
|
||||
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
int currentBottomNavBarIndex = 1;
|
||||
List<String>? pages;
|
||||
int? _pagesProfileId;
|
||||
bool? _pagesIsConsultant;
|
||||
int tapsCount = 0;
|
||||
|
||||
List<String> _getPages(bool isConsultant) {
|
||||
@@ -179,7 +181,16 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
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);
|
||||
|
||||
@@ -57,6 +57,17 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
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<void> connectToWebSocket() async {
|
||||
try {
|
||||
await newIncomingMessagesSubscription?.cancel();
|
||||
|
||||
@@ -12,9 +12,19 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
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<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
|
||||
@@ -39,17 +49,18 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
}
|
||||
|
||||
Future<void> 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<FeedState> {
|
||||
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<FeedState> {
|
||||
);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
StreamSubscription? _newNotificationsSubscription;
|
||||
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
|
||||
|
||||
void reset() {
|
||||
_notifications.clear();
|
||||
if (!isClosed) emit(const NotificationsState.empty());
|
||||
}
|
||||
|
||||
Future<void> connectToWebSocket() async {
|
||||
try {
|
||||
await _newNotificationsSubscription?.cancel();
|
||||
|
||||
@@ -22,6 +22,10 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
/// 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<void> getProfileBySlug(String slug) async {
|
||||
|
||||
Reference in New Issue
Block a user