From f541580281fc850d1f33dfa7ec164029957b62e1 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 27 Dec 2025 22:38:41 +0800 Subject: [PATCH] :bug: Fix cursor based loading in timeline cause duplicate data due to not covered the popularity reordered case, close #213 --- lib/pods/paging.dart | 27 +++++++++++++++++++++++---- lib/pods/timeline.dart | 16 ++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/pods/paging.dart b/lib/pods/paging.dart index 75387546..255a4878 100644 --- a/lib/pods/paging.dart +++ b/lib/pods/paging.dart @@ -8,6 +8,10 @@ abstract class PaginationController { bool get fetchedAll; bool get isLoading; + bool get hasMore; + set hasMore(bool value); + String? get cursor; + set cursor(String? value); FutureOr> fetch(); @@ -31,19 +35,31 @@ mixin AsyncPaginationController on AsyncNotifier> 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> build() async => fetch(); + bool hasMore = true; + + @override + String? cursor; + + @override + FutureOr> build() async { + cursor = null; + return fetch(); + } @override Future refresh() async { isLoading = true; totalCount = null; - state = AsyncData>([]); + hasMore = true; + cursor = null; + state = AsyncLoading>(); final newState = await AsyncValue.guard>(() async { return await fetch(); @@ -55,6 +71,7 @@ mixin AsyncPaginationController on AsyncNotifier> @override Future fetchFurther() async { if (fetchedAll) return; + if (isLoading) return; isLoading = true; state = AsyncLoading>(); @@ -77,7 +94,9 @@ mixin AsyncPaginationFilter on AsyncPaginationController // Reset the data isLoading = true; totalCount = null; - state = AsyncData>([]); + hasMore = true; + cursor = null; + state = AsyncLoading>(); currentFilter = filter; final newState = await AsyncValue.guard>(() async { diff --git a/lib/pods/timeline.dart b/lib/pods/timeline.dart index d1d36cf5..fc75dbdc 100644 --- a/lib/pods/timeline.dart +++ b/lib/pods/timeline.dart @@ -20,8 +20,6 @@ class ActivityListNotifier extends AsyncNotifier> Future> 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> .map((e) => SnTimelineEvent.fromJson(e as Map)) .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; }