Skip to content

Commit a1deefa

Browse files
committed
feat(account): add pagination to in-app notifications
- Implement fetch more functionality for breaking news and digest tabs - Add loading state for pagination - Update state to include hasMore and cursor for each notification type - Refactor notification filtering into a separate method - Use bloc_concurrency's droppable transformer for fetch more events
1 parent a1c530f commit a1deefa

File tree

1 file changed

+131
-18
lines changed

1 file changed

+131
-18
lines changed

lib/account/bloc/in_app_notification_center_bloc.dart

Lines changed: 131 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:bloc_concurrency/bloc_concurrency.dart';
34
import 'package:bloc/bloc.dart';
45
import 'package:collection/collection.dart';
56
import 'package:core/core.dart';
@@ -35,13 +36,19 @@ class InAppNotificationCenterBloc
3536
on<InAppNotificationCenterMarkAllAsRead>(_onMarkAllAsRead);
3637
on<InAppNotificationCenterTabChanged>(_onTabChanged);
3738
on<InAppNotificationCenterMarkOneAsRead>(_onMarkOneAsRead);
39+
on<InAppNotificationCenterFetchMoreRequested>(
40+
_onFetchMoreRequested,
41+
transformer: droppable(),
42+
);
3843
}
3944

4045
final DataRepository<InAppNotification> _inAppNotificationRepository;
4146
final AppBloc _appBloc;
4247
final Logger _logger;
4348

4449
/// Handles the request to load all notifications for the current user.
50+
/// This now only fetches the first page of notifications. Subsequent pages
51+
/// are loaded by [_onFetchMoreRequested].
4552
Future<void> _onSubscriptionRequested(
4653
InAppNotificationCenterSubscriptionRequested event,
4754
Emitter<InAppNotificationCenterState> emit,
@@ -58,38 +65,37 @@ class InAppNotificationCenterBloc
5865
try {
5966
final response = await _inAppNotificationRepository.readAll(
6067
userId: userId,
68+
// Fetch the first page with a defined limit.
69+
pagination: const PaginationOptions(limit: 20),
6170
sort: [const SortOption('createdAt', SortOrder.desc)],
6271
);
6372

6473
final allNotifications = response.items;
6574

6675
final breakingNews = <InAppNotification>[];
6776
final digests = <InAppNotification>[];
77+
_filterAndAddNotifications(
78+
notifications: allNotifications,
79+
breakingNewsList: breakingNews,
80+
digestList: digests,
81+
);
6882

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-
}
83+
// Since we are fetching all notifications together and then filtering,
84+
// the pagination cursor and hasMore status will be the same for both lists.
85+
// This assumes the backend doesn't support filtering by notification type
86+
// in the query itself.
87+
final hasMore = response.hasMore;
88+
final cursor = response.cursor;
8789

8890
emit(
8991
state.copyWith(
9092
status: InAppNotificationCenterStatus.success,
9193
breakingNewsNotifications: breakingNews,
9294
digestNotifications: digests,
95+
breakingNewsHasMore: hasMore,
96+
breakingNewsCursor: cursor,
97+
digestHasMore: hasMore,
98+
digestCursor: cursor,
9399
),
94100
);
95101
} on HttpException catch (e, s) {
@@ -112,6 +118,89 @@ class InAppNotificationCenterBloc
112118
}
113119
}
114120

121+
/// Handles fetching the next page of notifications when the user scrolls.
122+
Future<void> _onFetchMoreRequested(
123+
InAppNotificationCenterFetchMoreRequested event,
124+
Emitter<InAppNotificationCenterState> emit,
125+
) async {
126+
final isBreakingNewsTab = state.currentTabIndex == 0;
127+
final hasMore = isBreakingNewsTab
128+
? state.breakingNewsHasMore
129+
: state.digestHasMore;
130+
131+
if (state.status == InAppNotificationCenterStatus.loadingMore || !hasMore) {
132+
return;
133+
}
134+
135+
emit(state.copyWith(status: InAppNotificationCenterStatus.loadingMore));
136+
137+
final userId = _appBloc.state.user?.id;
138+
if (userId == null) {
139+
_logger.warning(
140+
'Cannot fetch more notifications: user is not logged in.',
141+
);
142+
emit(state.copyWith(status: InAppNotificationCenterStatus.failure));
143+
return;
144+
}
145+
146+
final cursor = isBreakingNewsTab
147+
? state.breakingNewsCursor
148+
: state.digestCursor;
149+
150+
try {
151+
final response = await _inAppNotificationRepository.readAll(
152+
userId: userId,
153+
pagination: PaginationOptions(limit: 20, cursor: cursor),
154+
sort: [const SortOption('createdAt', SortOrder.desc)],
155+
);
156+
157+
final newNotifications = response.items;
158+
final newBreakingNews = <InAppNotification>[];
159+
final newDigests = <InAppNotification>[];
160+
161+
_filterAndAddNotifications(
162+
notifications: newNotifications,
163+
breakingNewsList: newBreakingNews,
164+
digestList: newDigests,
165+
);
166+
167+
final nextCursor = response.cursor;
168+
final nextHasMore = response.hasMore;
169+
170+
emit(
171+
state.copyWith(
172+
status: InAppNotificationCenterStatus.success,
173+
breakingNewsNotifications: [
174+
...state.breakingNewsNotifications,
175+
...newBreakingNews,
176+
],
177+
digestNotifications: [...state.digestNotifications, ...newDigests],
178+
breakingNewsHasMore: nextHasMore,
179+
breakingNewsCursor: nextCursor,
180+
digestHasMore: nextHasMore,
181+
digestCursor: nextCursor,
182+
),
183+
);
184+
} on HttpException catch (e, s) {
185+
_logger.severe('Failed to fetch more in-app notifications.', e, s);
186+
emit(
187+
state.copyWith(status: InAppNotificationCenterStatus.failure, error: e),
188+
);
189+
} catch (e, s) {
190+
_logger.severe(
191+
'An unexpected error occurred while fetching more in-app notifications.',
192+
e,
193+
s,
194+
);
195+
emit(
196+
state.copyWith(
197+
status: InAppNotificationCenterStatus.failure,
198+
error: UnknownException(e.toString()),
199+
),
200+
);
201+
}
202+
}
203+
115204
/// Handles the event to change the active tab.
116205
Future<void> _onTabChanged(
117206
InAppNotificationCenterTabChanged event,
@@ -280,4 +369,28 @@ class InAppNotificationCenterBloc
280369
);
281370
}
282371
}
372+
373+
/// A helper method to filter a list of notifications into "Breaking News"
374+
/// and "Digests" categories and add them to the provided lists.
375+
void _filterAndAddNotifications({
376+
required List<InAppNotification> notifications,
377+
required List<InAppNotification> breakingNewsList,
378+
required List<InAppNotification> digestList,
379+
}) {
380+
for (final n in notifications) {
381+
final notificationType = n.payload.data['notificationType'] as String?;
382+
final contentType = n.payload.data['contentType'] as String?;
383+
384+
if (notificationType ==
385+
PushNotificationSubscriptionDeliveryType.dailyDigest.name ||
386+
notificationType ==
387+
PushNotificationSubscriptionDeliveryType.weeklyRoundup.name ||
388+
contentType == 'digest') {
389+
digestList.add(n);
390+
} else {
391+
// All other types go to breaking news.
392+
breakingNewsList.add(n);
393+
}
394+
}
395+
}
283396
}

0 commit comments

Comments
 (0)