tooot/src/components/Timeline/Refresh.tsx

315 lines
8.5 KiB
TypeScript
Raw Normal View History

2022-06-01 23:13:43 +02:00
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
import { PagedResponse } from '@utils/api/helpers'
import {
queryFunctionTimeline,
QueryKeyTimeline,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { setAccountStorage } from '@utils/storage/actions'
2022-06-01 23:13:43 +02:00
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useRef, useState } from 'react'
2022-06-01 23:13:43 +02:00
import { useTranslation } from 'react-i18next'
2023-01-06 22:58:01 +01:00
import { FlatList, Platform, Text, View } from 'react-native'
2022-06-01 23:13:43 +02:00
import Animated, {
Extrapolate,
interpolate,
runOnJS,
2023-01-30 00:25:46 +01:00
SharedValue,
2022-06-01 23:13:43 +02:00
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
export interface Props {
flRef: RefObject<FlatList<any>>
2022-06-01 23:13:43 +02:00
queryKey: QueryKeyTimeline
2023-01-30 13:40:43 +01:00
isFetchingPrev: SharedValue<boolean>
2023-01-30 00:25:46 +01:00
setFetchedCount: React.Dispatch<React.SetStateAction<number | null>>
2023-01-30 13:40:43 +01:00
scrollY: SharedValue<number>
fetchingType: SharedValue<0 | 1 | 2>
2022-06-01 23:13:43 +02:00
disableRefresh?: boolean
readMarker?: 'read_marker_following'
2022-06-01 23:13:43 +02:00
}
2023-01-07 12:10:41 +01:00
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
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)
2022-06-01 23:13:43 +02:00
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
2023-01-30 13:40:43 +01:00
isFetchingPrev,
2023-01-30 00:25:46 +01:00
setFetchedCount,
2022-06-01 23:13:43 +02:00
scrollY,
fetchingType,
disableRefresh = false,
readMarker
2022-06-01 23:13:43 +02:00
}) => {
if (Platform.OS !== 'ios') {
return null
}
if (disableRefresh) {
return null
}
const PREV_PER_BATCH = 1
const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>()
const prevStatusId = useRef<Mastodon.Status['id']>()
const queryClient = useQueryClient()
2023-02-02 14:15:37 +01:00
const { refetch, isFetched } = useTimelineQuery({ ...queryKey[1] })
2022-06-01 23:13:43 +02:00
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const [textRight, setTextRight] = useState(0)
const arrowY = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
scrollY.value,
[0, SEPARATION_Y_1],
[
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
],
Extrapolate.CLAMP
)
}
]
}))
const arrowTop = useAnimatedStyle(() => ({
marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0)
2022-06-01 23:13:43 +02:00
}))
const arrowStage = useSharedValue(0)
useAnimatedReaction(
() => {
2023-01-30 13:40:43 +01:00
if (isFetchingPrev.value) {
2022-06-01 23:13:43 +02:00
return false
}
switch (arrowStage.value) {
case 0:
if (scrollY.value < SEPARATION_Y_1) {
arrowStage.value = 1
return true
}
return false
case 1:
if (scrollY.value < SEPARATION_Y_2) {
arrowStage.value = 2
return true
}
if (scrollY.value > SEPARATION_Y_1) {
arrowStage.value = 0
return false
}
return false
case 2:
if (scrollY.value > SEPARATION_Y_2) {
arrowStage.value = 1
return false
}
return false
}
},
data => {
2023-02-02 14:15:37 +01:00
if (data && isFetched) {
2022-06-01 23:13:43 +02:00
runOnJS(haptics)('Light')
}
2023-02-02 14:15:37 +01:00
},
[isFetched]
2022-06-01 23:13:43 +02:00
)
2023-01-06 22:58:01 +01:00
2023-01-07 12:10:41 +01:00
const fetchAndScrolled = useSharedValue(false)
const runFetchPrevious = async () => {
2023-01-30 13:40:43 +01:00
if (isFetchingPrev.value) return
2023-01-06 22:58:01 +01:00
const firstPage =
queryClient.getQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey)?.pages[0]
2023-01-30 13:40:43 +01:00
isFetchingPrev.value = true
2023-01-09 22:28:53 +01:00
prevStatusId.current = firstPage?.body[0]?.id
2023-01-06 22:58:01 +01:00
await queryFunctionTimeline({
queryKey,
2023-01-07 18:01:08 +01:00
pageParam: firstPage?.links?.prev,
meta: {}
2023-01-06 22:58:01 +01:00
})
2023-01-30 13:40:43 +01:00
.then(async res => {
2023-01-30 00:25:46 +01:00
setFetchedCount(res.body.length)
2023-01-07 12:15:07 +01:00
if (!res.body.length) return
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
2023-01-07 12:10:41 +01:00
prevCache.current = res.body.slice(0, -PREV_PER_BATCH)
return {
...old,
2023-01-07 12:10:41 +01:00
pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages]
}
})
2023-01-07 12:10:41 +01:00
return res.body.length - PREV_PER_BATCH
})
.then(async nextLength => {
2023-01-07 12:15:07 +01:00
if (!nextLength) {
2023-01-30 13:40:43 +01:00
isFetchingPrev.value = false
2023-01-07 12:15:07 +01:00
return
}
2023-01-07 12:10:41 +01:00
for (let [index] of Array(Math.ceil(nextLength / PREV_PER_BATCH)).entries()) {
if (!fetchAndScrolled.value && index < 3 && scrollY.value > 15) {
fetchAndScrolled.value = true
flRef.current?.scrollToOffset({ offset: scrollY.value - 15, animated: true })
}
2023-02-11 22:04:32 +01:00
await new Promise<void>(promise => setTimeout(promise, 64))
2023-01-07 12:10:41 +01:00
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) {
2023-01-11 21:51:28 +01:00
page.body.unshift(...insert)
return page
2023-01-07 12:10:41 +01:00
} else {
return page
}
} else {
return page
}
})
}
})
}
2023-01-30 13:40:43 +01:00
isFetchingPrev.value = false
2023-01-07 12:10:41 +01:00
})
}
const runFetchLatest = async () => {
queryClient.invalidateQueries(queryKey)
if (readMarker) {
setAccountStorage([{ key: readMarker, value: undefined }])
}
2023-01-30 00:36:16 +01:00
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
return {
pages: [old.pages[0]],
pageParams: [old.pageParams[0]]
}
})
2023-01-06 22:58:01 +01:00
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
2022-06-01 23:13:43 +02:00
}
useAnimatedReaction(
() => {
return fetchingType.value
},
data => {
fetchingType.value = 0
switch (data) {
case 1:
2023-01-06 22:58:01 +01:00
runOnJS(runFetchPrevious)()
return
2022-06-01 23:13:43 +02:00
case 2:
runOnJS(runFetchLatest)()
2023-01-06 22:58:01 +01:00
return
2022-06-01 23:13:43 +02:00
}
},
[]
)
2023-02-02 14:15:37 +01:00
if (!isFetched) return null
2022-06-01 23:13:43 +02:00
return (
2023-01-06 22:58:01 +01:00
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
}}
>
<View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='arrow-left'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
2023-01-06 22:58:01 +01:00
/>
}
/>
</View>
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.refetch')}
/>
</View>
2022-06-01 23:13:43 +02:00
</Animated.View>
)
}
export default TimelineRefresh