Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions lib/account/bloc/in_app_notification_center_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class InAppNotificationCenterBloc
_onFetchMoreRequested,
transformer: droppable(),
);
on<InAppNotificationCenterReadItemsDeleted>(_onReadItemsDeleted);
}

/// The number of notifications to fetch per page.
Expand Down Expand Up @@ -334,6 +335,74 @@ class InAppNotificationCenterBloc
}
}

/// Handles deleting all read notifications in the current tab.
Future<void> _onReadItemsDeleted(
InAppNotificationCenterReadItemsDeleted event,
Emitter<InAppNotificationCenterState> emit,
) async {
final userId = _appBloc.state.user!.id;
try {
emit(state.copyWith(status: InAppNotificationCenterStatus.deleting));

final isBreakingNewsTab = state.currentTabIndex == 0;
final notificationsForTab = isBreakingNewsTab
? state.breakingNewsNotifications
: state.digestNotifications;

final readNotifications = notificationsForTab
.where((n) => n.isRead)
.toList();

if (readNotifications.isEmpty) {
_logger.info('No read notifications to delete in the current tab.');
emit(state.copyWith(status: InAppNotificationCenterStatus.success));
return;
}

final idsToDelete = readNotifications.map((n) => n.id).toList();

_logger.info('Deleting ${idsToDelete.length} read notifications...');

await Future.wait(
idsToDelete.map(
(id) => _inAppNotificationRepository.delete(id: id, userId: userId),
),
);

_logger.info('Deletion successful. Refreshing notification list.');

// After deletion, re-fetch the current tab's data to ensure consistency.
final filter = isBreakingNewsTab ? _breakingNewsFilter : _digestFilter;
final response = await _fetchNotifications(
userId: userId,
filter: filter,
);

// Update the state with the refreshed list.
if (isBreakingNewsTab) {
emit(
state.copyWith(
breakingNewsNotifications: response.items,
breakingNewsHasMore: response.hasMore,
breakingNewsCursor: response.cursor,
),
);
} else {
emit(
state.copyWith(
digestNotifications: response.items,
digestHasMore: response.hasMore,
digestCursor: response.cursor,
),
);
}

emit(state.copyWith(status: InAppNotificationCenterStatus.success));
} catch (error, stackTrace) {
_handleFetchError(emit, error, stackTrace);
}
}

