Skip to content

Headlines feed sorted by date #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
emit(
state.copyWith(
status: AccountStatus.success,
preferences: preferences,
preferences: _sortPreferences(preferences),
clearError: true,
),
);
Expand All @@ -98,7 +98,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
emit(
state.copyWith(
status: AccountStatus.success,
preferences: migratedPreferences,
preferences: _sortPreferences(migratedPreferences),
clearError: true,
),
);
Expand Down Expand Up @@ -127,7 +127,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
);
emit(
state.copyWith(
preferences: defaultPreferences,
preferences: _sortPreferences(defaultPreferences),
clearError: true,
status: AccountStatus.success,
),
Expand All @@ -146,7 +146,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
emit(
state.copyWith(
status: AccountStatus.success,
preferences: existingPreferences,
preferences: _sortPreferences(existingPreferences),
clearError: true,
),
);
Expand Down Expand Up @@ -216,15 +216,16 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
);

try {
final sortedPrefs = _sortPreferences(updatedPrefs);
await _userContentPreferencesRepository.update(
id: state.user!.id,
item: updatedPrefs,
item: sortedPrefs,
userId: state.user!.id,
);
emit(
state.copyWith(
status: AccountStatus.success,
preferences: updatedPrefs,
preferences: sortedPrefs,
clearError: true,
),
);
Expand Down Expand Up @@ -273,15 +274,16 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
);

try {
final sortedPrefs = _sortPreferences(updatedPrefs);
await _userContentPreferencesRepository.update(
id: state.user!.id,
item: updatedPrefs,
item: sortedPrefs,
userId: state.user!.id,
);
emit(
state.copyWith(
status: AccountStatus.success,
preferences: updatedPrefs,
preferences: sortedPrefs,
clearError: true,
),
);
Expand Down Expand Up @@ -331,15 +333,16 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
);

try {
final sortedPrefs = _sortPreferences(updatedPrefs);
await _userContentPreferencesRepository.update(
id: state.user!.id,
item: updatedPrefs,
item: sortedPrefs,
userId: state.user!.id,
);
emit(
state.copyWith(
status: AccountStatus.success,
preferences: updatedPrefs,
preferences: sortedPrefs,
clearError: true,
),
);
Expand Down Expand Up @@ -413,6 +416,32 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
}
}

/// Sorts the lists within UserContentPreferences locally.
///
/// This client-side sorting is necessary due to a backend limitation that
/// does not support sorting for saved or followed content lists. This
/// approach remains efficient as these lists are fetched all at once and
/// are kept small by user account-type limits.
UserContentPreferences _sortPreferences(UserContentPreferences preferences) {
// Sort saved headlines by updatedAt descending (newest first)
final sortedHeadlines = List<Headline>.from(preferences.savedHeadlines)
..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));

// Sort followed topics by name ascending
final sortedTopics = List<Topic>.from(preferences.followedTopics)
..sort((a, b) => a.name.compareTo(b.name));

// Sort followed sources by name ascending
final sortedSources = List<Source>.from(preferences.followedSources)
..sort((a, b) => a.name.compareTo(b.name));

return preferences.copyWith(
savedHeadlines: sortedHeadlines,
followedTopics: sortedTopics,
followedSources: sortedSources,
);
}

