11import 'dart:async' ;
22
33import 'package:bloc/bloc.dart' ;
4+ import 'package:bloc_concurrency/bloc_concurrency.dart' ;
45import 'package:collection/collection.dart' ;
56import 'package:core/core.dart' ;
67import 'package:data_repository/data_repository.dart' ;
@@ -30,85 +31,135 @@ class InAppNotificationCenterBloc
3031 _appBloc = appBloc,
3132 _logger = logger,
3233 super (const InAppNotificationCenterState ()) {
33- on < InAppNotificationCenterSubscriptionRequested > (_onSubscriptionRequested);
34+ on < InAppNotificationCenterSubscriptionRequested > (
35+ _onSubscriptionRequested,
36+ transformer: droppable (),
37+ );
3438 on < InAppNotificationCenterMarkedAsRead > (_onMarkedAsRead);
3539 on < InAppNotificationCenterMarkAllAsRead > (_onMarkAllAsRead);
3640 on < InAppNotificationCenterTabChanged > (_onTabChanged);
3741 on < InAppNotificationCenterMarkOneAsRead > (_onMarkOneAsRead);
42+ on < InAppNotificationCenterFetchMoreRequested > (
43+ _onFetchMoreRequested,
44+ transformer: droppable (),
45+ );
3846 }
3947
48+ /// The number of notifications to fetch per page.
49+ static const _notificationsFetchLimit = 10 ;
50+
4051 final DataRepository <InAppNotification > _inAppNotificationRepository;
4152 final AppBloc _appBloc;
4253 final Logger _logger;
4354
44- /// Handles the request to load all notifications for the current user.
55+ /// Handles the initial subscription request to fetch notifications for both
56+ /// tabs concurrently.
4557 Future <void > _onSubscriptionRequested (
4658 InAppNotificationCenterSubscriptionRequested event,
4759 Emitter <InAppNotificationCenterState > emit,
4860 ) async {
4961 emit (state.copyWith (status: InAppNotificationCenterStatus .loading));
50-
5162 final userId = _appBloc.state.user? .id;
5263 if (userId == null ) {
53- _logger.warning ('Cannot fetch notifications: user is not logged in.' );
64+ _logger.warning (
65+ 'Cannot fetch more notifications: user is not logged in.' ,
66+ );
5467 emit (state.copyWith (status: InAppNotificationCenterStatus .failure));
5568 return ;
5669 }
5770
5871 try {
59- final response = await _inAppNotificationRepository.readAll (
60- userId: userId,
61- sort: [const SortOption ('createdAt' , SortOrder .desc)],
62- );
72+ // Fetch both tabs' initial data in parallel and wait for their results.
73+ final results = await Future .wait ([
74+ _fetchNotifications (userId: userId, filter: _breakingNewsFilter),
75+ _fetchNotifications (userId: userId, filter: _digestFilter),
76+ ]);
6377
64- final allNotifications = response.items;
65-
66- final breakingNews = < InAppNotification > [];
67- final digests = < InAppNotification > [];
68-
69- // Filter notifications into their respective categories, prioritizing
70- // 'notificationType' from the backend, then falling back to 'contentType'.
71- for (final n in allNotifications) {
72- final notificationType = n.payload.data['notificationType' ] as String ? ;
73- final contentType = n.payload.data['contentType' ] as String ? ;
74-
75- if (notificationType ==
76- PushNotificationSubscriptionDeliveryType .dailyDigest.name ||
77- notificationType ==
78- PushNotificationSubscriptionDeliveryType .weeklyRoundup.name ||
79- contentType == 'digest' ) {
80- digests.add (n);
81- } else {
82- // All other types (including 'breakingOnly' notificationType,
83- // 'headline' contentType, or any unknown types) go to breaking news.
84- breakingNews.add (n);
85- }
86- }
78+ final breakingNewsResponse = results[0 ];
79+ final digestResponse = results[1 ];
8780
81+ // Perform a single, atomic state update with both results.
8882 emit (
8983 state.copyWith (
9084 status: InAppNotificationCenterStatus .success,
91- breakingNewsNotifications: breakingNews,
92- digestNotifications: digests,
85+ breakingNewsNotifications: breakingNewsResponse.items,
86+ breakingNewsHasMore: breakingNewsResponse.hasMore,
87+ breakingNewsCursor: breakingNewsResponse.cursor,
88+ digestNotifications: digestResponse.items,
89+ digestHasMore: digestResponse.hasMore,
90+ digestCursor: digestResponse.cursor,
9391 ),
9492 );
95- } on HttpException catch (e, s) {
96- _logger.severe ('Failed to fetch in-app notifications.' , e, s);
97- emit (
98- state.copyWith (status: InAppNotificationCenterStatus .failure, error: e),
99- );
100- } catch (e, s) {
101- _logger.severe (
102- 'An unexpected error occurred while fetching in-app notifications.' ,
103- e,
104- s,
93+ } catch (error, stackTrace) {
94+ _handleFetchError (emit, error, stackTrace);
95+ }
96+ }
97+
98+ /// Handles fetching the next page of notifications for the current tab.
99+ Future <void > _onFetchMoreRequested (
100+ InAppNotificationCenterFetchMoreRequested event,
101+ Emitter <InAppNotificationCenterState > emit,
102+ ) async {
103+ final isBreakingNewsTab = state.currentTabIndex == 0 ;
104+ final hasMore = isBreakingNewsTab
105+ ? state.breakingNewsHasMore
106+ : state.digestHasMore;
107+
108+ if (state.status == InAppNotificationCenterStatus .loadingMore || ! hasMore) {
109+ return ;
110+ }
111+
112+ emit (state.copyWith (status: InAppNotificationCenterStatus .loadingMore));
113+
114+ final userId = _appBloc.state.user? .id;
115+ if (userId == null ) {
116+ _logger.warning (
117+ 'Cannot fetch more notifications: user is not logged in.' ,
105118 );
106- emit (
107- state.copyWith (
108- status: InAppNotificationCenterStatus .failure,
109- error: UnknownException (e.toString ()),
110- ),
119+ emit (state.copyWith (status: InAppNotificationCenterStatus .failure));
120+ return ;
121+ }
122+
123+ final filter = isBreakingNewsTab ? _breakingNewsFilter : _digestFilter;
124+ final cursor = isBreakingNewsTab
125+ ? state.breakingNewsCursor
126+ : state.digestCursor;
127+
128+ try {
129+ final response = await _fetchNotifications (
130+ userId: userId,
131+ filter: filter,
132+ cursor: cursor,
111133 );
134+
135+ // Append the new items to the correct list.
136+ if (isBreakingNewsTab) {
137+ emit (
138+ state.copyWith (
139+ status: InAppNotificationCenterStatus .success,
140+ breakingNewsNotifications: [
141+ ...state.breakingNewsNotifications,
142+ ...response.items,
143+ ],
144+ breakingNewsHasMore: response.hasMore,
145+ breakingNewsCursor: response.cursor,
146+ ),
147+ );
148+ } else {
149+ emit (
150+ state.copyWith (
151+ status: InAppNotificationCenterStatus .success,
152+ digestNotifications: [
153+ ...state.digestNotifications,
154+ ...response.items,
155+ ],
156+ digestHasMore: response.hasMore,
157+ digestCursor: response.cursor,
158+ ),
159+ );
160+ }
161+ } catch (error, stackTrace) {
162+ _handleFetchError (emit, error, stackTrace);
112163 }
113164 }
114165
@@ -117,6 +168,8 @@ class InAppNotificationCenterBloc
117168 InAppNotificationCenterTabChanged event,
118169 Emitter <InAppNotificationCenterState > emit,
119170 ) async {
171+ // If the tab is changed, we don't need to re-fetch data as it was
172+ // already fetched on initial load. We just update the index.
120173 emit (state.copyWith (currentTabIndex: event.tabIndex));
121174 }
122175
@@ -280,4 +333,68 @@ class InAppNotificationCenterBloc
280333 );
281334 }
282335 }
336+
337+ /// A generic method to fetch notifications based on a filter.
338+ Future <PaginatedResponse <InAppNotification >> _fetchNotifications ({
339+ required String userId,
340+ required Map <String , dynamic > filter,
341+ String ? cursor,
342+ }) async {
343+ // This method now simply fetches and returns the data, or throws on error.
344+ // The responsibility of emitting state is moved to the event handlers.
345+ return _inAppNotificationRepository.readAll (
346+ userId: userId,
347+ filter: filter,
348+ pagination: PaginationOptions (
349+ limit: _notificationsFetchLimit,
350+ cursor: cursor,
351+ ),
352+ sort: [const SortOption ('createdAt' , SortOrder .desc)],
353+ );
354+ }
355+
356+ /// Filter for "Breaking News" notifications.
357+ ///
358+ /// This filter uses the `$nin` (not in) operator to exclude notifications
359+ /// that are explicitly typed as digests. All other notifications are
360+ /// considered "breaking news" for the purpose of this tab.
361+ Map <String , dynamic > get _breakingNewsFilter => {
362+ 'payload.data.notificationType' : {
363+ r'$nin' : [
364+ PushNotificationSubscriptionDeliveryType .dailyDigest.name,
365+ PushNotificationSubscriptionDeliveryType .weeklyRoundup.name,
366+ ],
367+ },
368+ };
369+
370+ /// Filter for "Digests" notifications.
371+ ///
372+ /// This filter uses the `$in` operator to select notifications that are
373+ /// explicitly typed as either a daily or weekly digest.
374+ Map <String , dynamic > get _digestFilter => {
375+ 'payload.data.notificationType' : {
376+ r'$in' : [
377+ PushNotificationSubscriptionDeliveryType .dailyDigest.name,
378+ PushNotificationSubscriptionDeliveryType .weeklyRoundup.name,
379+ ],
380+ },
381+ };
382+
383+ /// Centralized error handler for fetch operations.
384+ void _handleFetchError (
385+ Emitter <InAppNotificationCenterState > emit,
386+ Object error,
387+ StackTrace stackTrace,
388+ ) {
389+ _logger.severe ('Failed to fetch notifications.' , error, stackTrace);
390+ final httpException = error is HttpException
391+ ? error
392+ : UnknownException (error.toString ());
393+ emit (
394+ state.copyWith (
395+ status: InAppNotificationCenterStatus .failure,
396+ error: httpException,
397+ ),
398+ );
399+ }
283400}
0 commit comments