🐛 Fix cursor based loading in timeline cause duplicate data due to not covered the popularity reordered case, close #213

This commit is contained in:
2025-12-27 22:38:41 +08:00
parent 6e7eedc026
commit f541580281
2 changed files with 33 additions and 10 deletions

View File

@@ -8,6 +8,10 @@ abstract class PaginationController<T> {
bool get fetchedAll;
bool get isLoading;
bool get hasMore;
set hasMore(bool value);
String? get cursor;
set cursor(String? value);
FutureOr<List<T>> fetch();
@@ -31,19 +35,31 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
int get fetchedCount => state.value?.length ?? 0;
@override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
bool get fetchedAll =>
!hasMore || (totalCount != null && fetchedCount >= totalCount!);
@override
bool isLoading = false;
@override
FutureOr<List<T>> build() async => fetch();
bool hasMore = true;
@override
String? cursor;
@override
FutureOr<List<T>> build() async {
cursor = null;
return fetch();
}
@override
Future<void> refresh() async {
isLoading = true;
totalCount = null;
state = AsyncData<List<T>>([]);
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
@@ -55,6 +71,7 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
@override
Future<void> fetchFurther() async {
if (fetchedAll) return;
if (isLoading) return;
isLoading = true;
state = AsyncLoading<List<T>>();
@@ -77,7 +94,9 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
// Reset the data
isLoading = true;
totalCount = null;
state = AsyncData<List<T>>([]);
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
currentFilter = filter;
final newState = await AsyncValue.guard<List<T>>(() async {

View File

@@ -20,8 +20,6 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
Future<List<SnTimelineEvent>> fetch() async {
final client = ref.read(apiClientProvider);
final cursor = state.value?.lastOrNull?.createdAt.toUtc().toIso8601String();
final queryParameters = {
if (cursor != null) 'cursor': cursor,
'take': pageSize,
@@ -37,10 +35,16 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
.toList();
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
totalCount =
(state.value?.length ?? 0) + items.length + (hasMore ? pageSize : 0);
hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
// Find the latest createdAt timestamp from all items for cursor-based pagination
// This ensures we get items created before this timestamp, regardless of sort order
if (items.isNotEmpty) {
final latestCreatedAt = items
.where((e) => e.type.startsWith('posts.'))
.map((e) => e.createdAt)
.reduce((a, b) => a.isBefore(b) ? a : b);
cursor = latestCreatedAt.toUtc().toIso8601String();
}
return items;
}