Fix pagination through empty pages

This commit is contained in:
rafaeleyng
2026-08-11 16:21:13 -03:00
parent aafcb2eeff
commit d0e8900276
31 changed files with 267 additions and 138 deletions
+3 -3
View File
@@ -1363,7 +1363,7 @@
"count": 2
},
"typescript/no-floating-promises": {
"count": 2
"count": 1
},
"typescript/no-unsafe-member-access": {
"count": 2
@@ -1398,7 +1398,7 @@
"count": 3
},
"typescript/no-floating-promises": {
"count": 3
"count": 2
},
"typescript/no-unsafe-member-access": {
"count": 3
@@ -1802,4 +1802,4 @@
"count": 3
}
}
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ export class CustomFeedAPI implements FeedAPI {
res.data.feed = res.data.feed.slice(0, limit)
}
return {
cursor: res.data.feed.length ? res.data.cursor : undefined,
cursor: res.data.cursor,
feed: res.data.feed,
}
}
+1 -3
View File
@@ -42,10 +42,8 @@ export class LikesFeedAPI implements FeedAPI {
limit,
})
if (res.success) {
// HACKFIX: the API incorrectly returns a cursor when there are no items -sfn
const isEmptyPage = res.data.feed.length === 0
return {
cursor: isEmptyPage ? undefined : res.data.cursor,
cursor: res.data.cursor,
feed: res.data.feed,
}
}
+1 -2
View File
@@ -213,10 +213,9 @@ class MergeFeedSource {
const res = await this._getFeed(this.cursor, n)
if (res.success) {
this.cursor = res.data.cursor
this.hasMore = Boolean(this.cursor)
if (res.data.feed.length) {
this.queue = this.queue.concat(res.data.feed)
} else {
this.hasMore = false
}
} else {
this.hasMore = false
+9 -1
View File
@@ -13,6 +13,7 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/components/Toast'
@@ -22,7 +23,7 @@ export const RQKEY_getNotificationDeclaration = ['notification-declaration']
export function useActivitySubscriptionsQuery() {
const agent = useAgent()
return useInfiniteQuery({
const query = useInfiniteQuery({
queryKey: RQKEY_getActivitySubscriptions,
queryFn: async ({pageParam}) => {
const response =
@@ -34,6 +35,13 @@ export function useActivitySubscriptionsQuery() {
initialPageParam: undefined as string | undefined,
getNextPageParam: prev => prev.cursor,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.subscriptions.length,
0,
) ?? 0
useAutoPagination(query, itemCount, 50)
return query
}
export function useNotificationDeclarationQuery() {
+7 -1
View File
@@ -8,6 +8,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
export const RQKEY_ROOT = 'actor-search'
@@ -29,7 +30,7 @@ export function useActorSearch({
limit?: number
}) {
const agent = useAgent()
return useInfiniteQuery<
const result = useInfiniteQuery<
AppBskyActorSearchActors.OutputSchema,
Error,
InfiniteData<AppBskyActorSearchActors.OutputSchema>,
@@ -52,6 +53,11 @@ export function useActorSearch({
placeholderData: maintainData ? keepPreviousData : undefined,
select,
})
const itemCount =
result.data?.pages.reduce((count, page) => count + page.actors.length, 0) ??
0
useAutoPagination(result, itemCount, limit)
return result
}
function select(data: InfiniteData<AppBskyActorSearchActors.OutputSchema>) {
+21 -4
View File
@@ -1,7 +1,10 @@
import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 10
export const RQKEY_ROOT = 'actor-starter-packs'
export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
@@ -19,12 +22,12 @@ export function useActorStarterPacksQuery({
}) {
const agent = useAgent()
return useInfiniteQuery({
const query = useInfiniteQuery({
queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({
actor: did!,
limit: 10,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
@@ -33,6 +36,13 @@ export function useActorStarterPacksQuery({
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.starterPacks.length,
0,
) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function useActorStarterPacksWithMembershipsQuery({
@@ -44,12 +54,12 @@ export function useActorStarterPacksWithMembershipsQuery({
}) {
const agent = useAgent()
return useInfiniteQuery({
const query = useInfiniteQuery({
queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
actor: did!,
limit: 10,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
@@ -58,6 +68,13 @@ export function useActorStarterPacksWithMembershipsQuery({
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.starterPacksWithMembership.length,
0,
) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export async function invalidateActorStarterPacksQuery({
@@ -15,6 +15,7 @@ import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
useAutoPagination,
} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
@@ -25,7 +26,7 @@ export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot]
export function useBookmarksQuery() {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyBookmarkGetBookmarks.OutputSchema,
Error,
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>,
@@ -42,6 +43,13 @@ export function useBookmarksQuery() {
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.bookmarks.length,
0,
) ?? 0
useAutoPagination(query, itemCount, 50)
return query
}
export async function truncateAndInvalidate(qc: QueryClient) {
+7 -1
View File
@@ -6,6 +6,7 @@ import {
useQuery,
} from '@tanstack/react-query'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {type Match} from '#/components/contacts/state'
import type * as bsky from '#/types/bsky'
@@ -32,7 +33,7 @@ export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches']
export function useContactsMatchesQuery() {
const agent = useAgent()
return useInfiniteQuery({
const query = useInfiniteQuery({
queryKey: findContactsGetMatchesQueryKey,
queryFn: async ({pageParam}) => {
const matches = await agent.app.bsky.contact.getMatches({
@@ -44,6 +45,11 @@ export function useContactsMatchesQuery() {
getNextPageParam: lastPage => lastPage.cursor,
staleTime: STALE.MINUTES.ONE,
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.matches.length, 0) ??
0
useAutoPagination(query, itemCount, 50)
return query
}
export function optimisticRemoveMatch(queryClient: QueryClient, did: string) {
+6 -1
View File
@@ -13,6 +13,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
@@ -25,7 +26,7 @@ export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT_ALL, uri]
export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetList.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetList.OutputSchema>,
@@ -46,6 +47,10 @@ export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
getNextPageParam: lastPage => lastPage.cursor,
enabled: Boolean(uri),
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.items.length, 0) ?? 0
useAutoPagination(query, itemCount, limit)
return query
}
export function useAllListMembersQuery(uri?: string) {
+9 -2
View File
@@ -9,7 +9,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {createQueryKey} from '#/state/queries/util'
import {createQueryKey, useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
export type ListWithMembership =
@@ -28,7 +28,7 @@ export function useListsWithMembershipQuery({
}) {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetListsWithMembership.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>,
@@ -48,6 +48,13 @@ export function useListsWithMembershipQuery({
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.listsWithMembership.length,
0,
) ?? 0
useAutoPagination(query, itemCount, 50)
return query
}
export function updateListMembershipOptimistically({
+9 -2
View File
@@ -7,14 +7,16 @@ import {
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAutoPagination} from './util'
const RQKEY_ROOT = 'my-blocked-accounts'
const PAGE_SIZE = 30
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetBlocks.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>,
@@ -24,7 +26,7 @@ export function useMyBlockedAccountsQuery() {
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getBlocks({
limit: 30,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
@@ -32,6 +34,11 @@ export function useMyBlockedAccountsQuery() {
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.blocks.length, 0) ??
0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+8 -2
View File
@@ -7,14 +7,16 @@ import {
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAutoPagination} from './util'
const RQKEY_ROOT = 'my-muted-accounts'
const PAGE_SIZE = 30
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetMutes.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetMutes.OutputSchema>,
@@ -24,7 +26,7 @@ export function useMyMutedAccountsQuery() {
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getMutes({
limit: 30,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
@@ -32,6 +34,10 @@ export function useMyMutedAccountsQuery() {
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.mutes.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+5 -49
View File
@@ -16,7 +16,7 @@
* 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead.
*/
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {useCallback, useMemo, useRef} from 'react'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
@@ -40,6 +40,7 @@ import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
useAutoPagination,
} from '../util'
import {type FeedPage} from './types'
import {useUnreadNotificationsApi} from './unread'
@@ -221,54 +222,9 @@ export function useNotificationFeedQuery(opts: {
),
})
// The server may end up returning an empty page, a page with too few items,
// or a page with items that end up getting filtered out. When we fetch pages,
// we'll keep track of how many items we actually hope to see. If the server
// doesn't return enough items, we're going to continue asking for more items.
const lastItemCount = useRef(0)
const wantedItemCount = useRef(0)
const autoPaginationAttemptCount = useRef(0)
useEffect(() => {
const {data, isLoading, isRefetching, isFetchingNextPage, hasNextPage} =
query
// Count the items that we already have.
let itemCount = 0
for (const page of data?.pages || []) {
itemCount += page.items.length
}
// If items got truncated, reset the state we're tracking below.
if (itemCount !== lastItemCount.current) {
if (itemCount < lastItemCount.current) {
wantedItemCount.current = itemCount
}
lastItemCount.current = itemCount
}
// Now track how many items we really want, and fetch more if needed.
if (isLoading || isRefetching) {
// During the initial fetch, we want to get an entire page's worth of items.
wantedItemCount.current = PAGE_SIZE
} else if (isFetchingNextPage) {
if (itemCount > wantedItemCount.current) {
// We have more items than wantedItemCount, so wantedItemCount must be out of date.
// Some other code must have called fetchNextPage(), for example, from onEndReached.
// Adjust the wantedItemCount to reflect that we want one more full page of items.
wantedItemCount.current = itemCount + PAGE_SIZE
}
} else if (hasNextPage) {
// At this point we're not fetching anymore, so it's time to make a decision.
// If we didn't receive enough items from the server, paginate again until we do.
if (itemCount < wantedItemCount.current) {
autoPaginationAttemptCount.current++
if (autoPaginationAttemptCount.current < 50 /* failsafe */) {
query.fetchNextPage()
}
} else {
autoPaginationAttemptCount.current = 0
}
}
}, [query])
const itemCount =
query.data?.pages.reduce((count, page) => count + page.items.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
+8 -50
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {useCallback, useMemo, useRef} from 'react'
import {AppState} from 'react-native'
import {
type AppBskyActorDefs,
@@ -43,6 +43,7 @@ import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
useAutoPagination,
} from './util'
type ActorDid = string
@@ -362,56 +363,13 @@ export function usePostFeedQuery(
),
})
// The server may end up returning an empty page, a page with too few items,
// or a page with items that end up getting filtered out. When we fetch pages,
// we'll keep track of how many items we actually hope to see. If the server
// doesn't return enough items, we're going to continue asking for more items.
const lastItemCount = useRef(0)
const wantedItemCount = useRef(0)
const autoPaginationAttemptCount = useRef(0)
useEffect(() => {
const {data, isLoading, isRefetching, isFetchingNextPage, hasNextPage} =
query
// Count the items that we already have.
let itemCount = 0
for (const page of data?.pages || []) {
for (const slice of page.slices) {
itemCount += slice.items.length
}
let itemCount = 0
for (const page of query.data?.pages || []) {
for (const slice of page.slices) {
itemCount += slice.items.length
}
// If items got truncated, reset the state we're tracking below.
if (itemCount !== lastItemCount.current) {
if (itemCount < lastItemCount.current) {
wantedItemCount.current = itemCount
}
lastItemCount.current = itemCount
}
// Now track how many items we really want, and fetch more if needed.
if (isLoading || isRefetching) {
// During the initial fetch, we want to get an entire page's worth of items.
wantedItemCount.current = MIN_POSTS
} else if (isFetchingNextPage) {
if (itemCount > wantedItemCount.current) {
// We have more items than wantedItemCount, so wantedItemCount must be out of date.
// Some other code must have called fetchNextPage(), for example, from onEndReached.
// Adjust the wantedItemCount to reflect that we want one more full page of items.
wantedItemCount.current = itemCount + MIN_POSTS
}
} else if (hasNextPage) {
// At this point we're not fetching anymore, so it's time to make a decision.
// If we didn't receive enough items from the server, paginate again until we do.
if (itemCount < wantedItemCount.current) {
autoPaginationAttemptCount.current++
if (autoPaginationAttemptCount.current < 50 /* failsafe */) {
query.fetchNextPage()
}
} else {
autoPaginationAttemptCount.current = 0
}
}
}, [query])
}
useAutoPagination(query, itemCount, MIN_POSTS)
return query
}
+6 -2
View File
@@ -8,7 +8,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {createQueryKey} from '#/state/queries/util'
import {createQueryKey, useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
@@ -20,7 +20,7 @@ export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function useLikedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyFeedGetLikes.OutputSchema,
Error,
InfiniteData<AppBskyFeedGetLikes.OutputSchema>,
@@ -40,6 +40,10 @@ export function useLikedByQuery(resolvedUri: string | undefined) {
getNextPageParam: lastPage => lastPage.cursor,
enabled: !!resolvedUri,
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.likes.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
/**
+6 -1
View File
@@ -17,6 +17,7 @@ import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
useAutoPagination,
} from './util'
const PAGE_SIZE = 30
@@ -27,7 +28,7 @@ export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostQuotesQuery(resolvedUri: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyFeedGetQuotes.OutputSchema,
Error,
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>,
@@ -65,6 +66,10 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) {
}
},
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.posts.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+9 -1
View File
@@ -10,6 +10,7 @@ import {
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAutoPagination} from './util'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -20,7 +21,7 @@ export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyFeedGetRepostedBy.OutputSchema,
Error,
InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>,
@@ -40,6 +41,13 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
getNextPageParam: lastPage => lastPage.cursor,
enabled: !!resolvedUri,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.repostedBy.length,
0,
) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+6 -1
View File
@@ -8,6 +8,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useModerationOpts} from '../preferences/moderation-opts'
@@ -25,7 +26,7 @@ export function useProfileFeedgensQuery(
const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyFeedGetActorFeeds.OutputSchema,
Error,
InfiniteData<AppBskyFeedGetActorFeeds.OutputSchema>,
@@ -66,4 +67,8 @@ export function useProfileFeedgensQuery(
}
},
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.feeds.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
+9 -1
View File
@@ -11,6 +11,7 @@ import {
import {useAgent} from '#/state/session'
import {useAnalytics} from '#/analytics'
import {useAutoPagination} from './util'
const DEFAULT_SORT = 'latest'
const PAGE_SIZE = 30
@@ -37,7 +38,7 @@ export function useProfileFollowersQuery(
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetFollowers.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>,
@@ -58,6 +59,13 @@ export function useProfileFollowersQuery(
getNextPageParam: lastPage => lastPage.cursor,
enabled: !!did,
})
const itemCount =
query.data?.pages.reduce(
(count, page) => count + page.followers.length,
0,
) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+7 -1
View File
@@ -9,6 +9,7 @@ import {
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAnalytics} from '#/analytics'
import {useAutoPagination} from './util'
const DEFAULT_SORT = 'latest'
const PAGE_SIZE = 30
@@ -38,7 +39,7 @@ export function useProfileFollowsQuery(
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetFollows.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetFollows.OutputSchema>,
@@ -60,6 +61,11 @@ export function useProfileFollowsQuery(
getNextPageParam: lastPage => lastPage.cursor,
enabled: !!did,
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.follows.length, 0) ??
0
useAutoPagination(query, itemCount, limit || PAGE_SIZE)
return query
}
export function* findAllProfilesInQueryData(
+6 -1
View File
@@ -5,6 +5,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useModerationOpts} from '../preferences/moderation-opts'
@@ -18,7 +19,7 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const agent = useAgent()
return useInfiniteQuery<
const query = useInfiniteQuery<
AppBskyGraphGetLists.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetLists.OutputSchema>,
@@ -55,4 +56,8 @@ export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
}
},
})
const itemCount =
query.data?.pages.reduce((count, page) => count + page.lists.length, 0) ?? 0
useAutoPagination(query, itemCount, PAGE_SIZE)
return query
}
+7 -1
View File
@@ -24,6 +24,7 @@ import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
useAutoPagination,
} from './util'
const searchPostsQueryKeyRoot = 'search-posts'
@@ -64,7 +65,7 @@ export function useSearchPostsV2Query({
result: InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>
} | null>(null)
return useInfiniteQuery<
const result = useInfiniteQuery<
AppBskyFeedSearchPostsV2.OutputSchema,
Error,
InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>,
@@ -170,6 +171,11 @@ export function useSearchPostsV2Query({
[selectArgs],
),
})
const uris = new Set(
result.data?.pages.flatMap(page => page.posts.map(post => post.uri)),
)
useAutoPagination(result, uris.size, 25)
return result
}
export function* findAllPostsInQueryData(
+9 -1
View File
@@ -7,6 +7,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAutoPagination} from '#/state/queries/util'
import {useAgent} from '#/state/session'
export const RQKEY_ROOT = 'starter-pack-search'
@@ -28,7 +29,7 @@ export function useStarterPackSearch({
limit?: number
}) {
const agent = useAgent()
return useInfiniteQuery<
const result = useInfiniteQuery<
AppBskyGraphSearchStarterPacksV2.OutputSchema,
Error,
InfiniteData<AppBskyGraphSearchStarterPacksV2.OutputSchema>,
@@ -51,6 +52,13 @@ export function useStarterPackSearch({
placeholderData: maintainData ? keepPreviousData : undefined,
select,
})
const itemCount =
result.data?.pages.reduce(
(count, page) => count + page.starterPacks.length,
0,
) ?? 0
useAutoPagination(result, itemCount, limit)
return result
}
function select(
+41
View File
@@ -0,0 +1,41 @@
import {renderHook} from '@testing-library/react-native'
import {useAutoPagination} from './util'
function query(overrides: Record<string, unknown> = {}) {
return {
data: {pageParams: [undefined]},
isLoading: false,
isRefetching: false,
isFetchingNextPage: false,
hasNextPage: true,
fetchNextPage: jest.fn().mockResolvedValue(undefined),
...overrides,
}
}
describe('useAutoPagination', () => {
it('fetches another page when visible items are missing', () => {
const value = query()
renderHook(() => useAutoPagination(value, 0, 10))
expect(value.fetchNextPage).toHaveBeenCalledTimes(1)
})
it('stops when the requested number of items is visible', () => {
const value = query()
renderHook(() => useAutoPagination(value, 10, 10))
expect(value.fetchNextPage).not.toHaveBeenCalled()
})
it('stops when the server repeats a cursor', () => {
const value = query({data: {pageParams: [undefined, 'a', 'a']}})
renderHook(() => useAutoPagination(value, 0, 10))
expect(value.fetchNextPage).not.toHaveBeenCalled()
})
})
+52
View File
@@ -1,3 +1,4 @@
import {useEffect, useRef} from 'react'
import {
type AppBskyActorDefs,
AppBskyEmbedRecord,
@@ -14,6 +15,57 @@ import {
import * as bsky from '#/types/bsky'
type AutoPaginationQuery = {
data?: {pageParams: unknown[]}
isLoading: boolean
isRefetching: boolean
isFetchingNextPage: boolean
hasNextPage: boolean
fetchNextPage: () => Promise<unknown>
}
export function useAutoPagination(
query: AutoPaginationQuery,
itemCount: number,
pageSize: number,
) {
const lastItemCount = useRef(0)
const wantedItemCount = useRef(pageSize)
const attemptCount = useRef(0)
useEffect(() => {
if (itemCount !== lastItemCount.current) {
if (itemCount < lastItemCount.current) {
wantedItemCount.current = itemCount
}
lastItemCount.current = itemCount
}
if (query.isLoading || query.isRefetching) {
wantedItemCount.current = pageSize
} else if (query.isFetchingNextPage) {
if (itemCount > wantedItemCount.current) {
wantedItemCount.current = itemCount + pageSize
}
} else if (query.hasNextPage) {
if (itemCount < wantedItemCount.current) {
const pageParams = query.data?.pageParams
const repeatedCursor =
pageParams &&
pageParams.length > 1 &&
Object.is(pageParams.at(-1), pageParams.at(-2))
if (repeatedCursor) return
attemptCount.current++
if (attemptCount.current < 50) {
void query.fetchNextPage()
}
} else {
attemptCount.current = 0
}
}
}, [itemCount, pageSize, query])
}
export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [
string,
T,
+1 -1
View File
@@ -79,7 +79,7 @@ export function ProfileFeedgens({
error,
refetch,
} = useProfileFeedgensQuery(did, opts)
const isEmpty = !isPending && !data?.pages[0]?.feeds.length
const isEmpty = !isPending && !data?.pages.some(page => page.feeds.length)
const {data: preferences} = usePreferencesQuery()
const navigation = useNavigation()
const {currentAccount} = useSession()
+1 -1
View File
@@ -90,7 +90,7 @@ export function ListMembers({
hasNextPage,
isFetchingNextPage,
} = useListMembersQuery(list)
const isEmpty = !isFetching && !data?.pages[0].items.length
const isEmpty = !isFetching && !data?.pages.some(page => page.items.length)
const isOwner =
currentAccount && data?.pages[0].list.creator.did === currentAccount.did
+1 -1
View File
@@ -79,7 +79,7 @@ export function ProfileLists({
error,
refetch,
} = useProfileListsQuery(did, opts)
const isEmpty = !isPending && !data?.pages[0]?.lists.length
const isEmpty = !isPending && !data?.pages.some(page => page.lists.length)
const {data: preferences} = usePreferencesQuery()
const navigation = useNavigation()
const {currentAccount} = useSession()
@@ -36,7 +36,7 @@ export function ModerationBlockedAccounts({}: Props) {
fetchNextPage,
isFetchingNextPage,
} = useMyBlockedAccountsQuery()
const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
const isEmpty = !isFetching && !data?.pages.some(page => page.blocks.length)
const profiles = useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.blocks)
+1 -1
View File
@@ -36,7 +36,7 @@ export function ModerationMutedAccounts({}: Props) {
fetchNextPage,
isFetchingNextPage,
} = useMyMutedAccountsQuery()
const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
const isEmpty = !isFetching && !data?.pages.some(page => page.mutes.length)
const profiles = useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.mutes)