import 'dart:async'; import 'package:flutter/foundation.dart'; import '../../../../core/logging/logger.dart'; import '../../data/models/notification_item.dart'; import '../../data/repositories/notification_repository.dart'; enum NotificationStatus { initial, loading, loaded, error } class NotificationState { const NotificationState({ this.status = NotificationStatus.initial, this.items = const [], this.unreadCount = 0, this.hasMore = false, this.nextCursor, this.errorMessage, }); final NotificationStatus status; final List items; final int unreadCount; final bool hasMore; final String? nextCursor; final String? errorMessage; NotificationState copyWith({ NotificationStatus? status, List? items, int? unreadCount, bool? hasMore, String? nextCursor, String? errorMessage, }) { return NotificationState( status: status ?? this.status, items: items ?? this.items, unreadCount: unreadCount ?? this.unreadCount, hasMore: hasMore ?? this.hasMore, nextCursor: nextCursor ?? this.nextCursor, errorMessage: errorMessage ?? this.errorMessage, ); } } sealed class NotificationEvent {} final class LoadNotifications extends NotificationEvent {} final class RefreshNotifications extends NotificationEvent {} final class LoadMoreNotifications extends NotificationEvent {} final class MarkNotificationRead extends NotificationEvent { MarkNotificationRead({required this.notificationId}); final String notificationId; } final class MarkAllNotificationsRead extends NotificationEvent {} final class RefreshUnreadCount extends NotificationEvent {} final class NotificationCreatedEvent extends NotificationEvent { NotificationCreatedEvent({required this.item}); final NotificationItem item; } final class NotificationReadUpdatedEvent extends NotificationEvent { NotificationReadUpdatedEvent({ required this.notificationId, required this.isRead, }); final String notificationId; final bool isRead; } final class NotificationRevokedEvent extends NotificationEvent { NotificationRevokedEvent({required this.notificationId}); final String notificationId; } class NotificationBloc extends ChangeNotifier { NotificationBloc({required NotificationRepository repository}) : _repository = repository; final NotificationRepository _repository; final Logger _logger = getLogger('features.notifications.bloc'); NotificationState _state = const NotificationState(); NotificationState get state => _state; Future handleEvent(NotificationEvent event) async { switch (event) { case LoadNotifications(): await _loadNotifications(); case RefreshNotifications(): await _refreshNotifications(); case LoadMoreNotifications(): await _loadMore(); case MarkNotificationRead(): await _markRead(event.notificationId); case MarkAllNotificationsRead(): await _markAllRead(); case RefreshUnreadCount(): await _refreshUnreadCount(); case NotificationCreatedEvent(): _handleCreated(event.item); case NotificationReadUpdatedEvent(): _handleReadUpdated(event.notificationId, event.isRead); case NotificationRevokedEvent(): _handleRevoked(event.notificationId); } } Future _loadNotifications() async { if (_state.status == NotificationStatus.loading) return; _state = _state.copyWith(status: NotificationStatus.loading); notifyListeners(); try { final result = await _repository.listNotifications(limit: 20); _state = _state.copyWith( status: NotificationStatus.loaded, items: result.items, hasMore: result.hasMore, nextCursor: result.nextCursor, ); notifyListeners(); } catch (error, stackTrace) { _logger.error( message: 'Load notifications failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); _state = _state.copyWith( status: NotificationStatus.error, errorMessage: error.toString(), ); notifyListeners(); } } Future _refreshNotifications() async { try { final result = await _repository.listNotifications(limit: 20); _state = _state.copyWith( status: NotificationStatus.loaded, items: result.items, hasMore: result.hasMore, nextCursor: result.nextCursor, ); notifyListeners(); } catch (error, stackTrace) { _logger.error( message: 'Refresh notifications failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); } } Future _loadMore() async { if (!_state.hasMore || _state.nextCursor == null) return; try { final result = await _repository.listNotifications( limit: 20, cursor: _state.nextCursor, ); final allItems = [..._state.items, ...result.items]; _state = _state.copyWith( items: allItems, hasMore: result.hasMore, nextCursor: result.nextCursor, ); notifyListeners(); } catch (error, stackTrace) { _logger.error( message: 'Load more notifications failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); } } Future _markRead(String notificationId) async { final idx = _state.items.indexWhere((item) => item.id == notificationId); if (idx == -1) return; if (_state.items[idx].isRead) return; _logger.info( message: 'Mark notification read started', extra: {'notification_id': notificationId}, ); try { final updated = await _repository.markRead( notificationId: notificationId, ); final targetIndex = _state.items.indexWhere( (item) => item.id == updated.id, ); if (targetIndex == -1) { return; } _state = _state.copyWith( items: [ ..._state.items.sublist(0, targetIndex), updated, ..._state.items.sublist(targetIndex + 1), ], unreadCount: _state.unreadCount > 0 ? _state.unreadCount - 1 : 0, ); notifyListeners(); _logger.info( message: 'Mark notification read succeeded', extra: {'notification_id': notificationId}, ); } catch (error, stackTrace) { _logger.error( message: 'Mark read failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); } } Future _markAllRead() async { _logger.info(message: 'Mark all notifications read started'); try { await _repository.markAllRead(); _state = _state.copyWith( items: _state.items.map((item) => item.copyWith(isRead: true)).toList(), unreadCount: 0, ); notifyListeners(); _logger.info(message: 'Mark all notifications read succeeded'); } catch (error, stackTrace) { _logger.error( message: 'Mark all read failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); } } Future _refreshUnreadCount() async { try { final count = await _repository.getUnreadCount(); _state = _state.copyWith(unreadCount: count); notifyListeners(); } catch (error, stackTrace) { _logger.error( message: 'Refresh unread count failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); } } void _handleCreated(NotificationItem item) { final exists = _state.items.any((i) => i.id == item.id); if (exists) return; _state = _state.copyWith( items: [item, ..._state.items], unreadCount: _state.unreadCount + (item.isRead ? 0 : 1), ); notifyListeners(); } void _handleReadUpdated(String notificationId, bool isRead) { final idx = _state.items.indexWhere((item) => item.id == notificationId); if (idx == -1) return; final wasUnread = !_state.items[idx].isRead; final nowRead = isRead; _state = _state.copyWith( items: [ ..._state.items.sublist(0, idx), _state.items[idx].copyWith(isRead: nowRead), ..._state.items.sublist(idx + 1), ], ); if (wasUnread && nowRead) { _state = _state.copyWith( unreadCount: _state.unreadCount > 0 ? _state.unreadCount - 1 : 0, ); } else if (!wasUnread && !nowRead) { _state = _state.copyWith(unreadCount: _state.unreadCount + 1); } notifyListeners(); } void _handleRevoked(String notificationId) { final matchingItems = _state.items.where( (i) => i.notificationId == notificationId, ); if (matchingItems.isEmpty) return; final item = matchingItems.first; final wasUnread = !item.isRead; _state = _state.copyWith( items: _state.items .where((i) => i.notificationId != notificationId) .toList(), ); if (wasUnread) { _state = _state.copyWith( unreadCount: _state.unreadCount > 0 ? _state.unreadCount - 1 : 0, ); } notifyListeners(); } }