diff --git a/lib/account/bloc/in_app_notification_center_bloc.dart b/lib/account/bloc/in_app_notification_center_bloc.dart index 6d54b468..f2cd2f04 100644 --- a/lib/account/bloc/in_app_notification_center_bloc.dart +++ b/lib/account/bloc/in_app_notification_center_bloc.dart @@ -43,6 +43,7 @@ class InAppNotificationCenterBloc _onFetchMoreRequested, transformer: droppable(), ); + on(_onReadItemsDeleted); } /// The number of notifications to fetch per page. @@ -334,6 +335,74 @@ class InAppNotificationCenterBloc } } + /// Handles deleting all read notifications in the current tab. + Future _onReadItemsDeleted( + InAppNotificationCenterReadItemsDeleted event, + Emitter 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> _fetchNotifications({ required String userId, diff --git a/lib/account/bloc/in_app_notification_center_event.dart b/lib/account/bloc/in_app_notification_center_event.dart index e791a0d3..7ae04d26 100644 --- a/lib/account/bloc/in_app_notification_center_event.dart +++ b/lib/account/bloc/in_app_notification_center_event.dart @@ -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(); +} diff --git a/lib/account/bloc/in_app_notification_center_state.dart b/lib/account/bloc/in_app_notification_center_state.dart index 2aba115a..ba3a1842 100644 --- a/lib/account/bloc/in_app_notification_center_state.dart +++ b/lib/account/bloc/in_app_notification_center_state.dart @@ -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} @@ -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 get props => [ status, diff --git a/lib/account/view/in_app_notification_center_page.dart b/lib/account/view/in_app_notification_center_page.dart index c3bf325d..e11c28f7 100644 --- a/lib/account/view/in_app_notification_center_page.dart +++ b/lib/account/view/in_app_notification_center_page.dart @@ -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().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().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( + 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().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().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().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()), + ), + ], ), + ); + }, ); } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cb26e1b2..d3ce4c1f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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 diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 572c9e6b..000eb612 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1326,4 +1326,15 @@ class AppLocalizationsAr extends AppLocalizations { @override String get notificationCenterTabDigests => 'الملخصات'; + + @override + String get deleteReadNotificationsButtonTooltip => + 'حذف جميع الإشعارات المقروءة'; + + @override + String get deleteReadNotificationsDialogContent => + 'هل أنت متأكد أنك تريد حذف جميع الإشعارات المقروءة في علامة التبويب هذه؟ لا يمكن التراجع عن هذا الإجراء.'; + + @override + String get deleteButtonLabel => 'حذف'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e0af59c4..1b38de71 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1330,4 +1330,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationCenterTabDigests => 'Digests'; + + @override + String get deleteReadNotificationsButtonTooltip => + 'Delete all read notifications'; + + @override + String get deleteReadNotificationsDialogContent => + 'Are you sure you want to delete all read notifications in this tab? This action cannot be undone.'; + + @override + String get deleteButtonLabel => 'Delete'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index a0a4c621..74732154 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1721,5 +1721,17 @@ "notificationCenterTabDigests": "الملخصات", "@notificationCenterTabDigests": { "description": "Label for the 'Digests' tab in the notification center." + }, + "deleteReadNotificationsButtonTooltip": "حذف جميع الإشعارات المقروءة", + "@deleteReadNotificationsButtonTooltip": { + "description": "Tooltip for the button to delete all read notifications in the current tab." + }, + "deleteReadNotificationsDialogContent": "هل أنت متأكد أنك تريد حذف جميع الإشعارات المقروءة في علامة التبويب هذه؟ لا يمكن التراجع عن هذا الإجراء.", + "@deleteReadNotificationsDialogContent": { + "description": "The main text in the dialog confirming the deletion of read notifications." + }, + "deleteButtonLabel": "حذف", + "@deleteButtonLabel": { + "description": "Generic label for a delete button." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d01cab85..5fdd68e4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1721,5 +1721,17 @@ "notificationCenterTabDigests": "Digests", "@notificationCenterTabDigests": { "description": "Label for the 'Digests' tab in the notification center." + }, + "deleteReadNotificationsButtonTooltip": "Delete all read notifications", + "@deleteReadNotificationsButtonTooltip": { + "description": "Tooltip for the button to delete all read notifications in the current tab." + }, + "deleteReadNotificationsDialogContent": "Are you sure you want to delete all read notifications in this tab? This action cannot be undone.", + "@deleteReadNotificationsDialogContent": { + "description": "The main text in the dialog confirming the deletion of read notifications." + }, + "deleteButtonLabel": "Delete", + "@deleteButtonLabel": { + "description": "Generic label for a delete button." } } \ No newline at end of file