@@ -27,14 +27,19 @@ class InAppNotificationCenterBloc
2727
2828 /// {@macro in_app_notification_center_bloc}
2929 InAppNotificationCenterBloc ({
30+ // The BLoC should not be responsible for creating its own dependencies.
31+ // They should be provided from the outside.
3032 required DataRepository <InAppNotification > inAppNotificationRepository,
3133 required AppBloc appBloc,
3234 required Logger logger,
3335 }) : _inAppNotificationRepository = inAppNotificationRepository,
3436 _appBloc = appBloc,
3537 _logger = logger,
3638 super (const InAppNotificationCenterState ()) {
37- on < InAppNotificationCenterSubscriptionRequested > (_onSubscriptionRequested);
39+ on < InAppNotificationCenterSubscriptionRequested > (
40+ _onSubscriptionRequested,
41+ transformer: droppable (),
42+ );
3843 on < InAppNotificationCenterMarkedAsRead > (_onMarkedAsRead);
3944 on < InAppNotificationCenterMarkAllAsRead > (_onMarkAllAsRead);
4045 on < InAppNotificationCenterTabChanged > (_onTabChanged);
@@ -49,79 +54,43 @@ class InAppNotificationCenterBloc
4954 final AppBloc _appBloc;
5055 final Logger _logger;
5156
52- /// Handles the request to load all notifications for the current user.
53- /// This now only fetches the first page of notifications. Subsequent pages
54- /// are loaded by [_onFetchMoreRequested] .
57+ /// Handles the initial subscription request to fetch notifications for both
58+ /// tabs concurrently.
5559 Future <void > _onSubscriptionRequested (
5660 InAppNotificationCenterSubscriptionRequested event,
5761 Emitter <InAppNotificationCenterState > emit,
5862 ) async {
5963 emit (state.copyWith (status: InAppNotificationCenterStatus .loading));
60-
6164 final userId = _appBloc.state.user? .id;
6265 if (userId == null ) {
63- _logger.warning ('Cannot fetch notifications: user is not logged in.' );
66+ _logger.warning (
67+ 'Cannot fetch more notifications: user is not logged in.' ,
68+ );
6469 emit (state.copyWith (status: InAppNotificationCenterStatus .failure));
6570 return ;
6671 }
6772
68- try {
69- final response = await _inAppNotificationRepository.readAll (
73+ // Fetch both tabs' initial data in parallel.
74+ await Future .wait ([
75+ _fetchNotifications (
76+ emit: emit,
7077 userId: userId,
71- // Fetch the first page with a defined limit.
72- pagination: const PaginationOptions (limit: _notificationsFetchLimit),
73- sort: [const SortOption ('createdAt' , SortOrder .desc)],
74- );
75-
76- final allNotifications = response.items;
77-
78- final breakingNews = < InAppNotification > [];
79- final digests = < InAppNotification > [];
80- _filterAndAddNotifications (
81- notifications: allNotifications,
82- breakingNewsList: breakingNews,
83- digestList: digests,
84- );
85-
86- // Since we are fetching all notifications together and then filtering,
87- // the pagination cursor and hasMore status will be the same for both lists.
88- // This assumes the backend doesn't support filtering by notification type
89- // in the query itself.
90- final hasMore = response.hasMore;
91- final cursor = response.cursor;
78+ filter: _breakingNewsFilter,
79+ isInitialFetch: true ,
80+ ),
81+ _fetchNotifications (
82+ emit: emit,
83+ userId: userId,
84+ filter: _digestFilter,
85+ isInitialFetch: true ,
86+ ),
87+ ]);
9288
93- emit (
94- state.copyWith (
95- status: InAppNotificationCenterStatus .success,
96- breakingNewsNotifications: breakingNews,
97- digestNotifications: digests,
98- breakingNewsHasMore: hasMore,
99- breakingNewsCursor: cursor,
100- digestHasMore: hasMore,
101- digestCursor: cursor,
102- ),
103- );
104- } on HttpException catch (e, s) {
105- _logger.severe ('Failed to fetch in-app notifications.' , e, s);
106- emit (
107- state.copyWith (status: InAppNotificationCenterStatus .failure, error: e),
108- );
109- } catch (e, s) {
110- _logger.severe (
111- 'An unexpected error occurred while fetching in-app notifications.' ,
112- e,
113- s,
114- );
115- emit (
116- state.copyWith (
117- status: InAppNotificationCenterStatus .failure,
118- error: UnknownException (e.toString ()),
119- ),
120- );
121- }
89+ // After both fetches are complete, set the status to success.
90+ emit (state.copyWith (status: InAppNotificationCenterStatus .success));
12291 }
12392
124- /// Handles fetching the next page of notifications when the user scrolls .
93+ /// Handles fetching the next page of notifications for the current tab .
12594 Future <void > _onFetchMoreRequested (
12695 InAppNotificationCenterFetchMoreRequested event,
12796 Emitter <InAppNotificationCenterState > emit,
@@ -146,72 +115,29 @@ class InAppNotificationCenterBloc
146115 return ;
147116 }
148117
118+ final filter = isBreakingNewsTab ? _breakingNewsFilter : _digestFilter;
149119 final cursor = isBreakingNewsTab
150120 ? state.breakingNewsCursor
151121 : state.digestCursor;
152122
153- try {
154- final response = await _inAppNotificationRepository.readAll (
155- userId: userId,
156- pagination: PaginationOptions (
157- limit: _notificationsFetchLimit,
158- cursor: cursor,
159- ),
160- sort: [const SortOption ('createdAt' , SortOrder .desc)],
161- );
162-
163- final newNotifications = response.items;
164- final newBreakingNews = < InAppNotification > [];
165- final newDigests = < InAppNotification > [];
166-
167- _filterAndAddNotifications (
168- notifications: newNotifications,
169- breakingNewsList: newBreakingNews,
170- digestList: newDigests,
171- );
172-
173- final nextCursor = response.cursor;
174- final nextHasMore = response.hasMore;
123+ await _fetchNotifications (
124+ emit: emit,
125+ userId: userId,
126+ filter: filter,
127+ cursor: cursor,
128+ );
175129
176- emit (
177- state.copyWith (
178- status: InAppNotificationCenterStatus .success,
179- breakingNewsNotifications: [
180- ...state.breakingNewsNotifications,
181- ...newBreakingNews,
182- ],
183- digestNotifications: [...state.digestNotifications, ...newDigests],
184- breakingNewsHasMore: nextHasMore,
185- breakingNewsCursor: nextCursor,
186- digestHasMore: nextHasMore,
187- digestCursor: nextCursor,
188- ),
189- );
190- } on HttpException catch (e, s) {
191- _logger.severe ('Failed to fetch more in-app notifications.' , e, s);
192- emit (
193- state.copyWith (status: InAppNotificationCenterStatus .failure, error: e),
194- );
195- } catch (e, s) {
196- _logger.severe (
197- 'An unexpected error occurred while fetching more in-app notifications.' ,
198- e,
199- s,
200- );
201- emit (
202- state.copyWith (
203- status: InAppNotificationCenterStatus .failure,
204- error: UnknownException (e.toString ()),
205- ),
206- );
207- }
130+ // After fetch, set status back to success.
131+ emit (state.copyWith (status: InAppNotificationCenterStatus .success));
208132 }
209133
210134 /// Handles the event to change the active tab.
211135 Future <void > _onTabChanged (
212136 InAppNotificationCenterTabChanged event,
213137 Emitter <InAppNotificationCenterState > emit,
214138 ) async {
139+ // If the tab is changed, we don't need to re-fetch data as it was
140+ // already fetched on initial load. We just update the index.
215141 emit (state.copyWith (currentTabIndex: event.tabIndex));
216142 }
217143
@@ -376,27 +302,91 @@ class InAppNotificationCenterBloc
376302 }
377303 }
378304
379- /// A helper method to filter a list of notifications into "Breaking News"
380- /// and "Digests" categories and add them to the provided lists.
381- void _filterAndAddNotifications ({
382- required List <InAppNotification > notifications,
383- required List <InAppNotification > breakingNewsList,
384- required List <InAppNotification > digestList,
305+ /// A generic method to fetch notifications based on a filter.
306+ Future <void > _fetchNotifications ({
307+ required Emitter <InAppNotificationCenterState > emit,
308+ required String userId,
309+ required Map <String , dynamic > filter,
310+ String ? cursor,
311+ bool isInitialFetch = false ,
385312 }) {
386- for (final n in notifications) {
387- final notificationType = n.payload.data['notificationType' ] as String ? ;
388- final contentType = n.payload.data['contentType' ] as String ? ;
389-
390- if (notificationType ==
391- PushNotificationSubscriptionDeliveryType .dailyDigest.name ||
392- notificationType ==
393- PushNotificationSubscriptionDeliveryType .weeklyRoundup.name ||
394- contentType == 'digest' ) {
395- digestList.add (n);
396- } else {
397- // All other types go to breaking news.
398- breakingNewsList.add (n);
399- }
400- }
313+ final isBreakingNewsFilter = filter == _breakingNewsFilter;
314+
315+ return _inAppNotificationRepository
316+ .readAll (
317+ userId: userId,
318+ filter: filter,
319+ pagination: PaginationOptions (
320+ limit: _notificationsFetchLimit,
321+ cursor: cursor,
322+ ),
323+ sort: [const SortOption ('createdAt' , SortOrder .desc)],
324+ )
325+ .then ((response) {
326+ final newNotifications = response.items;
327+ final nextCursor = response.cursor;
328+ final hasMore = response.hasMore;
329+
330+ if (isBreakingNewsFilter) {
331+ emit (
332+ state.copyWith (
333+ breakingNewsNotifications: isInitialFetch
334+ ? newNotifications
335+ : [...state.breakingNewsNotifications, ...newNotifications],
336+ breakingNewsHasMore: hasMore,
337+ breakingNewsCursor: nextCursor,
338+ ),
339+ );
340+ } else {
341+ emit (
342+ state.copyWith (
343+ digestNotifications: isInitialFetch
344+ ? newNotifications
345+ : [...state.digestNotifications, ...newNotifications],
346+ digestHasMore: hasMore,
347+ digestCursor: nextCursor,
348+ ),
349+ );
350+ }
351+ })
352+ .catchError ((Object e, StackTrace s) {
353+ _logger.severe ('Failed to fetch notifications.' , e, s);
354+ final httpException = e is HttpException
355+ ? e
356+ : UnknownException (e.toString ());
357+ emit (
358+ state.copyWith (
359+ status: InAppNotificationCenterStatus .failure,
360+ error: httpException,
361+ ),
362+ );
363+ });
401364 }
365+
366+ /// Filter for "Breaking News" notifications.
367+ ///
368+ /// This filter uses the `$nin` (not in) operator to exclude notifications
369+ /// that are explicitly typed as digests. All other notifications are
370+ /// considered "breaking news" for the purpose of this tab.
371+ Map <String , dynamic > get _breakingNewsFilter => {
372+ 'payload.data.notificationType' : {
373+ r'$nin' : [
374+ PushNotificationSubscriptionDeliveryType .dailyDigest.name,
375+ PushNotificationSubscriptionDeliveryType .weeklyRoundup.name,
376+ ],
377+ },
378+ };
379+
380+ /// Filter for "Digests" notifications.
381+ ///
382+ /// This filter uses the `$in` operator to select notifications that are
383+ /// explicitly typed as either a daily or weekly digest.
384+ Map <String , dynamic > get _digestFilter => {
385+ 'payload.data.notificationType' : {
386+ r'$in' : [
387+ PushNotificationSubscriptionDeliveryType .dailyDigest.name,
388+ PushNotificationSubscriptionDeliveryType .weeklyRoundup.name,
389+ ],
390+ },
391+ };
402392}
0 commit comments