diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx index 8cb0aa9c..5fda1626 100644 --- a/src/components/Timeline/Refresh.tsx +++ b/src/components/Timeline/Refresh.tsx @@ -1,12 +1,17 @@ import haptics from '@components/haptics' import Icon from '@components/Icon' import { InfiniteData, useQueryClient } from '@tanstack/react-query' -import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline' +import { PagedResponse } from '@utils/api/helpers' +import { + queryFunctionTimeline, + QueryKeyTimeline, + useTimelineQuery +} from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { RefObject, useCallback, useRef, useState } from 'react' +import React, { RefObject, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { FlatList, LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native' +import { FlatList, Platform, Text, View } from 'react-native' import { Circle } from 'react-native-animated-spinkit' import Animated, { Extrapolate, @@ -26,7 +31,7 @@ export interface Props { disableRefresh?: boolean } -const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 +const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2 export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2) export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2) @@ -44,86 +49,17 @@ const TimelineRefresh: React.FC = ({ return null } - const fetchingLatestIndex = useRef(0) - const refetchActive = useRef(false) + const PREV_PER_BATCH = 1 + const prevActive = useRef(false) + const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>() + const prevStatusId = useRef() - const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } = - useTimelineQuery({ - ...queryKey[1], - options: { - getPreviousPageParam: firstPage => - firstPage?.links?.prev && { - ...(firstPage.links.prev.isOffset - ? { offset: firstPage.links.prev.id } - : { min_id: firstPage.links.prev.id }), - // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 - limit: '3' - }, - select: data => { - if (refetchActive.current) { - data.pageParams = [data.pageParams[0]] - data.pages = [data.pages[0]] - refetchActive.current = false - } - return data - }, - onSuccess: () => { - if (fetchingLatestIndex.current > 0) { - if (fetchingLatestIndex.current > 5) { - clearFirstPage() - fetchingLatestIndex.current = 0 - } else { - if (hasPreviousPage) { - fetchPreviousPage() - fetchingLatestIndex.current++ - } else { - clearFirstPage() - fetchingLatestIndex.current = 0 - } - } - } - } - } - }) + const queryClient = useQueryClient() + const { refetch, isFetching } = useTimelineQuery({ ...queryKey[1] }) const { t } = useTranslation('componentTimeline') const { colors } = useTheme() - const queryClient = useQueryClient() - const clearFirstPage = () => { - queryClient.setQueryData | undefined>(queryKey, data => { - if (data?.pages[0] && data.pages[0].body.length === 0) { - return { - pages: data.pages.slice(1), - pageParams: data.pageParams.slice(1) - } - } else { - return data - } - }) - } - const prepareRefetch = () => { - refetchActive.current = true - queryClient.setQueryData | undefined>(queryKey, data => { - if (data) { - data.pageParams = [undefined] - const newFirstPage: TimelineData = { body: [] } - for (let page of data.pages) { - // @ts-ignore - newFirstPage.body.push(...page.body) - if (newFirstPage.body.length > 10) break - } - data.pages = [newFirstPage] - } - - return data - }) - } - const callRefetch = async () => { - await refetch() - setTimeout(() => flRef.current?.scrollToOffset({ offset: 1 }), 50) - } - const [textRight, setTextRight] = useState(0) const arrowY = useAnimatedStyle(() => ({ transform: [ @@ -145,14 +81,6 @@ const TimelineRefresh: React.FC = ({ })) const arrowStage = useSharedValue(0) - const onLayout = useCallback( - ({ nativeEvent }: LayoutChangeEvent) => { - if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { - setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) - } - }, - [textRight] - ) useAnimatedReaction( () => { if (isFetching) { @@ -190,8 +118,81 @@ const TimelineRefresh: React.FC = ({ }, [isFetching] ) - const wrapperStartLatest = () => { - fetchingLatestIndex.current = 1 + + const runFetchPrevious = async () => { + if (prevActive.current) return + + const firstPage = + queryClient.getQueryData< + InfiniteData< + PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]> + > + >(queryKey)?.pages[0] + + prevActive.current = true + prevStatusId.current = firstPage?.body[0].id + + await queryFunctionTimeline({ + queryKey, + pageParam: firstPage?.links?.prev && { + ...(firstPage.links.prev.isOffset + ? { offset: firstPage.links.prev.id } + : { min_id: firstPage.links.prev.id }) + }, + meta: {} + }).then(res => { + queryClient.setQueryData< + InfiniteData< + PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]> + > + >(queryKey, old => { + if (!old) return old + + prevCache.current = res.body.slice(0, -PREV_PER_BATCH) + return { ...old, pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages] } + }) + }) + } + useEffect(() => { + const loop = async () => { + for await (const _ of Array(Math.ceil((prevCache.current?.length || 0) / PREV_PER_BATCH))) { + await new Promise(promise => setTimeout(promise, 32)) + queryClient.setQueryData< + InfiniteData< + PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]> + > + >(queryKey, old => { + if (!old) return old + + return { + ...old, + pages: old.pages.map((page, index) => { + if (index === 0) { + const insert = prevCache.current?.slice(-PREV_PER_BATCH) + prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH) + if (insert) { + return { ...page, body: [...insert, ...page.body] } + } else { + return page + } + } else { + return page + } + }) + } + }) + + break + } + prevActive.current = false + } + loop() + }, [prevCache.current]) + + const runFetchLatest = async () => { + queryClient.invalidateQueries(queryKey) + await refetch() + setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50) } useAnimatedReaction( @@ -202,95 +203,84 @@ const TimelineRefresh: React.FC = ({ fetchingType.value = 0 switch (data) { case 1: - runOnJS(wrapperStartLatest)() - runOnJS(clearFirstPage)() - runOnJS(fetchPreviousPage)() - break + runOnJS(runFetchPrevious)() + return case 2: - runOnJS(prepareRefetch)() - runOnJS(callRefetch)() - break + runOnJS(runFetchLatest)() + return } }, [] ) - const headerPadding = useAnimatedStyle( - () => ({ - paddingTop: - fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage) - ? withTiming(StyleConstants.Spacing.M * 2.5) - : withTiming(0) - }), - [fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading] - ) - return ( - - - {isFetching ? ( - - - - ) : ( - <> - - - + + {prevActive.current || isFetching ? ( + + + + ) : ( + <> + + { + if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { + setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) } - /> - - - - - - )} - + }} + children={t('refresh.fetchPreviousPage')} + /> + + } + /> + + + { + if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) { + setTextRight(nativeEvent.layout.x + nativeEvent.layout.width) + } + }} + children={t('refresh.refetch')} + /> + + + )} ) } -const styles = StyleSheet.create({ - base: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - height: CONTAINER_HEIGHT * 2, - alignItems: 'center' - }, - container1: { - flex: 1, - flexDirection: 'row', - height: CONTAINER_HEIGHT - }, - container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' }, - explanation: { - fontSize: StyleConstants.Font.Size.S, - lineHeight: CONTAINER_HEIGHT - } -}) - export default TimelineRefresh diff --git a/src/components/Timeline/index.tsx b/src/components/Timeline/index.tsx index 7121652c..7e4f1c92 100644 --- a/src/components/Timeline/index.tsx +++ b/src/components/Timeline/index.tsx @@ -129,13 +129,11 @@ const Timeline: React.FC = ({ /> ) } - maintainVisibleContentPosition={ - isFetching - ? { - minIndexForVisible: 0 - } - : undefined - } + {...(!isLoading && { + maintainVisibleContentPosition: { + minIndexForVisible: 0 + } + })} {...androidRefreshControl} {...customProps} /> diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index 13e604c1..8364db05 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -74,7 +74,7 @@ const TabSharedToot: React.FC> = ({ const match = urlMatcher(toot.url || toot.uri) const highlightIndex = useRef(0) - const query = useQuery<{ pages: { body: (Mastodon.Status & { _key?: 'cached' })[] }[] }>( + const query = useQuery<{ pages: { body: (Mastodon.Status & { key?: 'cached' })[] }[] }>( queryKey.local, async () => { const context = await apiInstance<{ @@ -106,7 +106,7 @@ const TabSharedToot: React.FC> = ({ } }, { - initialData: { pages: [{ body: [{ ...toot, _level: 0, _key: 'cached' }] }] }, + initialData: { pages: [{ body: [{ ...toot, _level: 0, key: 'cached' }] }] }, enabled: !toot._remote, staleTime: 0, refetchOnMount: true, @@ -178,6 +178,7 @@ const TabSharedToot: React.FC> = ({ }, { enabled: + query.isFetched && ['public', 'unlisted'].includes(toot.visibility) && match?.domain !== getAccountStorage.string('auth.domain'), staleTime: 0, @@ -204,7 +205,7 @@ const TabSharedToot: React.FC> = ({ local => local.uri === remote.uri ) if (localMatch) { - delete localMatch._key + delete localMatch.key return localMatch } else { return { @@ -262,7 +263,7 @@ const TabSharedToot: React.FC> = ({ ref={flRef} scrollEventThrottle={16} windowSize={7} - data={query.data.pages?.[0].body} + data={query.data?.pages?.[0].body} renderItem={({ item, index }) => { const prev = query.data.pages[0].body[index - 1]?._level || 0 const curr = item._level diff --git a/src/utils/api/helpers/index.ts b/src/utils/api/helpers/index.ts index 8185460b..babf3ea5 100644 --- a/src/utils/api/helpers/index.ts +++ b/src/utils/api/helpers/index.ts @@ -67,7 +67,7 @@ const handleError = type LinkFormat = { id: string; isOffset: boolean } export type PagedResponse = { body: T - links: { prev?: LinkFormat; next?: LinkFormat } + links?: { prev?: LinkFormat; next?: LinkFormat } } export { ctx, handleError, userAgent } diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index ba38c591..b85ed23c 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -52,7 +52,10 @@ export type QueryKeyTimeline = [ ) ] -const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext) => { +export const queryFunctionTimeline = async ({ + queryKey, + pageParam +}: QueryFunctionContext) => { const page = queryKey[1] let params: { [key: string]: string } = { limit: 40, ...pageParam } @@ -165,7 +168,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext> = T extends Promise ? U : never -export type TimelineData = Unpromise> +export type TimelineData = Unpromise> const useTimelineQuery = ({ options, ...queryKeyParams @@ -228,7 +231,7 @@ const useTimelineQuery = ({ options?: UseInfiniteQueryOptions, AxiosError> }) => { const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }] - return useInfiniteQuery(queryKey, queryFunction, { + return useInfiniteQuery(queryKey, queryFunctionTimeline, { refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false,