mirror of
https://github.com/tooot-app/app
synced 2025-04-19 04:37:24 +02:00
Simplify and improve pagination
This commit is contained in:
parent
44f8900902
commit
7db8b26dd9
@ -16,16 +16,7 @@ export interface Props {
|
|||||||
const TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
|
const TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
|
||||||
const { hasNextPage } = useTimelineQuery({
|
const { hasNextPage } = useTimelineQuery({
|
||||||
...queryKey[1],
|
...queryKey[1],
|
||||||
options: {
|
options: { enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'] }
|
||||||
enabled: !disableInfinity,
|
|
||||||
notifyOnChangeProps: ['hasNextPage'],
|
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage?.links?.next && {
|
|
||||||
...(lastPage.links.next.isOffset
|
|
||||||
? { offset: lastPage.links.next.id }
|
|
||||||
: { max_id: lastPage.links.next.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
@ -148,11 +148,7 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||||||
|
|
||||||
await queryFunctionTimeline({
|
await queryFunctionTimeline({
|
||||||
queryKey,
|
queryKey,
|
||||||
pageParam: firstPage?.links?.prev && {
|
pageParam: firstPage?.links?.prev,
|
||||||
...(firstPage.links.prev.isOffset
|
|
||||||
? { offset: firstPage.links.prev.id }
|
|
||||||
: { min_id: firstPage.links.prev.id })
|
|
||||||
},
|
|
||||||
meta: {}
|
meta: {}
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -52,13 +52,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
notifyOnChangeProps: Platform.select({
|
notifyOnChangeProps: Platform.select({
|
||||||
ios: ['dataUpdatedAt', 'isFetching'],
|
ios: ['dataUpdatedAt', 'isFetching'],
|
||||||
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
|
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
|
||||||
}),
|
})
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage?.links?.next && {
|
|
||||||
...(lastPage.links.next.isOffset
|
|
||||||
? { offset: lastPage.links.next.id }
|
|
||||||
: { max_id: lastPage.links.next.id })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -23,17 +23,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
|
|||||||
const { t } = useTranslation(['common', 'screenTabs'])
|
const { t } = useTranslation(['common', 'screenTabs'])
|
||||||
|
|
||||||
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
|
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
|
||||||
const { data, refetch, fetchNextPage, hasNextPage } = useListAccountsQuery({
|
const { data, refetch, fetchNextPage, hasNextPage } = useListAccountsQuery({ ...queryKey[1] })
|
||||||
...queryKey[1],
|
|
||||||
options: {
|
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage?.links?.next && {
|
|
||||||
...(lastPage.links.next.isOffset
|
|
||||||
? { offset: lastPage.links.next.id }
|
|
||||||
: { max_id: lastPage.links.next.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mutation = useListAccountsMutation({
|
const mutation = useListAccountsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
@ -33,12 +33,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
|
|||||||
|
|
||||||
const queryKey: QueryKeyUsers = ['Users', params]
|
const queryKey: QueryKeyUsers = ['Users', params]
|
||||||
const { data, isFetching, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({
|
const { data, isFetching, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({
|
||||||
...queryKey[1],
|
...queryKey[1]
|
||||||
options: {
|
|
||||||
getPreviousPageParam: firstPage =>
|
|
||||||
firstPage.links?.prev?.id && { min_id: firstPage.links.prev.id },
|
|
||||||
getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ctx, handleError, PagedResponse, userAgent } from './helpers'
|
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
|
||||||
|
|
||||||
export type Params = {
|
export type Params = {
|
||||||
method: 'get' | 'post' | 'put' | 'delete'
|
method: 'get' | 'post' | 'put' | 'delete'
|
||||||
@ -49,29 +49,7 @@ const apiGeneral = async <T = unknown>({
|
|||||||
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
|
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
|
||||||
: Object.keys(body).length) && { data: body })
|
: Object.keys(body).length) && { data: body })
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))
|
||||||
let links: {
|
|
||||||
prev?: { id: string; isOffset: boolean }
|
|
||||||
next?: { id: string; isOffset: boolean }
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
if (response.headers?.link) {
|
|
||||||
const linksParsed = response.headers.link.matchAll(
|
|
||||||
new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
|
|
||||||
)
|
|
||||||
for (const link of linksParsed) {
|
|
||||||
switch (link[3]) {
|
|
||||||
case 'prev':
|
|
||||||
links.prev = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
links.next = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve({ body: response.data, links })
|
|
||||||
})
|
|
||||||
.catch(handleError())
|
.catch(handleError())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react-native'
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import Constants from 'expo-constants'
|
import Constants from 'expo-constants'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
|
import parse from 'url-parse'
|
||||||
|
|
||||||
const userAgent = {
|
const userAgent = {
|
||||||
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
|
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
|
||||||
@ -64,10 +65,42 @@ const handleError =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseHeaderLinks = (headerLink?: string): PagedResponse['links'] => {
|
||||||
|
if (!headerLink) return undefined
|
||||||
|
|
||||||
|
const links: PagedResponse['links'] = {}
|
||||||
|
|
||||||
|
const linkParsed = [...headerLink.matchAll(/<(\S+?)>; *rel="(next|prev)"/gi)]
|
||||||
|
for (const link of linkParsed) {
|
||||||
|
const queries = parse(link[1], true).query
|
||||||
|
const isOffset = !!queries.offset?.length
|
||||||
|
|
||||||
|
switch (link[2]) {
|
||||||
|
case 'prev':
|
||||||
|
const prevId = isOffset ? queries.offset : queries.min_id
|
||||||
|
if (prevId) links.prev = isOffset ? { offset: prevId } : { min_id: prevId }
|
||||||
|
break
|
||||||
|
case 'next':
|
||||||
|
const nextId = isOffset ? queries.offset : queries.max_id
|
||||||
|
if (nextId) links.next = isOffset ? { offset: nextId } : { max_id: nextId }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.prev || links.next) {
|
||||||
|
return links
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LinkFormat = { id: string; isOffset: boolean }
|
type LinkFormat = { id: string; isOffset: boolean }
|
||||||
export type PagedResponse<T = unknown> = {
|
export type PagedResponse<T = unknown> = {
|
||||||
body: T
|
body: T
|
||||||
links?: { prev?: LinkFormat; next?: LinkFormat }
|
links?: {
|
||||||
|
prev?: { min_id: string } | { offset: string }
|
||||||
|
next?: { max_id: string } | { offset: string }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ctx, handleError, userAgent }
|
export { ctx, handleError, userAgent }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { getAccountDetails } from '@utils/storage/actions'
|
import { getAccountDetails } from '@utils/storage/actions'
|
||||||
import axios, { AxiosRequestConfig } from 'axios'
|
import axios, { AxiosRequestConfig } from 'axios'
|
||||||
import { ctx, handleError, PagedResponse, userAgent } from './helpers'
|
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
|
||||||
|
|
||||||
export type Params = {
|
export type Params = {
|
||||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
||||||
@ -57,29 +57,7 @@ const apiInstance = async <T = unknown>({
|
|||||||
...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
|
...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
|
||||||
...extras
|
...extras
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))
|
||||||
let links: {
|
|
||||||
prev?: { id: string; isOffset: boolean }
|
|
||||||
next?: { id: string; isOffset: boolean }
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
if (response.headers?.link) {
|
|
||||||
const linksParsed = response.headers.link.matchAll(
|
|
||||||
new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
|
|
||||||
)
|
|
||||||
for (const link of linksParsed) {
|
|
||||||
switch (link[3]) {
|
|
||||||
case 'prev':
|
|
||||||
links.prev = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
links.next = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve({ body: response.data, links })
|
|
||||||
})
|
|
||||||
.catch(handleError())
|
.catch(handleError())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
import { PagedResponse } from '@utils/api/helpers'
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
|
import { infinitePageParams } from './utils'
|
||||||
|
|
||||||
export type QueryKeyLists = ['Lists']
|
export type QueryKeyLists = ['Lists']
|
||||||
|
|
||||||
@ -98,10 +99,16 @@ const useListAccountsQuery = ({
|
|||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKeyListAccounts[1] & {
|
}: QueryKeyListAccounts[1] & {
|
||||||
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Account[]>, AxiosError>
|
options?: Omit<
|
||||||
|
UseInfiniteQueryOptions<PagedResponse<Mastodon.Account[]>, AxiosError>,
|
||||||
|
'getPreviousPageParam' | 'getNextPageParam'
|
||||||
|
>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyListAccounts = ['ListAccounts', queryKeyParams]
|
const queryKey: QueryKeyListAccounts = ['ListAccounts', queryKeyParams]
|
||||||
return useInfiniteQuery(queryKey, accountsQueryFunction, options)
|
return useInfiniteQuery(queryKey, accountsQueryFunction, {
|
||||||
|
...options,
|
||||||
|
...infinitePageParams
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountsMutationVarsLists = {
|
type AccountsMutationVarsLists = {
|
||||||
|
@ -11,14 +11,14 @@ import apiInstance from '@utils/api/instance'
|
|||||||
import { featureCheck } from '@utils/helpers/featureCheck'
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import { useNavState } from '@utils/navigation/navigators'
|
import { useNavState } from '@utils/navigation/navigators'
|
||||||
import { queryClient } from '@utils/queryHooks'
|
import { queryClient } from '@utils/queryHooks'
|
||||||
import { StorageAccount } from '@utils/storage/account'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
|
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
import { searchLocalStatus } from './search'
|
import { searchLocalStatus } from './search'
|
||||||
import deleteItem from './timeline/deleteItem'
|
import deleteItem from './timeline/deleteItem'
|
||||||
import editItem from './timeline/editItem'
|
import editItem from './timeline/editItem'
|
||||||
import updateStatusProperty from './timeline/updateStatusProperty'
|
import updateStatusProperty from './timeline/updateStatusProperty'
|
||||||
|
import { infinitePageParams } from './utils'
|
||||||
|
|
||||||
export type QueryKeyTimeline = [
|
export type QueryKeyTimeline = [
|
||||||
'Timeline',
|
'Timeline',
|
||||||
@ -156,7 +156,16 @@ export const queryFunctionTimeline = async ({
|
|||||||
case 'Account':
|
case 'Account':
|
||||||
if (!page.id) return Promise.reject('Timeline query account id not provided')
|
if (!page.id) return Promise.reject('Timeline query account id not provided')
|
||||||
|
|
||||||
if (page.exclude_reblogs) {
|
if (page.only_media) {
|
||||||
|
return apiInstance<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
url: `accounts/${page.id}/statuses`,
|
||||||
|
params: {
|
||||||
|
only_media: 'true',
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (page.exclude_reblogs) {
|
||||||
if (pageParam && pageParam.hasOwnProperty('max_id')) {
|
if (pageParam && pageParam.hasOwnProperty('max_id')) {
|
||||||
return apiInstance<Mastodon.Status[]>({
|
return apiInstance<Mastodon.Status[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -196,8 +205,8 @@ export const queryFunctionTimeline = async ({
|
|||||||
url: `accounts/${page.id}/statuses`,
|
url: `accounts/${page.id}/statuses`,
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
exclude_replies: page.exclude_reblogs.toString(),
|
exclude_replies: false,
|
||||||
only_media: page.only_media.toString()
|
only_media: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -247,14 +256,18 @@ const useTimelineQuery = ({
|
|||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKeyTimeline[1] & {
|
}: QueryKeyTimeline[1] & {
|
||||||
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>
|
options?: Omit<
|
||||||
|
UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>,
|
||||||
|
'getPreviousPageParam' | 'getNextPageParam'
|
||||||
|
>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
|
||||||
return useInfiniteQuery(queryKey, queryFunctionTimeline, {
|
return useInfiniteQuery(queryKey, queryFunctionTimeline, {
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
...options
|
...options,
|
||||||
|
...infinitePageParams
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import apiInstance from '@utils/api/instance'
|
|||||||
import { urlMatcher } from '@utils/helpers/urlMatcher'
|
import { urlMatcher } from '@utils/helpers/urlMatcher'
|
||||||
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
|
import { infinitePageParams } from './utils'
|
||||||
|
|
||||||
export type QueryKeyUsers = ['Users', TabSharedStackParamList['Tab-Shared-Users']]
|
export type QueryKeyUsers = ['Users', TabSharedStackParamList['Tab-Shared-Users']]
|
||||||
|
|
||||||
@ -73,13 +74,19 @@ const useUsersQuery = ({
|
|||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKeyUsers[1] & {
|
}: QueryKeyUsers[1] & {
|
||||||
options?: UseInfiniteQueryOptions<
|
options?: Omit<
|
||||||
|
UseInfiniteQueryOptions<
|
||||||
PagedResponse<Mastodon.Account[]> & { warnIncomplete: boolean; remoteData: boolean },
|
PagedResponse<Mastodon.Account[]> & { warnIncomplete: boolean; remoteData: boolean },
|
||||||
AxiosError
|
AxiosError
|
||||||
|
>,
|
||||||
|
'getPreviousPageParam' | 'getNextPageParam'
|
||||||
>
|
>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyUsers = ['Users', { ...queryKeyParams }]
|
const queryKey: QueryKeyUsers = ['Users', { ...queryKeyParams }]
|
||||||
return useInfiniteQuery(queryKey, queryFunction, options)
|
return useInfiniteQuery(queryKey, queryFunction, {
|
||||||
|
...options,
|
||||||
|
...infinitePageParams
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useUsersQuery }
|
export { useUsersQuery }
|
||||||
|
@ -2,10 +2,8 @@ import { InfiniteData } from '@tanstack/react-query'
|
|||||||
import { PagedResponse } from '@utils/api/helpers'
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
|
|
||||||
export const infinitePageParams = {
|
export const infinitePageParams = {
|
||||||
getPreviousPageParam: (firstPage: PagedResponse<any>) =>
|
getPreviousPageParam: (firstPage: PagedResponse<any>) => firstPage.links?.prev,
|
||||||
firstPage.links?.prev && { min_id: firstPage.links.next },
|
getNextPageParam: (lastPage: PagedResponse<any>) => lastPage.links?.next
|
||||||
getNextPageParam: (lastPage: PagedResponse<any>) =>
|
|
||||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>
|
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user