Files
eatvio_app/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
alex-chevski 5f0b7e026b 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
2026-10-30 15:15:21 +03:00

502 lines
22 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class NotificationsCubit extends Cubit<NotificationsState> {
final List<BaseNotificationModel> _notifications = [];
final WsManager wsManager;
final NotificationsRepository notificationsRepository;
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();
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
_handleNotification(event.data);
});
} catch (e, st) {
await reportFrom(e, st, this);
}
}
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
try {
final createdAt = notificationJson['created_at'];
final isRead = notificationJson['read_at'] != null;
return switch (type) {
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
// New notification types
NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
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),
// New notification types
NotificationType.dietCancelled => DietCancelledNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.phoneChanged => PhoneChangedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.subscription => SubscriptionNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalDietIsDefinedToCustomer => PersonalDietIsDefinedToCustomerNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.motivateUser => MotivateUserNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))),
};
} catch (e, st) {
unawaited(reportFrom(e, st, this));
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
}
}
void _handleNotification(String notificationData) {
try {
_handleNotificationParsed(notificationData);
} catch (e, st) {
unawaited(reportFrom(e, st, this));
}
}
void _handleNotificationParsed(String notificationData) {
final json = jsonDecode(notificationData) as Map<String, dynamic>;
// Улучшенная логика определения типа уведомления (порядок: database_type → type → notification_class_name)
String? stringifiedType;
// Сначала проверяем поле 'database_type' в корне JSON
if (json.containsKey('database_type') && json['database_type'] != null) {
stringifiedType = json['database_type'].toString();
log('Found database_type in root: $stringifiedType');
}
// Затем поле 'type' в корне
else if (json.containsKey('type') && json['type'] != null) {
stringifiedType = json['type'].toString();
log('Found type in root: $stringifiedType');
}
// Затем поле 'notification_class_name' в корне
else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) {
stringifiedType = json['notification_class_name'].toString();
log('Found notification_class_name in root: $stringifiedType');
}
// Если есть поле 'data', проверяем внутри него в том же порядке
else if (json.containsKey('data') && json['data'] is Map<String, dynamic>) {
final data = json['data'] as Map<String, dynamic>;
if (data.containsKey('database_type') && data['database_type'] != null) {
stringifiedType = data['database_type'].toString();
log('Found database_type in data: $stringifiedType');
} else if (data.containsKey('type') && data['type'] != null) {
stringifiedType = data['type'].toString();
log('Found type in data: $stringifiedType');
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
stringifiedType = data['notification_class_name'].toString();
log('Found notification_class_name in data: $stringifiedType');
}
}
// Если тип не найден, используем unknown
final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown;
log('Final type: $type, stringifiedType: $stringifiedType');
final notification = _parseNotification(json, type);
_notifications.insert(0, notification);
final currentState = state;
if (currentState is NotificationsStateLoaded) {
emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true));
}
}
Future<void> getNotifications() async {
try {
final result = await notificationsRepository.getNotifications(page: 1);
if (result != null) {
final notifications = result['notifications'] as List<BaseNotificationModel>;
final pagination = result['pagination'] as Map<String, dynamic>;
_notifications.clear();
_notifications.addAll(notifications);
final currentPage = pagination['current_page'] as int;
final lastPage = pagination['last_page'] as int;
final hasNextPage = currentPage < lastPage;
emit(
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty,
currentPage: currentPage,
hasNextPage: hasNextPage,
),
);
}
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}
Future<void> loadMoreNotifications() async {
final currentState = state;
if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) {
try {
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.currentPage + 1;
final result = await notificationsRepository.getNotifications(page: nextPage);
if (result != null) {
final newNotifications = result['notifications'] as List<BaseNotificationModel>;
final pagination = result['pagination'] as Map<String, dynamic>;
_notifications.addAll(newNotifications);
final currentPage = pagination['current_page'] as int;
final lastPage = pagination['last_page'] as int;
final hasNextPage = currentPage < lastPage;
emit(
currentState.copyWith(
notifications: _notifications,
notificationsCount: _notifications.length,
currentPage: currentPage,
hasNextPage: hasNextPage,
isLoadingMore: false,
),
);
} else {
emit(currentState.copyWith(isLoadingMore: false));
}
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(currentState.copyWith(isLoadingMore: false));
}
}
}
Future<void> markNotificationsAsRead() async {
final currentState = state;
if (currentState is NotificationsStateLoaded) {
try {
emit(currentState.copyWith(isLoading: true));
await notificationsRepository.markNotificationsAsRead();
await getNotifications();
emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(currentState.copyWith(isLoading: false));
}
}
}
}