/// A generic method to fetch notifications based on a filter.
Future<PaginatedResponse<InAppNotification>> _fetchNotifications({
required String userId,
Expand Down
7 changes: 7 additions & 0 deletions lib/account/bloc/in_app_notification_center_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ class InAppNotificationCenterFetchMoreRequested
extends InAppNotificationCenterEvent {
const InAppNotificationCenterFetchMoreRequested();
}

/// Dispatched when the user requests to delete all read items in the
/// currently active tab.
class InAppNotificationCenterReadItemsDeleted
extends InAppNotificationCenterEvent {
const InAppNotificationCenterReadItemsDeleted();
}
13 changes: 13 additions & 0 deletions lib/account/bloc/in_app_notification_center_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ enum InAppNotificationCenterStatus {

/// The state when an error has occurred.
failure,

/// The state when read notifications are being deleted.
deleting,
}

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

/// A convenience getter to determine if the current tab has any read items.
bool get hasReadItemsInCurrentTab {
final isBreakingNewsTab = currentTabIndex == 0;
if (isBreakingNewsTab) {
return breakingNewsNotifications.any((n) => n.isRead);
} else {
return digestNotifications.any((n) => n.isRead);
}
}

@override
List<Object> get props => [
status,
Expand Down
239 changes: 149 additions & 90 deletions lib/account/view/in_app_notification_center_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,100 +52,159 @@ class _InAppNotificationCenterPageState
Widget build(BuildContext context) {
final l10n = AppLocalizationsX(context).l10n;

return Scaffold(
appBar: AppBar(
title: Text(l10n.notificationCenterPageTitle),
actions: [
BlocBuilder<
InAppNotificationCenterBloc,
InAppNotificationCenterState
>(
builder: (context, state) {
final hasUnread = state.notifications.any((n) => !n.isRead);
return IconButton(
onPressed: hasUnread
? () {
context.read<InAppNotificationCenterBloc>().add(
const InAppNotificationCenterMarkAllAsRead(),
);
}
: null,
icon: const Icon(Icons.done_all),
);
},
),
],
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: l10n.notificationCenterTabBreakingNews),
Tab(text: l10n.notificationCenterTabDigests),
],
),
),
body:
BlocConsumer<
InAppNotificationCenterBloc,
InAppNotificationCenterState
>(
listener: (context, state) {
if (state.status == InAppNotificationCenterStatus.failure &&
state.error != null) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.error!.message),
backgroundColor: Theme.of(context).colorScheme.error,
return BlocBuilder<
InAppNotificationCenterBloc,
InAppNotificationCenterState
>(
builder: (context, state) {
final isDeleting =
state.status == InAppNotificationCenterStatus.deleting;

return WillPopScope(
onWillPop: () async => !isDeleting,
child: Stack(
children: [
Scaffold(
appBar: AppBar(
title: Text(l10n.notificationCenterPageTitle),
actions: [
IconButton(
onPressed:
!isDeleting &&
state.notifications.any((n) => !n.isRead)
? () {
context.read<InAppNotificationCenterBloc>().add(
const InAppNotificationCenterMarkAllAsRead(),
);
}
: null,
icon: const Icon(Icons.done_all),
tooltip: l10n.notificationCenterMarkAllAsReadButton,
),
);
}
},
builder: (context, state) {
if (state.status == InAppNotificationCenterStatus.loading &&
state.breakingNewsNotifications.isEmpty &&
state.digestNotifications.isEmpty) {
return LoadingStateWidget(
icon: Icons.notifications_none_outlined,
headline: l10n.notificationCenterLoadingHeadline,
subheadline: l10n.notificationCenterLoadingSubheadline,
);
}
IconButton(
tooltip: l10n.deleteReadNotificationsButtonTooltip,
icon: const Icon(Icons.delete_sweep_outlined),
onPressed: !isDeleting && state.hasReadItemsInCurrentTab
? () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(
l10n.deleteConfirmationDialogTitle,
),
content: Text(
l10n.deleteReadNotificationsDialogContent,
),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(context, false),
child: Text(l10n.cancelButtonLabel),
),
TextButton(
onPressed: () =>
Navigator.pop(context, true),
child: Text(l10n.deleteButtonLabel),
),
],
),
);
if (confirmed == true && context.mounted) {
context.read<InAppNotificationCenterBloc>().add(
const InAppNotificationCenterReadItemsDeleted(),
);
}
}
: null,
),
],
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: l10n.notificationCenterTabBreakingNews),
Tab(text: l10n.notificationCenterTabDigests),
],
),
),
body:
BlocConsumer<
InAppNotificationCenterBloc,
InAppNotificationCenterState
>(
listener: (context, state) {
if (state.status ==
InAppNotificationCenterStatus.failure &&
state.error != null) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.error!.message),
backgroundColor: Theme.of(
context,
).colorScheme.error,
),
);
}
},
builder: (context, state) {
if (state.status ==
InAppNotificationCenterStatus.loading &&
state.breakingNewsNotifications.isEmpty &&
state.digestNotifications.isEmpty) {
return LoadingStateWidget(
icon: Icons.notifications_none_outlined,
headline: l10n.notificationCenterLoadingHeadline,
subheadline:
l10n.notificationCenterLoadingSubheadline,
);
}

if (state.status == InAppNotificationCenterStatus.failure &&
state.breakingNewsNotifications.isEmpty &&
state.digestNotifications.isEmpty) {
return FailureStateWidget(
exception:
state.error ??
OperationFailedException(
l10n.notificationCenterFailureHeadline,
),
onRetry: () {
context.read<InAppNotificationCenterBloc>().add(
const InAppNotificationCenterSubscriptionRequested(),
);
},
);
}
if (state.status ==
InAppNotificationCenterStatus.failure &&
state.breakingNewsNotifications.isEmpty &&
state.digestNotifications.isEmpty) {
return FailureStateWidget(
exception:
state.error ??
OperationFailedException(
l10n.notificationCenterFailureHeadline,
),
onRetry: () {
context.read<InAppNotificationCenterBloc>().add(
const InAppNotificationCenterSubscriptionRequested(),
);
},
);
}

return TabBarView(
controller: _tabController,
children: [
_NotificationList(
status: state.status,
notifications: state.breakingNewsNotifications,
hasMore: state.breakingNewsHasMore,
),
_NotificationList(
status: state.status,
notifications: state.digestNotifications,
hasMore: state.digestHasMore,
),
],
);
},
return TabBarView(
controller: _tabController,
children: [
_NotificationList(
status: state.status,
notifications: state.breakingNewsNotifications,
hasMore: state.breakingNewsHasMore,
),
_NotificationList(
status: state.status,
notifications: state.digestNotifications,
hasMore: state.digestHasMore,
),
],
);
},
),
),
if (isDeleting)
ColoredBox(
color: Colors.black.withOpacity(0.5),
child: const Center(child: CircularProgressIndicator()),
),
],
),
);
},
);
}
}
Expand Down
18 changes: 18 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,24 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Digests'**
String get notificationCenterTabDigests;

/// Tooltip for the button to delete all read notifications in the current tab.
///
/// In en, this message translates to:
/// **'Delete all read notifications'**
String get deleteReadNotificationsButtonTooltip;

/// The main text in the dialog confirming the deletion of read notifications.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete all read notifications in this tab? This action cannot be undone.'**
String get deleteReadNotificationsDialogContent;

/// Generic label for a delete button.
///
/// In en, this message translates to:
/// **'Delete'**
String get deleteButtonLabel;
}

class _AppLocalizationsDelegate
Expand Down
Loading
Loading