Skip to content

Commit 9f4178f

Browse files
committed
feat(feed): inject ads and account actions
- Integrated FeedInjectorService - Added AppBloc dependency - Updated feed item loading logic - Handled missing AppConfig
1 parent d715d5e commit 9f4178f

File tree

1 file changed

+131
-64
lines changed

1 file changed

+131
-64
lines changed

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 131 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import 'package:bloc/bloc.dart';
44
import 'package:bloc_concurrency/bloc_concurrency.dart';
55
import 'package:equatable/equatable.dart';
66
import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository
7+
import 'package:ht_main/app/bloc/app_bloc.dart'; // Added
78
import 'package:ht_main/headlines-feed/models/headline_filter.dart';
8-
import 'package:ht_shared/ht_shared.dart'
9-
show Headline, HtHttpException; // Shared models and standardized exceptions
9+
import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added
10+
import 'package:ht_shared/ht_shared.dart'; // Updated for FeedItem, AppConfig, User
1011

1112
part 'headlines_feed_event.dart';
1213
part 'headlines_feed_state.dart';
@@ -15,15 +16,21 @@ part 'headlines_feed_state.dart';
1516
/// Manages the state for the headlines feed feature.
1617
///
1718
/// Handles fetching headlines, applying filters, pagination, and refreshing
18-
/// the feed using the provided [HtDataRepository].
19+
/// the feed using the provided [HtDataRepository]. It uses [FeedInjectorService]
20+
/// to inject ads and account actions into the feed.
1921
/// {@endtemplate}
2022
class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
2123
/// {@macro headlines_feed_bloc}
2224
///
23-
/// Requires a [HtDataRepository<Headline>] to interact with the data layer.
24-
HeadlinesFeedBloc({required HtDataRepository<Headline> headlinesRepository})
25-
: _headlinesRepository = headlinesRepository,
26-
super(HeadlinesFeedInitial()) {
25+
/// Requires repositories and services for its operations.
26+
HeadlinesFeedBloc({
27+
required HtDataRepository<Headline> headlinesRepository,
28+
required FeedInjectorService feedInjectorService, // Added
29+
required AppBloc appBloc, // Added
30+
}) : _headlinesRepository = headlinesRepository,
31+
_feedInjectorService = feedInjectorService, // Added
32+
_appBloc = appBloc, // Added
33+
super(HeadlinesFeedInitial()) {
2734
on<HeadlinesFeedFetchRequested>(
2835
_onHeadlinesFeedFetchRequested,
2936
transformer:
@@ -39,6 +46,8 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
3946
}
4047

4148
final HtDataRepository<Headline> _headlinesRepository;
49+
final FeedInjectorService _feedInjectorService; // Added
50+
final AppBloc _appBloc; // Added
4251

4352
/// The number of headlines to fetch per page during pagination or initial load.
4453
static const _headlinesFetchLimit = 10;
@@ -67,15 +76,32 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
6776
.join(',');
6877
}
6978

70-
final response = await _headlinesRepository.readAllByQuery(
79+
final headlineResponse = await _headlinesRepository.readAllByQuery(
7180
queryParams,
7281
limit: _headlinesFetchLimit,
7382
);
83+
84+
final currentUser = _appBloc.state.user;
85+
final appConfig = _appBloc.state.appConfig;
86+
87+
if (appConfig == null) {
88+
// AppConfig is crucial for injection rules.
89+
emit(const HeadlinesFeedError(message: 'App configuration not available.'));
90+
return;
91+
}
92+
93+
final processedFeedItems = _feedInjectorService.injectItems(
94+
headlines: headlineResponse.items,
95+
user: currentUser,
96+
appConfig: appConfig,
97+
currentFeedItemCount: 0, // Initial load for filters
98+
);
99+
74100
emit(
75101
HeadlinesFeedLoaded(
76-
headlines: response.items,
77-
hasMore: response.hasMore,
78-
cursor: response.cursor,
102+
feedItems: processedFeedItems, // Changed
103+
hasMore: headlineResponse.hasMore, // Based on original headline fetch
104+
cursor: headlineResponse.cursor,
79105
filter: event.filter, // Store the applied filter
80106
),
81107
);
@@ -99,14 +125,30 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
99125
emit(HeadlinesFeedLoading()); // Show loading indicator
100126
try {
101127
// Fetch the first page with no filters
102-
final response = await _headlinesRepository.readAll(
128+
final headlineResponse = await _headlinesRepository.readAll(
103129
limit: _headlinesFetchLimit,
104130
);
131+
132+
final currentUser = _appBloc.state.user;
133+
final appConfig = _appBloc.state.appConfig;
134+
135+
if (appConfig == null) {
136+
emit(const HeadlinesFeedError(message: 'App configuration not available.'));
137+
return;
138+
}
139+
140+
final processedFeedItems = _feedInjectorService.injectItems(
141+
headlines: headlineResponse.items,
142+
user: currentUser,
143+
appConfig: appConfig,
144+
currentFeedItemCount: 0,
145+
);
146+
105147
emit(
106148
HeadlinesFeedLoaded(
107-
headlines: response.items,
108-
hasMore: response.hasMore,
109-
cursor: response.cursor,
149+
feedItems: processedFeedItems, // Changed
150+
hasMore: headlineResponse.hasMore,
151+
cursor: headlineResponse.cursor,
110152
),
111153
);
112154
} on HtHttpException catch (e) {
@@ -130,42 +172,42 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
130172
Emitter<HeadlinesFeedState> emit,
131173
) async {
132174
// Determine current filter and cursor based on state
133-
var currentFilter = const HeadlineFilter(); // Made type explicit
134-
var currentCursor = // Made type explicit
135-
event.cursor; // Use event's cursor if provided (for pagination)
136-
var currentHeadlines = <Headline>[];
137-
var isPaginating = false;
175+
var currentFilter = const HeadlineFilter();
176+
String? currentCursorForFetch = event.cursor;
177+
List<FeedItem> currentFeedItems = [];
178+
bool isPaginating = false;
179+
int currentFeedItemCountForInjector = 0;
138180

139181
if (state is HeadlinesFeedLoaded) {
140182
final loadedState = state as HeadlinesFeedLoaded;
141183
currentFilter = loadedState.filter;
142-
// Only use state's cursor if event's cursor is null (i.e., not explicit pagination request)
143-
currentCursor ??= loadedState.cursor;
144-
currentHeadlines = loadedState.headlines;
145-
// Check if we should paginate
146-
isPaginating = event.cursor != null && loadedState.hasMore;
147-
if (isPaginating && state is HeadlinesFeedLoadingSilently) {
148-
return; // Avoid concurrent pagination
149-
}
150-
if (!loadedState.hasMore && event.cursor != null) {
151-
return; // Don't fetch if no more items
184+
currentFeedItems = loadedState.feedItems;
185+
currentFeedItemCountForInjector = loadedState.feedItems.length;
186+
187+
if (event.cursor != null) { // Explicit pagination request
188+
if (!loadedState.hasMore) return; // No more items to fetch
189+
isPaginating = true;
190+
currentCursorForFetch = loadedState.cursor; // Use BLoC's cursor for safety
191+
} else { // Initial fetch or refresh (event.cursor is null)
192+
currentFeedItems = []; // Reset for non-pagination
193+
currentFeedItemCountForInjector = 0;
152194
}
153-
} else if (state is HeadlinesFeedLoading ||
154-
state is HeadlinesFeedLoadingSilently) {
155-
// Avoid concurrent fetches if already loading, unless it's explicit pagination
156-
if (event.cursor == null) return;
195+
} else if (state is HeadlinesFeedLoading || state is HeadlinesFeedLoadingSilently) {
196+
if (event.cursor == null) return; // Avoid concurrent initial fetches
157197
}
198+
// For initial load or if event.cursor is null, currentCursorForFetch remains null.
158199

159-
// Emit appropriate loading state
160-
if (isPaginating) {
161-
emit(HeadlinesFeedLoadingSilently());
162-
} else {
163-
// Initial load or load after error/clear
164-
emit(HeadlinesFeedLoading());
165-
currentHeadlines = []; // Reset headlines on non-pagination fetch
166-
}
200+
emit(isPaginating ? HeadlinesFeedLoadingSilently() : HeadlinesFeedLoading());
167201

168202
try {
203+
final currentUser = _appBloc.state.user;
204+
final appConfig = _appBloc.state.appConfig;
205+
206+
if (appConfig == null) {
207+
emit(const HeadlinesFeedError(message: 'App configuration not available.'));
208+
return;
209+
}
210+
169211
final queryParams = <String, dynamic>{};
170212
if (currentFilter.categories?.isNotEmpty ?? false) {
171213
queryParams['categories'] = currentFilter.categories!
@@ -178,19 +220,27 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
178220
.join(',');
179221
}
180222

181-
final response = await _headlinesRepository.readAllByQuery(
223+
final headlineResponse = await _headlinesRepository.readAllByQuery(
182224
queryParams,
183225
limit: _headlinesFetchLimit,
184-
startAfterId: currentCursor, // Use determined cursor
226+
startAfterId: currentCursorForFetch,
227+
);
228+
229+
final newProcessedFeedItems = _feedInjectorService.injectItems(
230+
headlines: headlineResponse.items,
231+
user: currentUser,
232+
appConfig: appConfig,
233+
currentFeedItemCount: currentFeedItemCountForInjector,
185234
);
235+
186236
emit(
187237
HeadlinesFeedLoaded(
188-
// Append if paginating, otherwise replace
189-
headlines:
190-
isPaginating ? currentHeadlines + response.items : response.items,
191-
hasMore: response.hasMore,
192-
cursor: response.cursor,
193-
filter: currentFilter, // Preserve the filter
238+
feedItems: isPaginating
239+
? (List.of(currentFeedItems)..addAll(newProcessedFeedItems))
240+
: newProcessedFeedItems,
241+
hasMore: headlineResponse.hasMore,
242+
cursor: headlineResponse.cursor,
243+
filter: currentFilter,
194244
),
195245
);
196246
} on HtHttpException catch (e) {
@@ -211,36 +261,53 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
211261
) async {
212262
emit(HeadlinesFeedLoading()); // Show loading indicator for refresh
213263

214-
// Determine the filter currently applied in the state
215-
var currentFilter = const HeadlineFilter(); // Made type explicit
264+
var currentFilter = const HeadlineFilter();
216265
if (state is HeadlinesFeedLoaded) {
217266
currentFilter = (state as HeadlinesFeedLoaded).filter;
218267
}
219268

220269
try {
270+
final currentUser = _appBloc.state.user;
271+
final appConfig = _appBloc.state.appConfig;
272+
273+
if (appConfig == null) {
274+
emit(const HeadlinesFeedError(message: 'App configuration not available.'));
275+
return;
276+
}
277+
221278
final queryParams = <String, dynamic>{};
222279
if (currentFilter.categories?.isNotEmpty ?? false) {
223-
queryParams['categories'] = currentFilter.categories!
224-
.map((c) => c.id)
225-
.join(',');
280+
queryParams['categories'] =
281+
currentFilter.categories!.map((c) => c.id).join(',');
226282
}
227283
if (currentFilter.sources?.isNotEmpty ?? false) {
228-
queryParams['sources'] = currentFilter.sources!
229-
.map((s) => s.id)
230-
.join(',');
284+
queryParams['sources'] =
285+
currentFilter.sources!.map((s) => s.id).join(',');
231286
}
232287

233-
// Fetch the first page using the current filter
234-
final response = await _headlinesRepository.readAllByQuery(
288+
final headlineResponse = await _headlinesRepository.readAllByQuery(
235289
queryParams,
236290
limit: _headlinesFetchLimit,
237291
);
292+
293+
final List<Headline> headlinesToInject = headlineResponse.items;
294+
final User? userForInjector = currentUser;
295+
final AppConfig configForInjector = appConfig;
296+
const int itemCountForInjector = 0;
297+
298+
final processedFeedItems = _feedInjectorService.injectItems(
299+
headlines: headlinesToInject,
300+
user: userForInjector,
301+
appConfig: configForInjector,
302+
currentFeedItemCount: itemCountForInjector,
303+
);
304+
238305
emit(
239306
HeadlinesFeedLoaded(
240-
headlines: response.items, // Replace headlines on refresh
241-
hasMore: response.hasMore,
242-
cursor: response.cursor,
243-
filter: currentFilter, // Preserve the filter
307+
feedItems: processedFeedItems,
308+
hasMore: headlineResponse.hasMore,
309+
cursor: headlineResponse.cursor,
310+
filter: currentFilter,
244311
),
245312
);
246313
} on HtHttpException catch (e) {

0 commit comments

Comments
 (0)