@@ -4,9 +4,10 @@ import 'package:bloc/bloc.dart';
4
4
import 'package:bloc_concurrency/bloc_concurrency.dart' ;
5
5
import 'package:equatable/equatable.dart' ;
6
6
import 'package:ht_data_repository/ht_data_repository.dart' ; // Generic Data Repository
7
+ import 'package:ht_main/app/bloc/app_bloc.dart' ; // Added
7
8
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
10
11
11
12
part 'headlines_feed_event.dart' ;
12
13
part 'headlines_feed_state.dart' ;
@@ -15,15 +16,21 @@ part 'headlines_feed_state.dart';
15
16
/// Manages the state for the headlines feed feature.
16
17
///
17
18
/// 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.
19
21
/// {@endtemplate}
20
22
class HeadlinesFeedBloc extends Bloc <HeadlinesFeedEvent , HeadlinesFeedState > {
21
23
/// {@macro headlines_feed_bloc}
22
24
///
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 ()) {
27
34
on < HeadlinesFeedFetchRequested > (
28
35
_onHeadlinesFeedFetchRequested,
29
36
transformer:
@@ -39,6 +46,8 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
39
46
}
40
47
41
48
final HtDataRepository <Headline > _headlinesRepository;
49
+ final FeedInjectorService _feedInjectorService; // Added
50
+ final AppBloc _appBloc; // Added
42
51
43
52
/// The number of headlines to fetch per page during pagination or initial load.
44
53
static const _headlinesFetchLimit = 10 ;
@@ -67,15 +76,32 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
67
76
.join (',' );
68
77
}
69
78
70
- final response = await _headlinesRepository.readAllByQuery (
79
+ final headlineResponse = await _headlinesRepository.readAllByQuery (
71
80
queryParams,
72
81
limit: _headlinesFetchLimit,
73
82
);
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
+
74
100
emit (
75
101
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,
79
105
filter: event.filter, // Store the applied filter
80
106
),
81
107
);
@@ -99,14 +125,30 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
99
125
emit (HeadlinesFeedLoading ()); // Show loading indicator
100
126
try {
101
127
// Fetch the first page with no filters
102
- final response = await _headlinesRepository.readAll (
128
+ final headlineResponse = await _headlinesRepository.readAll (
103
129
limit: _headlinesFetchLimit,
104
130
);
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
+
105
147
emit (
106
148
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,
110
152
),
111
153
);
112
154
} on HtHttpException catch (e) {
@@ -130,42 +172,42 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
130
172
Emitter <HeadlinesFeedState > emit,
131
173
) async {
132
174
// 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 ;
138
180
139
181
if (state is HeadlinesFeedLoaded ) {
140
182
final loadedState = state as HeadlinesFeedLoaded ;
141
183
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 ;
152
194
}
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
157
197
}
198
+ // For initial load or if event.cursor is null, currentCursorForFetch remains null.
158
199
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 ());
167
201
168
202
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
+
169
211
final queryParams = < String , dynamic > {};
170
212
if (currentFilter.categories? .isNotEmpty ?? false ) {
171
213
queryParams['categories' ] = currentFilter.categories!
@@ -178,19 +220,27 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
178
220
.join (',' );
179
221
}
180
222
181
- final response = await _headlinesRepository.readAllByQuery (
223
+ final headlineResponse = await _headlinesRepository.readAllByQuery (
182
224
queryParams,
183
225
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,
185
234
);
235
+
186
236
emit (
187
237
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,
194
244
),
195
245
);
196
246
} on HtHttpException catch (e) {
@@ -211,36 +261,53 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
211
261
) async {
212
262
emit (HeadlinesFeedLoading ()); // Show loading indicator for refresh
213
263
214
- // Determine the filter currently applied in the state
215
- var currentFilter = const HeadlineFilter (); // Made type explicit
264
+ var currentFilter = const HeadlineFilter ();
216
265
if (state is HeadlinesFeedLoaded ) {
217
266
currentFilter = (state as HeadlinesFeedLoaded ).filter;
218
267
}
219
268
220
269
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
+
221
278
final queryParams = < String , dynamic > {};
222
279
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 (',' );
226
282
}
227
283
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 (',' );
231
286
}
232
287
233
- // Fetch the first page using the current filter
234
- final response = await _headlinesRepository.readAllByQuery (
288
+ final headlineResponse = await _headlinesRepository.readAllByQuery (
235
289
queryParams,
236
290
limit: _headlinesFetchLimit,
237
291
);
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
+
238
305
emit (
239
306
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,
244
311
),
245
312
);
246
313
} on HtHttpException catch (e) {
0 commit comments