diff --git a/lib/features/feed/presentation/pages/home_screen.dart b/lib/features/feed/presentation/pages/home_screen.dart index 9ba15aa9..d4d1adfe 100644 --- a/lib/features/feed/presentation/pages/home_screen.dart +++ b/lib/features/feed/presentation/pages/home_screen.dart @@ -166,8 +166,7 @@ class HomeScreen extends ConsumerWidget { ); final unreadMessages = ref.watch(unreadMessagesCountProvider); - final unreadNotifications = - ref.watch(unreadNotificationsCountProvider).asData?.value ?? 0; + final unreadNotifications = ref.watch(unreadNotificationsCountProvider); return Scaffold( key: const Key('home_scaffold'), diff --git a/lib/features/notifications/data/datasources/push_notification_service.dart b/lib/features/notifications/data/datasources/push_notification_service.dart index 22f0a096..8d4b431f 100644 --- a/lib/features/notifications/data/datasources/push_notification_service.dart +++ b/lib/features/notifications/data/datasources/push_notification_service.dart @@ -168,6 +168,8 @@ class PushNotificationService { } else { _route.push('/home/notifications'); } + case 'artist_pro_activated': + return; case 'new_post_by_followed': _route.push('/home/behind-the-track/$resourceId'); case 'new_message': diff --git a/lib/features/notifications/data/models/notification_model.dart b/lib/features/notifications/data/models/notification_model.dart index c4f8c246..edcb3a6d 100644 --- a/lib/features/notifications/data/models/notification_model.dart +++ b/lib/features/notifications/data/models/notification_model.dart @@ -70,6 +70,8 @@ class NotificationModel extends NotificationEntity { return NotificationType.comment; case 'new_post_by_followed': return NotificationType.newPostByFollowed; + case 'artist_pro_activated': + return NotificationType.artistProActivated; default: return NotificationType.newPostByFollowed; } diff --git a/lib/features/notifications/domain/entities/notification_entity.dart b/lib/features/notifications/domain/entities/notification_entity.dart index e4d9a15f..a26ca51c 100644 --- a/lib/features/notifications/domain/entities/notification_entity.dart +++ b/lib/features/notifications/domain/entities/notification_entity.dart @@ -8,7 +8,14 @@ import 'package:equatable/equatable.dart'; /// - [comment] — a comment was left on a track. /// - [newPostByFollowed] — a followed user uploaded a new track. Filtered out /// at the data layer and never shown in the UI. -enum NotificationType { follow, like, repost, comment, newPostByFollowed } +enum NotificationType { + follow, + like, + repost, + comment, + newPostByFollowed, + artistProActivated, +} /// The kind of content the notification refers to. /// diff --git a/lib/features/notifications/presentation/pages/notifications_screen.dart b/lib/features/notifications/presentation/pages/notifications_screen.dart index dcb60635..fdb8c13b 100644 --- a/lib/features/notifications/presentation/pages/notifications_screen.dart +++ b/lib/features/notifications/presentation/pages/notifications_screen.dart @@ -299,6 +299,7 @@ class _NotificationScreenState extends ConsumerState { case NotificationType.comment: if (embedId != null) context.push('/home/behind-the-track/$embedId'); case NotificationType.newPostByFollowed: + case NotificationType.artistProActivated: return; } } diff --git a/lib/features/notifications/presentation/providers/notifications_provider.dart b/lib/features/notifications/presentation/providers/notifications_provider.dart index 7def79a1..dd0842d6 100644 --- a/lib/features/notifications/presentation/providers/notifications_provider.dart +++ b/lib/features/notifications/presentation/providers/notifications_provider.dart @@ -123,7 +123,6 @@ class NotificationsNotifier extends StateNotifier { state = state.copyWith( items: result.items, - unreadCount: result.unreadCount, hasNext: result.hasNext, currentPage: 1, isLoading: false, @@ -138,6 +137,9 @@ class NotificationsNotifier extends StateNotifier { for (final n in unread) { _markRead(n.id).catchError((_) {}); } + + // All unread items have been marked as read — drop the badge to zero. + state = state.copyWith(unreadCount: 0); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } @@ -221,11 +223,21 @@ class NotificationsNotifier extends StateNotifier { /// optimistically (if the payload is complete) or falls back to a silent /// re-fetch (when the backend sends a stripped payload without actor info). void onSocketNotificationCreated(Map data) { - state = state.copyWith(unreadCount: state.unreadCount + 1); try { final payload = data['notification'] as Map; final notification = NotificationModel.fromJson(payload); final rawType = payload['type'] as String?; + if (notification.type == NotificationType.artistProActivated || + notification.type == NotificationType.newPostByFollowed) { + _markRead(notification.id).catchError((_) {}); + return; + } + // Skip replays of notifications already in the list + if (state.items.any((n) => n.id == notification.id)) return; + // Only bump the badge for genuinely new unread notifications + if (!notification.isRead) { + state = state.copyWith(unreadCount: state.unreadCount + 1); + } if (_activeType == null || rawType == _activeType) { state = state.copyWith(items: [notification, ...state.items]); return; diff --git a/lib/features/notifications/presentation/providers/unread_count_provider.dart b/lib/features/notifications/presentation/providers/unread_count_provider.dart index 63e0f80c..0c1c8d13 100644 --- a/lib/features/notifications/presentation/providers/unread_count_provider.dart +++ b/lib/features/notifications/presentation/providers/unread_count_provider.dart @@ -1,8 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rythmify/features/notifications/domain/usecases/get_unread_count_usecase.dart'; -import 'repo_provider.dart'; +import 'notifications_provider.dart'; -final unreadNotificationsCountProvider = FutureProvider((ref) { - final repo = ref.watch(repositoryprovider); - return GetUnreadCountUsecase(repo).call(); +/// Badge count shown in the app bar / tab bar. +/// +/// Derived from [notificationsProvider] so it drops to zero immediately +/// after the notifications page opens and marks everything as read. +final unreadNotificationsCountProvider = Provider((ref) { + return ref.watch(notificationsProvider).unreadCount; }); diff --git a/lib/features/notifications/presentation/widgets/notification_tile.dart b/lib/features/notifications/presentation/widgets/notification_tile.dart index 45b31825..814fac06 100644 --- a/lib/features/notifications/presentation/widgets/notification_tile.dart +++ b/lib/features/notifications/presentation/widgets/notification_tile.dart @@ -29,7 +29,8 @@ class NotificationTile extends StatelessWidget { @override Widget build(BuildContext context) { - if (notification.type == NotificationType.newPostByFollowed) { + if (notification.type == NotificationType.newPostByFollowed || + notification.type == NotificationType.artistProActivated) { return const SizedBox.shrink(); } return InkWell( @@ -136,6 +137,7 @@ class NotificationTile extends StatelessWidget { ], ); case NotificationType.newPostByFollowed: + case NotificationType.artistProActivated: return const SizedBox.shrink(); } } @@ -280,6 +282,7 @@ class _AvatarWithBadge extends StatelessWidget { case NotificationType.repost: return const Color(0xFF00BCD4); case NotificationType.newPostByFollowed: + case NotificationType.artistProActivated: return Colors.transparent; } } @@ -295,6 +298,7 @@ class _AvatarWithBadge extends StatelessWidget { case NotificationType.repost: return Icons.repeat_rounded; case NotificationType.newPostByFollowed: + case NotificationType.artistProActivated: return Icons.notifications_rounded; } } diff --git a/test/features/profile/Data/profile_pages_test.dart b/test/features/profile/Data/profile_pages_test.dart index 3bfe00c1..19271c01 100644 --- a/test/features/profile/Data/profile_pages_test.dart +++ b/test/features/profile/Data/profile_pages_test.dart @@ -476,10 +476,7 @@ void main() { ), ); - expect( - find.byKey(const Key('public_profile_play_button')), - findsOneWidget, - ); + expect(find.byKey(const Key('public_profile_play_button')), findsWidgets); }); testWidgets('should show shuffle button', (tester) async { diff --git a/test/features/profile/Domain/profile_entity_test.dart b/test/features/profile/Domain/profile_entity_test.dart index 9548c25f..a7e35ded 100644 --- a/test/features/profile/Domain/profile_entity_test.dart +++ b/test/features/profile/Domain/profile_entity_test.dart @@ -218,7 +218,7 @@ void main() { group('ProfileEntity — props', () { test('should expose all fields in props', () { - expect(tProfile.props.length, 18); + expect(tProfile.props.length, 19); }); test('should include null fields in props for minimal profile', () { diff --git a/test/features/track/domain/usecases/track_usecases_test.dart b/test/features/track/domain/usecases/track_usecases_test.dart index eab32ccd..ff999b1e 100644 --- a/test/features/track/domain/usecases/track_usecases_test.dart +++ b/test/features/track/domain/usecases/track_usecases_test.dart @@ -143,7 +143,7 @@ void main() { await usecase('track-123', true); - verify(() => mockRepository.toggleLike('track-123', false)).called(1); + verify(() => mockRepository.toggleLike('track-123', true)).called(1); }); test('should return early if id is empty', () async { @@ -168,7 +168,7 @@ void main() { await usecase('track-123', false); - verify(() => mockRepository.toggleRepost('track-123', true)).called(1); + verify(() => mockRepository.toggleRepost('track-123', false)).called(1); }, ); diff --git a/test/features/track/presentation/providers/track_interaction_provider_test.dart b/test/features/track/presentation/providers/track_interaction_provider_test.dart index 9b451868..c6910b96 100644 --- a/test/features/track/presentation/providers/track_interaction_provider_test.dart +++ b/test/features/track/presentation/providers/track_interaction_provider_test.dart @@ -39,7 +39,7 @@ void main() { await notifier.handleToggleLike('track-123', false); - verify(() => mockToggleLike.call('track-123', false)).called(1); + verify(() => mockToggleLike.call('track-123', true)).called(1); }); test('handleToggleLike handles errors gracefully', () async { @@ -62,7 +62,7 @@ void main() { await notifier.handleToggleRepost('track-123', true); - verify(() => mockToggleRepost.call('track-123', true)).called(1); + verify(() => mockToggleRepost.call('track-123', false)).called(1); }); test('handleToggleRepost handles errors gracefully', () async { @@ -74,7 +74,7 @@ void main() { await expectLater( notifier.handleToggleRepost('track-123', true), - completes, + throwsA(isA()), ); }); });