@@ -2,6 +2,7 @@ import 'dart:async';
22
33import 'package:bloc/bloc.dart' ;
44import 'package:bloc_concurrency/bloc_concurrency.dart' ;
5+ import 'package:collection/collection.dart' ;
56import 'package:core/core.dart' ;
67import 'package:data_repository/data_repository.dart' ;
78import 'package:equatable/equatable.dart' ;
@@ -265,19 +266,38 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
265266 HeadlinesFeedFiltersApplied event,
266267 Emitter <HeadlinesFeedState > emit,
267268 ) async {
268- String ? newActiveFilterId;
269+ String newActiveFilterId;
269270
270- // If a saved filter was explicitly passed (e.g., just created), use its ID.
271+ // Prioritize the explicitly passed savedFilter to prevent race conditions.
272+ // This is crucial for the "save and apply" flow, where the AppBloc might
273+ // not have updated the savedFilters list in this bloc's state yet.
271274 if (event.savedFilter != null ) {
272275 newActiveFilterId = event.savedFilter! .id;
273276 } else {
274- // Otherwise, determine if this is a pre-existing saved filter being
275- // re-applied or a custom one.
276- final isSavedFilter =
277- state.activeFilterId != 'all' &&
278- state.activeFilterId != 'custom' &&
279- state.activeFilterId != null ;
280- newActiveFilterId = isSavedFilter ? state.activeFilterId : 'custom' ;
277+ // If no filter is explicitly passed, determine if the applied filter
278+ // matches an existing saved filter. This handles re-applying a saved
279+ // filter or applying a one-time "custom" filter.
280+ final matchingSavedFilter = state.savedFilters.firstWhereOrNull ((
281+ savedFilter,
282+ ) {
283+ // Use sets for order-agnostic comparison of filter contents.
284+ final appliedTopics = event.filter.topics? .toSet () ?? {};
285+ final savedTopics = savedFilter.topics.toSet ();
286+ final appliedSources = event.filter.sources? .toSet () ?? {};
287+ final savedSources = savedFilter.sources.toSet ();
288+ final appliedCountries = event.filter.eventCountries? .toSet () ?? {};
289+ final savedCountries = savedFilter.countries.toSet ();
290+
291+ return const SetEquality <Topic >().equals (appliedTopics, savedTopics) &&
292+ const SetEquality <Source >().equals (appliedSources, savedSources) &&
293+ const SetEquality <Country >().equals (
294+ appliedCountries,
295+ savedCountries,
296+ );
297+ });
298+
299+ // If a match is found, use its ID. Otherwise, mark it as 'custom'.
300+ newActiveFilterId = matchingSavedFilter? .id ?? 'custom' ;
281301 }
282302
283303 // When applying new filters, this is considered a major feed change,
@@ -550,15 +570,15 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
550570 /// Handles the selection of the "Followed" filter from the filter bar.
551571 ///
552572 /// This creates a [HeadlineFilter] from the user's followed items and
553- /// triggers a full feed refresh.
573+ /// triggers a full feed refresh directly within this handler. It no longer
574+ /// delegates to `HeadlinesFeedFiltersApplied` to prevent a bug where the
575+ /// filter would be incorrectly identified as 'custom'.
554576 Future <void > _onFollowedFilterSelected (
555577 FollowedFilterSelected event,
556578 Emitter <HeadlinesFeedState > emit,
557579 ) async {
558580 final userPreferences = _appBloc.state.userContentPreferences;
559581 if (userPreferences == null ) {
560- // This case should ideally not happen if the UI is built correctly,
561- // as the "Followed" button is only shown when preferences are available.
562582 return ;
563583 }
564584
@@ -568,15 +588,77 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
568588 eventCountries: userPreferences.followedCountries,
569589 );
570590
571- // Set the active filter ID and then dispatch an event to apply the filter
572- // and refresh the feed.
573- emit (state. copyWith (activeFilterId : 'followed' ));
574- add (
575- HeadlinesFeedFiltersApplied (
591+ // This is a major feed change, so clear the ad cache.
592+ _inlineAdCacheService. clearAllAds ();
593+ emit (
594+ state. copyWith (
595+ status : HeadlinesFeedStatus .loading,
576596 filter: newFilter,
577- adThemeStyle: event.adThemeStyle,
597+ activeFilterId: 'followed' ,
598+ feedItems: [],
599+ clearCursor: true ,
578600 ),
579601 );
602+
603+ try {
604+ final currentUser = _appBloc.state.user;
605+ final appConfig = _appBloc.state.remoteConfig;
606+
607+ if (appConfig == null ) {
608+ emit (state.copyWith (status: HeadlinesFeedStatus .failure));
609+ return ;
610+ }
611+
612+ final headlineResponse = await _headlinesRepository.readAll (
613+ filter: _buildFilter (newFilter),
614+ pagination: const PaginationOptions (limit: _headlinesFetchLimit),
615+ sort: [const SortOption ('updatedAt' , SortOrder .desc)],
616+ );
617+
618+ final decorationResult = await _feedDecoratorService.decorateFeed (
619+ headlines: headlineResponse.items,
620+ user: currentUser,
621+ remoteConfig: appConfig,
622+ followedTopicIds: userPreferences.followedTopics
623+ .map ((t) => t.id)
624+ .toList (),
625+ followedSourceIds: userPreferences.followedSources
626+ .map ((s) => s.id)
627+ .toList (),
628+ imageStyle: _appBloc.state.settings! .feedPreferences.headlineImageStyle,
629+ adThemeStyle: event.adThemeStyle,
630+ );
631+
632+ emit (
633+ state.copyWith (
634+ status: HeadlinesFeedStatus .success,
635+ feedItems: decorationResult.decoratedItems,
636+ hasMore: headlineResponse.hasMore,
637+ cursor: headlineResponse.cursor,
638+ ),
639+ );
640+
641+ final injectedDecorator = decorationResult.injectedDecorator;
642+ if (injectedDecorator != null && currentUser? .id != null ) {
643+ if (injectedDecorator is CallToActionItem ) {
644+ _appBloc.add (
645+ AppUserFeedDecoratorShown (
646+ userId: currentUser! .id,
647+ feedDecoratorType: injectedDecorator.decoratorType,
648+ ),
649+ );
650+ } else if (injectedDecorator is ContentCollectionItem ) {
651+ _appBloc.add (
652+ AppUserFeedDecoratorShown (
653+ userId: currentUser! .id,
654+ feedDecoratorType: injectedDecorator.decoratorType,
655+ ),
656+ );
657+ }
658+ }
659+ } on HttpException catch (e) {
660+ emit (state.copyWith (status: HeadlinesFeedStatus .failure, error: e));
661+ }
580662 }
581663
582664 @override
0 commit comments