Skip to content

Commit a641bd4

Browse files
authored
Merge pull request #218 from flutter-news-app-full-source-code/feat/Delete-Read-Notifications
Feat/delete read notifications
2 parents 2d5b61c + f3cb3ec commit a641bd4

9 files changed

+302
-90
lines changed

lib/account/bloc/in_app_notification_center_bloc.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class InAppNotificationCenterBloc
4343
_onFetchMoreRequested,
4444
transformer: droppable(),
4545
);
46+
on<InAppNotificationCenterReadItemsDeleted>(_onReadItemsDeleted);
4647
}
4748

4849
/// The number of notifications to fetch per page.
@@ -334,6 +335,74 @@ class InAppNotificationCenterBloc
334335
}
335336
}
336337

338+
/// Handles deleting all read notifications in the current tab.
339+
Future<void> _onReadItemsDeleted(
340+
InAppNotificationCenterReadItemsDeleted event,
341+
Emitter<InAppNotificationCenterState> emit,
342+
) async {
343+
final userId = _appBloc.state.user!.id;
344+
try {
345+
emit(state.copyWith(status: InAppNotificationCenterStatus.deleting));
346+
347+
final isBreakingNewsTab = state.currentTabIndex == 0;
348+
final notificationsForTab = isBreakingNewsTab
349+
? state.breakingNewsNotifications
350+
: state.digestNotifications;
351+
352+
final readNotifications = notificationsForTab
353+
.where((n) => n.isRead)
354+
.toList();
355+
356+
if (readNotifications.isEmpty) {
357+
_logger.info('No read notifications to delete in the current tab.');
358+
emit(state.copyWith(status: InAppNotificationCenterStatus.success));
359+
return;
360+
}
361+
362+
final idsToDelete = readNotifications.map((n) => n.id).toList();
363+
364+
_logger.info('Deleting ${idsToDelete.length} read notifications...');
365+
366+
await Future.wait(
367+
idsToDelete.map(
368+
(id) => _inAppNotificationRepository.delete(id: id, userId: userId),
369+
),
370+
);
371+
372+
_logger.info('Deletion successful. Refreshing notification list.');
373+
374+
// After deletion, re-fetch the current tab's data to ensure consistency.
375+
final filter = isBreakingNewsTab ? _breakingNewsFilter : _digestFilter;
376+
final response = await _fetchNotifications(
377+
userId: userId,
378+
filter: filter,
379+
);
380+
381+
// Update the state with the refreshed list.
382+
if (isBreakingNewsTab) {
383+
emit(
384+
state.copyWith(
385+
breakingNewsNotifications: response.items,
386+
breakingNewsHasMore: response.hasMore,
387+
breakingNewsCursor: response.cursor,
388+
),
389+
);
390+
} else {
391+
emit(
392+
state.copyWith(
393+
digestNotifications: response.items,
394+
digestHasMore: response.hasMore,
395+
digestCursor: response.cursor,
396+
),
397+
);
398+
}
399+
400+
emit(state.copyWith(status: InAppNotificationCenterStatus.success));
401+
} catch (error, stackTrace) {
402+
_handleFetchError(emit, error, stackTrace);
403+
}
404+
}
405+
337406
/// A generic method to fetch notifications based on a filter.
338407
Future<PaginatedResponse<InAppNotification>> _fetchNotifications({
339408
required String userId,

lib/account/bloc/in_app_notification_center_event.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,10 @@ class InAppNotificationCenterFetchMoreRequested
6262
extends InAppNotificationCenterEvent {
6363
const InAppNotificationCenterFetchMoreRequested();
6464
}
65+
66+
/// Dispatched when the user requests to delete all read items in the
67+
/// currently active tab.
68+
class InAppNotificationCenterReadItemsDeleted
69+
extends InAppNotificationCenterEvent {
70+
const InAppNotificationCenterReadItemsDeleted();
71+
}

lib/account/bloc/in_app_notification_center_state.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ enum InAppNotificationCenterStatus {
1616

1717
/// The state when an error has occurred.
1818
failure,
19+
20+
/// The state when read notifications are being deleted.
21+
deleting,
1922
}
2023

2124
/// {@template in_app_notification_center_state}
@@ -69,6 +72,16 @@ class InAppNotificationCenterState extends Equatable {
6972
/// The cursor for fetching the next page of digest notifications.
7073
final String? digestCursor;
7174

75+
/// A convenience getter to determine if the current tab has any read items.
76+
bool get hasReadItemsInCurrentTab {
77+
final isBreakingNewsTab = currentTabIndex == 0;
78+
if (isBreakingNewsTab) {
79+
return breakingNewsNotifications.any((n) => n.isRead);
80+
} else {
81+
return digestNotifications.any((n) => n.isRead);
82+
}
83+
}
84+
7285
@override
7386
List<Object> get props => [
7487
status,

lib/account/view/in_app_notification_center_page.dart

Lines changed: 149 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -52,100 +52,159 @@ class _InAppNotificationCenterPageState
5252
Widget build(BuildContext context) {
5353
final l10n = AppLocalizationsX(context).l10n;
5454

55-
return Scaffold(
56-
appBar: AppBar(
57-
title: Text(l10n.notificationCenterPageTitle),
58-
actions: [
59-
BlocBuilder<
60-
InAppNotificationCenterBloc,
61-
InAppNotificationCenterState
62-
>(
63-
builder: (context, state) {
64-
final hasUnread = state.notifications.any((n) => !n.isRead);
65-
return IconButton(
66-
onPressed: hasUnread
67-
? () {
68-
context.read<InAppNotificationCenterBloc>().add(
69-
const InAppNotificationCenterMarkAllAsRead(),
70-
);
71-
}
72-
: null,
73-
icon: const Icon(Icons.done_all),
74-
);
75-
},
76-
),
77-
],
78-
bottom: TabBar(
79-
controller: _tabController,
80-
tabs: [
81-
Tab(text: l10n.notificationCenterTabBreakingNews),
82-
Tab(text: l10n.notificationCenterTabDigests),
83-
],
84-
),
85-
),
86-
body:
87-
BlocConsumer<
88-
InAppNotificationCenterBloc,
89-
InAppNotificationCenterState
90-
>(
91-
listener: (context, state) {
92-
if (state.status == InAppNotificationCenterStatus.failure &&
93-
state.error != null) {
94-
ScaffoldMessenger.of(context)
95-
..hideCurrentSnackBar()
96-
..showSnackBar(
97-
SnackBar(
98-
content: Text(state.error!.message),
99-
backgroundColor: Theme.of(context).colorScheme.error,
55+
return BlocBuilder<
56+
InAppNotificationCenterBloc,
57+
InAppNotificationCenterState
58+
>(
59+
builder: (context, state) {
60+
final isDeleting =
61+
state.status == InAppNotificationCenterStatus.deleting;
62+
63+
return WillPopScope(
64+
onWillPop: () async => !isDeleting,
65+
child: Stack(
66+
children: [
67+
Scaffold(
68+
appBar: AppBar(
69+
title: Text(l10n.notificationCenterPageTitle),
70+
actions: [
71+
IconButton(
72+
onPressed:
73+
!isDeleting &&
74+
state.notifications.any((n) => !n.isRead)
75+
? () {
76+
context.read<InAppNotificationCenterBloc>().add(
77+
const InAppNotificationCenterMarkAllAsRead(),
78+
);
79+
}
80+
: null,
81+
icon: const Icon(Icons.done_all),
82+
tooltip: l10n.notificationCenterMarkAllAsReadButton,
10083
),
101-
);
102-
}
103-
},
104-
builder: (context, state) {
105-
if (state.status == InAppNotificationCenterStatus.loading &&
106-
state.breakingNewsNotifications.isEmpty &&
107-
state.digestNotifications.isEmpty) {
108-
return LoadingStateWidget(
109-
icon: Icons.notifications_none_outlined,
110-
headline: l10n.notificationCenterLoadingHeadline,
111-
subheadline: l10n.notificationCenterLoadingSubheadline,
112-
);
113-
}
84+
IconButton(
85+
tooltip: l10n.deleteReadNotificationsButtonTooltip,
86+
icon: const Icon(Icons.delete_sweep_outlined),
87+
onPressed: !isDeleting && state.hasReadItemsInCurrentTab
88+
? () async {
89+
final confirmed = await showDialog<bool>(
90+
context: context,
91+
builder: (context) => AlertDialog(
92+
title: Text(
93+
l10n.deleteConfirmationDialogTitle,
94+
),
95+
content: Text(
96+
l10n.deleteReadNotificationsDialogContent,
97+
),
98+
actions: [
99+
TextButton(
100+
onPressed: () =>
101+
Navigator.pop(context, false),
102+
child: Text(l10n.cancelButtonLabel),
103+
),
104+
TextButton(
105+
onPressed: () =>
106+
Navigator.pop(context, true),
107+
child: Text(l10n.deleteButtonLabel),
108+
),
109+
],
110+
),
111+
);
112+
if (confirmed == true && context.mounted) {
113+
context.read<InAppNotificationCenterBloc>().add(
114+
const InAppNotificationCenterReadItemsDeleted(),
115+
);
116+
}
117+
}
118+
: null,
119+
),
120+
],
121+
bottom: TabBar(
122+
controller: _tabController,
123+
tabs: [
124+
Tab(text: l10n.notificationCenterTabBreakingNews),
125+
Tab(text: l10n.notificationCenterTabDigests),
126+
],
127+
),
128+
),
129+
body:
130+
BlocConsumer<
131+
InAppNotificationCenterBloc,
132+
InAppNotificationCenterState
133+
>(
134+
listener: (context, state) {
135+
if (state.status ==
136+
InAppNotificationCenterStatus.failure &&
137+
state.error != null) {
138+
ScaffoldMessenger.of(context)
139+
..hideCurrentSnackBar()
140+
..showSnackBar(
141+
SnackBar(
142+
content: Text(state.error!.message),
143+
backgroundColor: Theme.of(
144+
context,
145+
).colorScheme.error,
146+
),
147+
);
148+
}
149+
},
150+
builder: (context, state) {
151+
if (state.status ==
152+
InAppNotificationCenterStatus.loading &&
153+
state.breakingNewsNotifications.isEmpty &&
154+
state.digestNotifications.isEmpty) {
155+
return LoadingStateWidget(
156+
icon: Icons.notifications_none_outlined,
157+
headline: l10n.notificationCenterLoadingHeadline,
158+
subheadline:
159+
l10n.notificationCenterLoadingSubheadline,
160+
);
161+
}
114162

115-
if (state.status == InAppNotificationCenterStatus.failure &&
116-
state.breakingNewsNotifications.isEmpty &&
117-
state.digestNotifications.isEmpty) {
118-
return FailureStateWidget(
119-
exception:
120-
state.error ??
121-
OperationFailedException(
122-
l10n.notificationCenterFailureHeadline,
123-
),
124-
onRetry: () {
125-
context.read<InAppNotificationCenterBloc>().add(
126-
const InAppNotificationCenterSubscriptionRequested(),
127-
);
128-
},
129-
);
130-
}
163+
if (state.status ==
164+
InAppNotificationCenterStatus.failure &&
165+
state.breakingNewsNotifications.isEmpty &&
166+
state.digestNotifications.isEmpty) {
167+
return FailureStateWidget(
168+
exception:
169+
state.error ??
170+
OperationFailedException(
171+
l10n.notificationCenterFailureHeadline,
172+
),
173+
onRetry: () {
174+
context.read<InAppNotificationCenterBloc>().add(
175+
const InAppNotificationCenterSubscriptionRequested(),
176+
);
177+
},
178+
);
179+
}
131180

132-
return TabBarView(
133-
controller: _tabController,
134-
children: [
135-
_NotificationList(
136-
status: state.status,
137-
notifications: state.breakingNewsNotifications,
138-
hasMore: state.breakingNewsHasMore,
139-
),
140-
_NotificationList(
141-
status: state.status,
142-
notifications: state.digestNotifications,
143-
hasMore: state.digestHasMore,
144-
),
145-
],
146-
);
147-
},
181+
return TabBarView(
182+
controller: _tabController,
183+
children: [
184+
_NotificationList(
185+
status: state.status,
186+
notifications: state.breakingNewsNotifications,
187+
hasMore: state.breakingNewsHasMore,
188+
),
189+
_NotificationList(
190+
status: state.status,
191+
notifications: state.digestNotifications,
192+
hasMore: state.digestHasMore,
193+
),
194+
],
195+
);
196+
},
197+
),
198+
),
199+
if (isDeleting)
200+
ColoredBox(
201+
color: Colors.black.withOpacity(0.5),
202+
child: const Center(child: CircularProgressIndicator()),
203+
),
204+
],
148205
),
206+
);
207+
},
149208
);
150209
}
151210
}

lib/l10n/app_localizations.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2521,6 +2521,24 @@ abstract class AppLocalizations {
25212521
/// In en, this message translates to:
25222522
/// **'Digests'**
25232523
String get notificationCenterTabDigests;
2524+
2525+
/// Tooltip for the button to delete all read notifications in the current tab.
2526+
///
2527+
/// In en, this message translates to:
2528+
/// **'Delete all read notifications'**
2529+
String get deleteReadNotificationsButtonTooltip;
2530+
2531+
/// The main text in the dialog confirming the deletion of read notifications.
2532+
///
2533+
/// In en, this message translates to:
2534+
/// **'Are you sure you want to delete all read notifications in this tab? This action cannot be undone.'**
2535+
String get deleteReadNotificationsDialogContent;
2536+
2537+
/// Generic label for a delete button.
2538+
///
2539+
/// In en, this message translates to:
2540+
/// **'Delete'**
2541+
String get deleteButtonLabel;
25242542
}
25252543

25262544
class _AppLocalizationsDelegate

0 commit comments

Comments
 (0)