diff --git a/lib/app_configuration/widgets/article_ad_settings_form.dart b/lib/app_configuration/widgets/article_ad_settings_form.dart index 274aae00..03a2bb2b 100644 --- a/lib/app_configuration/widgets/article_ad_settings_form.dart +++ b/lib/app_configuration/widgets/article_ad_settings_form.dart @@ -2,9 +2,9 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/banner_ad_shape_l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/in_article_ad_slot_type_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template article_ad_settings_form} diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart index 4e1cfaea..6e01c649 100644 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart +++ b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart @@ -4,64 +4,58 @@ import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; -import 'package:ui_kit/ui_kit.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; part 'archive_local_ads_event.dart'; part 'archive_local_ads_state.dart'; +/// {@template archive_local_ads_bloc} +/// A BLoC responsible for managing the state of archived local ads. +/// +/// It handles loading, restoring, and permanently deleting archived local ads, +/// leveraging the [PendingDeletionsService] for undo functionality. +/// {@endtemplate} class ArchiveLocalAdsBloc extends Bloc { + /// {@macro archive_local_ads_bloc} ArchiveLocalAdsBloc({ required DataRepository localAdsRepository, + required PendingDeletionsService pendingDeletionsService, }) : _localAdsRepository = localAdsRepository, + _pendingDeletionsService = pendingDeletionsService, super(const ArchiveLocalAdsState()) { on(_onLoadArchivedLocalAdsRequested); on(_onRestoreLocalAdRequested); on(_onDeleteLocalAdForeverRequested); on(_onUndoDeleteLocalAdRequested); - on<_ConfirmDeleteLocalAdRequested>(_onConfirmDeleteLocalAdRequested); + on<_DeletionServiceStatusChanged>(_onDeletionServiceStatusChanged); - _localAdUpdateSubscription = _localAdsRepository.entityUpdated - .where((type) => type == LocalAd) - .listen((_) { - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.native, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.banner, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.interstitial, - limit: kDefaultRowsPerPage, - ), - ); - add( - const LoadArchivedLocalAdsRequested( - adType: AdType.video, - limit: kDefaultRowsPerPage, - ), - ); - }); + // Listen to deletion events from the PendingDeletionsService. + _deletionEventSubscription = _pendingDeletionsService.deletionEvents.listen( + (event) { + if (event.item is LocalAd) { + add(_DeletionServiceStatusChanged(event)); + } + }, + ); } final DataRepository _localAdsRepository; - late final StreamSubscription _localAdUpdateSubscription; - Timer? _deleteTimer; + final PendingDeletionsService _pendingDeletionsService; + + /// Subscription to deletion events from the PendingDeletionsService. + late final StreamSubscription> + _deletionEventSubscription; @override Future close() { - _localAdUpdateSubscription.cancel(); - _deleteTimer?.cancel(); + _deletionEventSubscription.cancel(); return super.close(); } + /// Handles the request to load archived local ads for a specific type. + /// + /// Fetches paginated archived local ads from the repository and updates the state. Future _onLoadArchivedLocalAdsRequested( LoadArchivedLocalAdsRequested event, Emitter emit, @@ -221,10 +215,18 @@ class ArchiveLocalAdsBloc } } + /// Handles the request to restore an archived local ad. + /// + /// Optimistically removes the ad from the UI, updates its status to active + /// in the repository, and then updates the state. If the ad was pending + /// deletion, its pending deletion is cancelled. Future _onRestoreLocalAdRequested( RestoreLocalAdRequested event, Emitter emit, ) async { + // Cancel any pending deletion for this ad. + _pendingDeletionsService.undoDeletion(event.id); + LocalAd? adToRestore; final originalNativeAds = List.from(state.nativeAds); final originalBannerAds = List.from(state.bannerAds); @@ -239,13 +241,29 @@ class ArchiveLocalAdsBloc if (index == -1) return; adToRestore = originalNativeAds[index]; originalNativeAds.removeAt(index); - emit(state.copyWith(nativeAds: originalNativeAds)); + emit( + state.copyWith( + nativeAds: originalNativeAds, + lastPendingDeletionId: state.lastPendingDeletionId == event.id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case AdType.banner: final index = originalBannerAds.indexWhere((ad) => ad.id == event.id); if (index == -1) return; adToRestore = originalBannerAds[index]; originalBannerAds.removeAt(index); - emit(state.copyWith(bannerAds: originalBannerAds)); + emit( + state.copyWith( + bannerAds: originalBannerAds, + lastPendingDeletionId: state.lastPendingDeletionId == event.id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case AdType.interstitial: final index = originalInterstitialAds.indexWhere( (ad) => ad.id == event.id, @@ -253,13 +271,29 @@ class ArchiveLocalAdsBloc if (index == -1) return; adToRestore = originalInterstitialAds[index]; originalInterstitialAds.removeAt(index); - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + emit( + state.copyWith( + interstitialAds: originalInterstitialAds, + lastPendingDeletionId: state.lastPendingDeletionId == event.id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case AdType.video: final index = originalVideoAds.indexWhere((ad) => ad.id == event.id); if (index == -1) return; adToRestore = originalVideoAds[index]; originalVideoAds.removeAt(index); - emit(state.copyWith(videoAds: originalVideoAds)); + emit( + state.copyWith( + videoAds: originalVideoAds, + lastPendingDeletionId: state.lastPendingDeletionId == event.id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); } try { @@ -278,45 +312,88 @@ class ArchiveLocalAdsBloc id: event.id, item: updatedAd, ); - emit(state.copyWith(restoredLocalAd: updatedAd)); } on HttpException catch (e) { // Revert UI on failure switch (event.adType) { case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); + emit( + state.copyWith( + nativeAds: originalNativeAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); + emit( + state.copyWith( + bannerAds: originalBannerAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + emit( + state.copyWith( + interstitialAds: originalInterstitialAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); + emit( + state.copyWith( + videoAds: originalVideoAds, + exception: e, + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); } - emit(state.copyWith(exception: e)); } catch (e) { switch (event.adType) { case AdType.native: - emit(state.copyWith(nativeAds: originalNativeAds)); + emit( + state.copyWith( + nativeAds: originalNativeAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.banner: - emit(state.copyWith(bannerAds: originalBannerAds)); + emit( + state.copyWith( + bannerAds: originalBannerAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.interstitial: - emit(state.copyWith(interstitialAds: originalInterstitialAds)); + emit( + state.copyWith( + interstitialAds: originalInterstitialAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); case AdType.video: - emit(state.copyWith(videoAds: originalVideoAds)); + emit( + state.copyWith( + videoAds: originalVideoAds, + exception: UnknownException('An unexpected error occurred: $e'), + lastPendingDeletionId: state.lastPendingDeletionId, + ), + ); } - emit( - state.copyWith( - exception: UnknownException('An unexpected error occurred: $e'), - ), - ); } } + /// Handles the request to permanently delete an archived local ad. + /// + /// This optimistically removes the ad from the UI and initiates a + /// timed deletion via the [PendingDeletionsService]. Future _onDeleteLocalAdForeverRequested( DeleteLocalAdForeverRequested event, Emitter emit, ) async { - _deleteTimer?.cancel(); - LocalAd? adToDelete; final currentNativeAds = List.from(state.nativeAds); final currentBannerAds = List.from(state.bannerAds); @@ -360,130 +437,126 @@ class ArchiveLocalAdsBloc emit(state.copyWith(videoAds: currentVideoAds)); } - emit(state.copyWith(lastDeletedLocalAd: adToDelete)); + String snackbarTitle; + switch (adToDelete.adType) { + case 'native': + snackbarTitle = (adToDelete as LocalNativeAd).title; + case 'banner': + snackbarTitle = (adToDelete as LocalBannerAd).imageUrl; + case 'interstitial': + snackbarTitle = (adToDelete as LocalInterstitialAd).imageUrl; + case 'video': + snackbarTitle = (adToDelete as LocalVideoAd).videoUrl; + default: + snackbarTitle = adToDelete.id; + } + + emit( + state.copyWith( + lastPendingDeletionId: event.id, + snackbarLocalAdTitle: snackbarTitle, + ), + ); - _deleteTimer = Timer( - const Duration(seconds: 5), - () => add(_ConfirmDeleteLocalAdRequested(event.id, event.adType)), + // Request deletion via the service. + _pendingDeletionsService.requestDeletion( + item: adToDelete, + repository: _localAdsRepository, + undoDuration: const Duration(seconds: 5), ); } - Future _onConfirmDeleteLocalAdRequested( - _ConfirmDeleteLocalAdRequested event, + /// Handles the request to undo a pending deletion of an archived local ad. + /// + /// This cancels the deletion timer in the [PendingDeletionsService]. + Future _onUndoDeleteLocalAdRequested( + UndoDeleteLocalAdRequested event, Emitter emit, ) async { - try { - await _localAdsRepository.delete(id: event.id); - emit(state.copyWith(clearLastDeletedLocalAd: true)); - } on HttpException catch (e) { - // If deletion fails, restore the ad to the list - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { - case 'native': - final updatedAds = List.from(state.nativeAds) - ..add(restoredAd as LocalNativeAd); - emit(state.copyWith(nativeAds: updatedAds)); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..add(restoredAd as LocalBannerAd); - emit(state.copyWith(bannerAds: updatedAds)); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..add(restoredAd as LocalInterstitialAd); - emit(state.copyWith(interstitialAds: updatedAds)); - case 'video': - final updatedAds = List.from(state.videoAds) - ..add(restoredAd as LocalVideoAd); - emit(state.copyWith(videoAds: updatedAds)); - } - } - emit(state.copyWith(exception: e, clearLastDeletedLocalAd: true)); - } catch (e) { - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { + if (state.lastPendingDeletionId != null) { + _pendingDeletionsService.undoDeletion(state.lastPendingDeletionId!); + } + // The _onDeletionServiceStatusChanged will handle re-adding to the list + // and updating pendingDeletions when DeletionStatus.undone is emitted. + } + + /// Handles deletion events from the [PendingDeletionsService]. + /// + /// This method is called when an item's deletion is confirmed or undone + /// by the service. It updates the BLoC's state accordingly. + Future _onDeletionServiceStatusChanged( + _DeletionServiceStatusChanged event, + Emitter emit, + ) async { + final id = event.event.id; + final status = event.event.status; + final item = event.event.item; + + if (status == DeletionStatus.confirmed) { + // Deletion confirmed, no action needed in BLoC as it was optimistically removed. + // Ensure lastPendingDeletionId and snackbarLocalAdTitle are cleared if this was the one. + emit( + state.copyWith( + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); + } else if (status == DeletionStatus.undone) { + // Deletion undone, restore the local ad to the main list. + if (item is LocalAd) { + switch (item.adType) { case 'native': final updatedAds = List.from(state.nativeAds) - ..add(restoredAd as LocalNativeAd); - emit(state.copyWith(nativeAds: updatedAds)); + ..insert(0, item as LocalNativeAd); + emit( + state.copyWith( + nativeAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case 'banner': final updatedAds = List.from(state.bannerAds) - ..add(restoredAd as LocalBannerAd); - emit(state.copyWith(bannerAds: updatedAds)); + ..insert(0, item as LocalBannerAd); + emit( + state.copyWith( + bannerAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case 'interstitial': final updatedAds = List.from( state.interstitialAds, - )..add(restoredAd as LocalInterstitialAd); - emit(state.copyWith(interstitialAds: updatedAds)); + )..insert(0, item as LocalInterstitialAd); + emit( + state.copyWith( + interstitialAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), + ); case 'video': final updatedAds = List.from(state.videoAds) - ..add(restoredAd as LocalVideoAd); - emit(state.copyWith(videoAds: updatedAds)); - } - } - emit( - state.copyWith( - exception: UnknownException('An unexpected error occurred: $e'), - clearLastDeletedLocalAd: true, - ), - ); - } - } - - void _onUndoDeleteLocalAdRequested( - UndoDeleteLocalAdRequested event, - Emitter emit, - ) { - _deleteTimer?.cancel(); - if (state.lastDeletedLocalAd != null) { - final restoredAd = state.lastDeletedLocalAd!; - switch (restoredAd.adType) { - case 'native': - final updatedAds = List.from(state.nativeAds) - ..insert( - 0, - restoredAd as LocalNativeAd, + ..insert(0, item as LocalVideoAd); + emit( + state.copyWith( + videoAds: updatedAds, + lastPendingDeletionId: state.lastPendingDeletionId == id + ? null + : state.lastPendingDeletionId, + snackbarLocalAdTitle: null, + ), ); - emit( - state.copyWith( - nativeAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'banner': - final updatedAds = List.from(state.bannerAds) - ..insert(0, restoredAd as LocalBannerAd); - emit( - state.copyWith( - bannerAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'interstitial': - final updatedAds = List.from( - state.interstitialAds, - )..insert(0, restoredAd as LocalInterstitialAd); - emit( - state.copyWith( - interstitialAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); - case 'video': - final updatedAds = List.from(state.videoAds) - ..insert(0, restoredAd as LocalVideoAd); - emit( - state.copyWith( - videoAds: updatedAds, - clearLastDeletedLocalAd: true, - restoredLocalAd: restoredAd, - ), - ); + } } } } diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart index 96e29ff1..a4268847 100644 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart +++ b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_event.dart @@ -73,17 +73,12 @@ final class UndoDeleteLocalAdRequested extends ArchiveLocalAdsEvent { const UndoDeleteLocalAdRequested(); } -/// Internal event to confirm the permanent deletion of a local ad after a delay. -final class _ConfirmDeleteLocalAdRequested extends ArchiveLocalAdsEvent { - /// {@macro _confirm_delete_local_ad_requested} - const _ConfirmDeleteLocalAdRequested(this.id, this.adType); +/// Event to handle updates from the pending deletions service. +final class _DeletionServiceStatusChanged extends ArchiveLocalAdsEvent { + const _DeletionServiceStatusChanged(this.event); - /// The ID of the local ad to confirm deletion for. - final String id; - - /// The type of the local ad to confirm deletion for. - final AdType adType; + final DeletionEvent event; @override - List get props => [id, adType]; + List get props => [event]; } diff --git a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart index cc199f75..7b5d4393 100644 --- a/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart +++ b/lib/local_ads_management/bloc/archive_local_ads/archive_local_ads_state.dart @@ -29,8 +29,8 @@ class ArchiveLocalAdsState extends Equatable { this.videoAdsCursor, this.videoAdsHasMore = false, this.exception, - this.lastDeletedLocalAd, - this.restoredLocalAd, + this.lastPendingDeletionId, + this.snackbarLocalAdTitle, }); final ArchiveLocalAdsStatus status; @@ -86,11 +86,14 @@ class ArchiveLocalAdsState extends Equatable { /// The error describing an operation failure, if any. final HttpException? exception; - /// The last deleted local ad, used for undo functionality. - final LocalAd? lastDeletedLocalAd; + /// The ID of the local ad that was most recently added to pending deletions. + /// Used to trigger the snackbar display. + final String? lastPendingDeletionId; - /// The last restored local ad, used for triggering UI updates. - final LocalAd? restoredLocalAd; + /// The title of the local ad for which the snackbar should be displayed. + /// This is set when a deletion is requested and cleared when the snackbar + /// is no longer needed. + final String? snackbarLocalAdTitle; ArchiveLocalAdsState copyWith({ ArchiveLocalAdsStatus? status, @@ -111,10 +114,8 @@ class ArchiveLocalAdsState extends Equatable { String? videoAdsCursor, bool? videoAdsHasMore, HttpException? exception, - LocalAd? lastDeletedLocalAd, - LocalAd? restoredLocalAd, - bool clearLastDeletedLocalAd = false, - bool clearRestoredLocalAd = false, + String? lastPendingDeletionId, + String? snackbarLocalAdTitle, }) { return ArchiveLocalAdsState( status: status ?? this.status, @@ -137,13 +138,10 @@ class ArchiveLocalAdsState extends Equatable { videoAds: videoAds ?? this.videoAds, videoAdsCursor: videoAdsCursor ?? this.videoAdsCursor, videoAdsHasMore: videoAdsHasMore ?? this.videoAdsHasMore, - exception: exception ?? this.exception, - lastDeletedLocalAd: clearLastDeletedLocalAd - ? null - : lastDeletedLocalAd ?? this.lastDeletedLocalAd, - restoredLocalAd: clearRestoredLocalAd - ? null - : restoredLocalAd ?? this.restoredLocalAd, + exception: exception, + lastPendingDeletionId: + lastPendingDeletionId ?? this.lastPendingDeletionId, + snackbarLocalAdTitle: snackbarLocalAdTitle, ); } @@ -167,7 +165,7 @@ class ArchiveLocalAdsState extends Equatable { videoAdsCursor, videoAdsHasMore, exception, - lastDeletedLocalAd, - restoredLocalAd, + lastPendingDeletionId, + snackbarLocalAdTitle, ]; } diff --git a/lib/local_ads_management/view/archived_local_ads_page.dart b/lib/local_ads_management/view/archived_local_ads_page.dart index 029b66cb..c5a04956 100644 --- a/lib/local_ads_management/view/archived_local_ads_page.dart +++ b/lib/local_ads_management/view/archived_local_ads_page.dart @@ -6,12 +6,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/archive_local_ads/archive_local_ads_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/local_ads_management/bloc/local_ads_management_bloc.dart' - hide - DeleteLocalAdForeverRequested, - RestoreLocalAdRequested, - UndoDeleteLocalAdRequested; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -28,6 +24,7 @@ class ArchivedLocalAdsPage extends StatelessWidget { create: (context) => ArchiveLocalAdsBloc( localAdsRepository: context.read>(), + pendingDeletionsService: context.read(), ) ..add(const LoadArchivedLocalAdsRequested(adType: AdType.native)) ..add(const LoadArchivedLocalAdsRequested(adType: AdType.banner)) @@ -77,6 +74,13 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; + // Read the PendingDeletionsService instance directly. + // This is safe because PendingDeletionsService is a singleton and does not + // depend on the widget's BuildContext for its operations, preventing + // "deactivated widget's ancestor" errors when the SnackBar is dismissed + // after the widget has been unmounted. + final pendingDeletionsService = context.read(); + return Scaffold( appBar: AppBar( title: Text(l10n.archivedLocalAdsTitle), @@ -89,42 +93,12 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> ), body: BlocListener( listenWhen: (previous, current) => - previous.lastDeletedLocalAd != current.lastDeletedLocalAd || - (previous.nativeAds.length != current.nativeAds.length && - current.lastDeletedLocalAd == null) || - (previous.bannerAds.length != current.bannerAds.length && - current.lastDeletedLocalAd == null) || - (previous.interstitialAds.length != - current.interstitialAds.length && - current.lastDeletedLocalAd == null) || - (previous.videoAds.length != current.videoAds.length && - current.lastDeletedLocalAd == null) || - (previous.restoredLocalAd == null && - current.restoredLocalAd != null), + previous.lastPendingDeletionId != current.lastPendingDeletionId || + previous.snackbarLocalAdTitle != current.snackbarLocalAdTitle, listener: (context, state) { - if (state.lastDeletedLocalAd != null) { - String truncatedTitle; - switch (state.lastDeletedLocalAd!.adType) { - case 'native': - truncatedTitle = (state.lastDeletedLocalAd! as LocalNativeAd) - .title - .truncate(30); - case 'banner': - truncatedTitle = (state.lastDeletedLocalAd! as LocalBannerAd) - .imageUrl - .truncate(30); - case 'interstitial': - truncatedTitle = - (state.lastDeletedLocalAd! as LocalInterstitialAd).imageUrl - .truncate(30); - case 'video': - truncatedTitle = (state.lastDeletedLocalAd! as LocalVideoAd) - .videoUrl - .truncate(30); - default: - truncatedTitle = state.lastDeletedLocalAd!.id.truncate(30); - } - + if (state.snackbarLocalAdTitle != null) { + final adId = state.lastPendingDeletionId!; + final truncatedTitle = state.snackbarLocalAdTitle!.truncate(30); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -135,23 +109,15 @@ class _ArchivedLocalAdsViewState extends State<_ArchivedLocalAdsView> action: SnackBarAction( label: l10n.undo, onPressed: () { - context.read().add( - const UndoDeleteLocalAdRequested(), - ); + // Directly call undoDeletion on the service. + // This avoids using context.read on a potentially deactivated + // widget, which caused the "deactivated widget's ancestor" error. + pendingDeletionsService.undoDeletion(adId); }, ), ), ); } - // Trigger refresh of active ads in LocalAdsManagementBloc if an ad was restored - if (state.restoredLocalAd != null) { - context.read().add( - LoadLocalAdsRequested( - adType: state.restoredLocalAd!.toAdType(), - forceRefresh: true, - ), - ); - } }, child: TabBarView( controller: _tabController,