import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:real_estate_mobile/core/storage/secure_storage.dart'; import 'package:real_estate_mobile/features/messaging/data/socket_service.dart'; import 'package:real_estate_mobile/features/notifications/data/models/notification_model.dart'; import 'package:real_estate_mobile/features/notifications/data/notification_repository.dart'; /// Unread notification count — polled every 30 seconds. final unreadCountProvider = StateNotifierProvider((ref) { return UnreadCountNotifier(NotificationRepository()); }); class UnreadCountNotifier extends StateNotifier { final NotificationRepository _repo; Timer? _timer; final List _subs = []; UnreadCountNotifier(this._repo) : super(0) { refresh(); _timer = Timer.periodic(const Duration(seconds: 30), (_) => refresh()); // Refresh count immediately when socket events arrive final socket = SocketService(); _subs.add(socket.onNewMessage.listen((_) => refresh())); _subs.add(socket.onConnectionRequest.listen((_) => refresh())); _subs.add(socket.onConnectionResponse.listen((_) => refresh())); } Future refresh() async { // Skip if no token (logged out) — prevents 401 errors final token = await SecureStorage.getAccessToken(); if (token == null) { if (mounted) state = 0; return; } try { final count = await _repo.getUnreadCount(); if (mounted) state = count; } catch (_) { // Silently fail — badge just won't update } } /// Set count to zero immediately (optimistic update). void setZero() { if (mounted) state = 0; } /// Decrement count by 1 (optimistic update for single mark-as-read). void decrement() { if (mounted && state > 0) state = state - 1; } /// Stop polling (call on logout). void stopPolling() { _timer?.cancel(); _timer = null; if (mounted) state = 0; } /// Restart polling (call on login). void startPolling() { _timer?.cancel(); refresh(); _timer = Timer.periodic(const Duration(seconds: 30), (_) => refresh()); } @override void dispose() { _timer?.cancel(); for (final sub in _subs) { sub.cancel(); } super.dispose(); } } /// Notification list state. class NotificationListState { final List notifications; final bool isLoading; final bool isLoadingMore; final String? error; final String filter; // 'all' | 'unread' final int currentPage; final int totalPages; const NotificationListState({ this.notifications = const [], this.isLoading = false, this.isLoadingMore = false, this.error, this.filter = 'all', this.currentPage = 1, this.totalPages = 1, }); NotificationListState copyWith({ List? notifications, bool? isLoading, bool? isLoadingMore, String? error, String? filter, int? currentPage, int? totalPages, }) { return NotificationListState( notifications: notifications ?? this.notifications, isLoading: isLoading ?? this.isLoading, isLoadingMore: isLoadingMore ?? this.isLoadingMore, error: error, filter: filter ?? this.filter, currentPage: currentPage ?? this.currentPage, totalPages: totalPages ?? this.totalPages, ); } } final notificationListProvider = StateNotifierProvider( (ref) { return NotificationListNotifier(NotificationRepository(), ref); }); class NotificationListNotifier extends StateNotifier { final NotificationRepository _repo; final Ref _ref; NotificationListNotifier(this._repo, this._ref) : super(const NotificationListState()); Future loadNotifications({String filter = 'all'}) async { state = state.copyWith(isLoading: true, error: null, filter: filter); try { final response = await _repo.getNotifications(filter: filter, page: 1, limit: 20); state = state.copyWith( notifications: response.notifications, isLoading: false, currentPage: response.currentPage, totalPages: response.totalPages, ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } Future loadMore() async { if (state.isLoadingMore || state.currentPage >= state.totalPages) return; state = state.copyWith(isLoadingMore: true); try { final nextPage = state.currentPage + 1; final response = await _repo.getNotifications( filter: state.filter, page: nextPage, limit: 20); state = state.copyWith( notifications: [...state.notifications, ...response.notifications], isLoadingMore: false, currentPage: response.currentPage, totalPages: response.totalPages, ); } catch (_) { state = state.copyWith(isLoadingMore: false); } } Future markAsRead(String notificationId) async { try { await _repo.markAsRead(notificationId); state = state.copyWith( notifications: state.notifications.map((n) { if (n.id == notificationId) return n.copyWith(read: true); return n; }).toList(), ); // Decrement count immediately, then sync with server final countNotifier = _ref.read(unreadCountProvider.notifier); countNotifier.decrement(); } catch (_) {} } Future markAllAsRead() async { try { // Set count to 0 immediately for instant UI feedback _ref.read(unreadCountProvider.notifier).setZero(); await _repo.markAllAsRead(); state = state.copyWith( notifications: state.notifications.map((n) => n.copyWith(read: true)).toList(), ); } catch (_) { // If API fails, refresh from server to get correct count _ref.read(unreadCountProvider.notifier).refresh(); } } }