Skip to content

Commit b097e98

Browse files
committed
refactor(account): improve in-app notification fetching logic
- Separate fetching logic for "Breaking News" and "Digests" tabs - Implement concurrent fetching for both tabs on initial load - Use filtering to request specific notification types from the backend - Simplify notification fetching and error handling - Add droppable transformer to subscription request event handling
1 parent 5c279f7 commit b097e98

File tree

1 file changed

+124
-134
lines changed

1 file changed

+124
-134
lines changed

lib/account/bloc/in_app_notification_center_bloc.dart

Lines changed: 124 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)