cfd6d1e07e
Co-authored-by: Eric Bailey <git@esb.lol>
214 lines
6.0 KiB
TypeScript
214 lines
6.0 KiB
TypeScript
import {useEffect, useRef} from 'react'
|
|
import {type AtUri} from '@atproto/syntax'
|
|
import {
|
|
type InfiniteData,
|
|
type QueryClient,
|
|
type QueryKey,
|
|
} from '@tanstack/react-query'
|
|
|
|
import {app} from '#/lexicons'
|
|
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 lastPageParams = useRef(query.data?.pageParams)
|
|
const wantedItemCount = useRef(pageSize)
|
|
const attemptCount = useRef(0)
|
|
|
|
useEffect(() => {
|
|
const cursorOf = (param: unknown) =>
|
|
param && typeof param === 'object' && 'cursor' in param
|
|
? param.cursor
|
|
: param
|
|
const pageParams = query.data?.pageParams
|
|
const previousPageParams = lastPageParams.current
|
|
const continuedPagination =
|
|
pageParams &&
|
|
previousPageParams &&
|
|
pageParams.length > previousPageParams.length &&
|
|
previousPageParams.every((param, index) =>
|
|
Object.is(cursorOf(param), cursorOf(pageParams[index])),
|
|
)
|
|
if (
|
|
pageParams !== previousPageParams &&
|
|
previousPageParams &&
|
|
!continuedPagination
|
|
) {
|
|
wantedItemCount.current = pageSize
|
|
attemptCount.current = 0
|
|
}
|
|
lastPageParams.current = pageParams
|
|
|
|
if (itemCount !== lastItemCount.current) {
|
|
attemptCount.current = 0
|
|
if (itemCount < lastItemCount.current) {
|
|
wantedItemCount.current = Math.max(itemCount, pageSize)
|
|
}
|
|
lastItemCount.current = itemCount
|
|
}
|
|
|
|
if (query.isLoading || query.isRefetching) {
|
|
wantedItemCount.current = pageSize
|
|
attemptCount.current = 0
|
|
} else if (query.isFetchingNextPage) {
|
|
if (itemCount > wantedItemCount.current) {
|
|
wantedItemCount.current = itemCount + pageSize
|
|
}
|
|
} else if (query.hasNextPage) {
|
|
if (itemCount < wantedItemCount.current) {
|
|
const currentCursor = cursorOf(pageParams?.at(-1))
|
|
const repeatedCursor = pageParams
|
|
?.slice(0, -1)
|
|
.some(param => Object.is(cursorOf(param), currentCursor))
|
|
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,
|
|
{
|
|
persistedVersion?: number
|
|
},
|
|
]
|
|
|
|
/**
|
|
* Helper method to ensure consistent query keys and key ordering
|
|
*/
|
|
export function createQueryKey<T extends Record<string, unknown>>(
|
|
/**
|
|
* The query key root. All queries must have a root.
|
|
*/
|
|
root: string,
|
|
/**
|
|
* Any arguments the query depends on, and if changed, should result in the query being refetched.
|
|
*/
|
|
args: T,
|
|
options: {
|
|
/**
|
|
* If provided, this indicates that the query is persisted and the version
|
|
* of the persisted query format.
|
|
*
|
|
* This is used to ensure that when we make breaking changes to the
|
|
* persisted query format, we can increment the version and avoid trying to
|
|
* read old persisted queries with the new format.
|
|
*
|
|
* If you're persisting your queries, you probably want to set `gcTime:
|
|
* GCTIME.INFINITY` for this query, otherwise it'll get busted immediately
|
|
* after being persisted.
|
|
*/
|
|
persistedVersion?: number
|
|
} = {},
|
|
): StructuredQueryKey<T> {
|
|
return [root, args, options] as const
|
|
}
|
|
|
|
export function isQueryPersisted(
|
|
queryKey: QueryKey,
|
|
): queryKey is StructuredQueryKey<Record<string, unknown>> {
|
|
return (
|
|
Array.isArray(queryKey) &&
|
|
queryKey.length === 3 &&
|
|
typeof queryKey[0] === 'string' &&
|
|
typeof queryKey[1] === 'object' &&
|
|
queryKey[1] !== null &&
|
|
typeof queryKey[2] === 'object' &&
|
|
queryKey[2] !== null &&
|
|
'persistedVersion' in queryKey[2] &&
|
|
typeof queryKey[2].persistedVersion === 'number'
|
|
)
|
|
}
|
|
|
|
export async function truncateAndInvalidate<T = any>(
|
|
queryClient: QueryClient,
|
|
queryKey: QueryKey,
|
|
) {
|
|
queryClient.setQueriesData<InfiniteData<T>>({queryKey}, data => {
|
|
if (data) {
|
|
return {
|
|
pageParams: data.pageParams.slice(0, 1),
|
|
pages: data.pages.slice(0, 1),
|
|
}
|
|
}
|
|
return data
|
|
})
|
|
return queryClient.invalidateQueries({queryKey})
|
|
}
|
|
|
|
// Given an AtUri, this function will check if the AtUri matches a
|
|
// hit regardless of whether the AtUri uses a DID or handle as a host.
|
|
//
|
|
// AtUri should be the URI that is being searched for, while currentUri
|
|
// is the URI that is being checked. currentAuthor is the author
|
|
// of the currentUri that is being checked.
|
|
export function didOrHandleUriMatches(
|
|
atUri: AtUri,
|
|
record: {uri: string; author: app.bsky.actor.defs.ProfileViewBasic},
|
|
) {
|
|
if (atUri.host.startsWith('did:')) {
|
|
return atUri.href === record.uri
|
|
}
|
|
|
|
return atUri.host === record.author.handle && record.uri.endsWith(atUri.rkey)
|
|
}
|
|
|
|
export function getEmbeddedPost(
|
|
v: unknown,
|
|
): app.bsky.embed.record.ViewRecord | undefined {
|
|
if (bsky.isType(app.bsky.embed.record.view, v)) {
|
|
if (
|
|
bsky.isType(app.bsky.embed.record.viewRecord, v.record) &&
|
|
bsky.isType(app.bsky.feed.post, v.record.value)
|
|
) {
|
|
return v.record
|
|
}
|
|
}
|
|
if (bsky.isType(app.bsky.embed.recordWithMedia.view, v)) {
|
|
if (
|
|
bsky.isType(app.bsky.embed.record.viewRecord, v.record.record) &&
|
|
bsky.isType(app.bsky.feed.post, v.record.record.value)
|
|
) {
|
|
return v.record.record
|
|
}
|
|
}
|
|
}
|
|
|
|
export function embedViewRecordToPostView(
|
|
v: app.bsky.embed.record.ViewRecord,
|
|
): app.bsky.feed.defs.PostView {
|
|
return {
|
|
uri: v.uri,
|
|
cid: v.cid,
|
|
author: v.author,
|
|
record: v.value,
|
|
indexedAt: v.indexedAt,
|
|
labels: v.labels,
|
|
embed: v.embeds?.[0],
|
|
likeCount: v.likeCount,
|
|
quoteCount: v.quoteCount,
|
|
replyCount: v.replyCount,
|
|
repostCount: v.repostCount,
|
|
}
|
|
}
|