Fix pagination through empty pages
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user