@override
Future<void> close() {
_userSubscription.cancel();
Expand Down
6 changes: 3 additions & 3 deletions lib/account/bloc/available_sources_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:core/core.dart' show HttpException, Source;
import 'package:core/core.dart';
import 'package:data_repository/data_repository.dart';
import 'package:equatable/equatable.dart';

Expand Down Expand Up @@ -33,9 +33,9 @@ class AvailableSourcesBloc
emit(state.copyWith(status: AvailableSourcesStatus.loading));
try {
// Assuming readAll without parameters fetches all items.
// Add pagination if necessary for very large datasets.
// TODO(fulleni): Add pagination if necessary for very large datasets.
final response = await _sourcesRepository.readAll(
// limit: _sourcesLimit,
sort: [const SortOption('name')],
);
emit(
state.copyWith(
Expand Down
5 changes: 4 additions & 1 deletion lib/account/bloc/available_topics_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class AvailableTopicsBloc
}
emit(state.copyWith(status: AvailableTopicsStatus.loading));
try {
final response = await _topicsRepository.readAll();
// TODO(fulleni): Add pagination if necessary for very large datasets.
final response = await _topicsRepository.readAll(
sort: [const SortOption('name')],
);
emit(
state.copyWith(
status: AvailableTopicsStatus.success,
Expand Down
2 changes: 2 additions & 0 deletions lib/entity_details/bloc/entity_details_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class EntityDetailsBloc extends Bloc<EntityDetailsEvent, EntityDetailsState> {
final headlineResponse = await _headlinesRepository.readAll(
filter: filter,
pagination: const PaginationOptions(limit: _headlinesLimit),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final currentUser = _appBloc.state.user;
Expand Down Expand Up @@ -207,6 +208,7 @@ class EntityDetailsBloc extends Bloc<EntityDetailsEvent, EntityDetailsState> {
limit: _headlinesLimit,
cursor: state.headlinesCursor,
),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final currentUser = _appBloc.state.user;
Expand Down
4 changes: 3 additions & 1 deletion lib/headline-details/bloc/similar_headlines_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:core/core.dart' show Headline, HttpException, PaginationOptions;
import 'package:core/core.dart'
show Headline, HttpException, PaginationOptions, SortOption, SortOrder;
import 'package:data_repository/data_repository.dart';
import 'package:equatable/equatable.dart';

Expand Down Expand Up @@ -31,6 +32,7 @@ class SimilarHeadlinesBloc

final response = await _headlinesRepository.readAll(
filter: filter,
sort: [const SortOption('updatedAt', SortOrder.desc)],
pagination: const PaginationOptions(
limit:
_similarHeadlinesLimit +
Expand Down
2 changes: 2 additions & 0 deletions lib/headlines-feed/bloc/countries_filter_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CountriesFilterBloc
try {
final response = await _countriesRepository.readAll(
pagination: const PaginationOptions(limit: _countriesLimit),
sort: [const SortOption('name', SortOrder.desc)],
);
emit(
state.copyWith(
Expand Down Expand Up @@ -85,6 +86,7 @@ class CountriesFilterBloc
limit: _countriesLimit,
cursor: state.cursor,
),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);
emit(
state.copyWith(
Expand Down
4 changes: 4 additions & 0 deletions lib/headlines-feed/bloc/headlines_feed_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
limit: _headlinesFetchLimit,
cursor: state.cursor,
),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final newProcessedFeedItems = _feedInjectorService.injectItems(
Expand Down Expand Up @@ -147,6 +148,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
final headlineResponse = await _headlinesRepository.readAll(
filter: _buildFilter(state.filter),
pagination: const PaginationOptions(limit: _headlinesFetchLimit),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final processedFeedItems = _feedInjectorService.injectItems(
Expand Down Expand Up @@ -209,6 +211,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
final headlineResponse = await _headlinesRepository.readAll(
filter: _buildFilter(event.filter),
pagination: const PaginationOptions(limit: _headlinesFetchLimit),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final processedFeedItems = _feedInjectorService.injectItems(
Expand Down Expand Up @@ -269,6 +272,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {

final headlineResponse = await _headlinesRepository.readAll(
pagination: const PaginationOptions(limit: _headlinesFetchLimit),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);

final processedFeedItems = _feedInjectorService.injectItems(
Expand Down
2 changes: 2 additions & 0 deletions lib/headlines-feed/bloc/topics_filter_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class TopicsFilterBloc extends Bloc<TopicsFilterEvent, TopicsFilterState> {
try {
final response = await _topicsRepository.readAll(
pagination: const PaginationOptions(limit: _topicsLimit),
sort: [const SortOption('name', SortOrder.desc)],
);
emit(
state.copyWith(
Expand Down Expand Up @@ -87,6 +88,7 @@ class TopicsFilterBloc extends Bloc<TopicsFilterEvent, TopicsFilterState> {
limit: _topicsLimit,
cursor: state.cursor,
),
sort: [const SortOption('name', SortOrder.desc)],
);
emit(
state.copyWith(
Expand Down
6 changes: 6 additions & 0 deletions lib/headlines-search/bloc/headlines_search_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class HeadlinesSearchBloc
limit: _limit,
cursor: successState.cursor,
),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);
// Cast to List<Headline> for the injector
final headlines = response.items.cast<Headline>();
Expand Down Expand Up @@ -140,6 +141,7 @@ class HeadlinesSearchBloc
limit: _limit,
cursor: successState.cursor,
),
sort: [const SortOption('name', SortOrder.asc)],
);
emit(
successState.copyWith(
Expand All @@ -157,6 +159,7 @@ class HeadlinesSearchBloc
limit: _limit,
cursor: successState.cursor,
),
sort: [const SortOption('name', SortOrder.asc)],
);
emit(
successState.copyWith(
Expand Down Expand Up @@ -202,6 +205,7 @@ class HeadlinesSearchBloc
rawResponse = await _headlinesRepository.readAll(
filter: {'q': searchTerm},
pagination: const PaginationOptions(limit: _limit),
sort: [const SortOption('updatedAt', SortOrder.desc)],
);
final headlines = rawResponse.items.cast<Headline>();
final currentUser = _appBloc.state.user;
Expand All @@ -226,12 +230,14 @@ class HeadlinesSearchBloc
rawResponse = await _topicRepository.readAll(
filter: {'q': searchTerm},
pagination: const PaginationOptions(limit: _limit),
sort: [const SortOption('name', SortOrder.asc)],
);
processedItems = rawResponse.items.cast<FeedItem>();
case ContentType.source:
rawResponse = await _sourceRepository.readAll(
filter: {'q': searchTerm},
pagination: const PaginationOptions(limit: _limit),
sort: [const SortOption('name', SortOrder.asc)],
);
processedItems = rawResponse.items.cast<FeedItem>();
default:
Expand Down
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/services/spl
import 'package:flutter_news_app_mobile_client_full_source_code/bootstrap.dart';

// Define the current application environment (production/development/demo).
const appEnvironment = AppEnvironment.demo;
const appEnvironment = AppEnvironment.development;

void main() async {
final appConfig = switch (appEnvironment) {
Expand Down
Loading