1
0
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:
xmflsct 2023-01-07 18:01:08 +01:00
parent 44f8900902
commit 7db8b26dd9
12 changed files with 92 additions and 112 deletions

View File

@ -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()

View File

@ -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 => {

View File

@ -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 })
}
} }
}) })

View File

@ -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: () => {

View File

@ -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)

View File

@ -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())
} }

View File

@ -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 }

View File

@ -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())
} }

View File

@ -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 = {

View File

@ -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
}) })
} }

View File

@ -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 }

View File

@ -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[] | [] =>