This commit is contained in:
Zhiyuan Zheng 2022-06-01 23:13:43 +02:00
parent fe0a2532f1
commit 5ba3290254
2 changed files with 389 additions and 54 deletions

View File

@ -1,10 +1,6 @@
import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native'
import {
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -16,10 +12,20 @@ import {
RefreshControl,
StyleSheet
} from 'react-native'
import { InfiniteData, useQueryClient } from 'react-query'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, {
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props {
flRef?: RefObject<FlatList<any>>
@ -40,15 +46,12 @@ const Timeline: React.FC<Props> = ({
}) => {
const { colors } = useTheme()
const queryClient = useQueryClient()
const {
data,
refetch,
isFetching,
isLoading,
fetchPreviousPage,
fetchNextPage,
isFetchingPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
@ -57,12 +60,6 @@ const Timeline: React.FC<Props> = ({
ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
}),
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239
limit: '10'
},
getNextPageParam: lastPage =>
lastPage?.links?.next && {
max_id: lastPage.links.next
@ -92,6 +89,27 @@ const Timeline: React.FC<Props> = ({
const flRef = useRef<FlatList>(null)
const scrollY = useSharedValue(0)
const fetchingType = useSharedValue<0 | 1 | 2>(0)
const onScroll = useAnimatedScrollHandler(
{
onScroll: ({ contentOffset: { y } }) => {
scrollY.value = y
},
onEndDrag: ({ contentOffset: { y } }) => {
if (!disableRefresh && !isFetching) {
if (y <= SEPARATION_Y_2) {
fetchingType.value = 2
} else if (y <= SEPARATION_Y_1) {
fetchingType.value = 1
}
}
}
},
[isFetching]
)
const androidRefreshControl = Platform.select({
android: {
refreshControl: (
@ -115,46 +133,40 @@ const Timeline: React.FC<Props> = ({
})
return (
<FlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
}
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
{...(isFetchingPreviousPage && {
maintainVisibleContentPosition: { minIndexForVisible: 0 }
})}
refreshing={isFetchingPreviousPage}
onRefresh={() => {
if (!disableRefresh && !isFetchingPreviousPage) {
queryClient.setQueryData<InfiniteData<TimelineData> | 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
}
}
)
fetchPreviousPage()
<>
<TimelineRefresh
flRef={flRef}
queryKey={queryKey}
scrollY={scrollY}
fetchingType={fetchingType}
disableRefresh={disableRefresh}
/>
<AnimatedFlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
onScroll={onScroll}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
}
}}
{...androidRefreshControl}
{...customProps}
/>
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
maintainVisibleContentPosition={{
minIndexForVisible: 0
}}
{...androidRefreshControl}
{...customProps}
/>
</>
)
}

View File

@ -0,0 +1,323 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import {
QueryKeyTimeline,
TimelineData,
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 { useTranslation } from 'react-i18next'
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
import { InfiniteData, useQueryClient } from 'react-query'
export interface Props {
flRef: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
scrollY: Animated.SharedValue<number>
fetchingType: Animated.SharedValue<0 | 1 | 2>
disableRefresh?: boolean
}
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
)
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
scrollY,
fetchingType,
disableRefresh = false
}) => {
if (Platform.OS !== 'ios') {
return null
}
if (disableRefresh) {
return null
}
const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false)
const {
refetch,
isFetching,
isLoading,
fetchPreviousPage,
hasPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// 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 { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const queryClient = useQueryClient()
const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | 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<InfiniteData<TimelineData> | 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: [
{
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)
}))
const arrowStage = useSharedValue(0)
const onLayout = useCallback(
({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
},
[textRight]
)
useAnimatedReaction(
() => {
if (isFetching) {
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 => {
if (data) {
runOnJS(haptics)('Light')
}
},
[isFetching]
)
const wrapperStartLatest = () => {
fetchingLatestIndex.current = 1
}
useAnimatedReaction(
() => {
return fetchingType.value
},
data => {
fetchingType.value = 0
switch (data) {
case 1:
runOnJS(wrapperStartLatest)()
runOnJS(clearFirstPage)()
runOnJS(fetchPreviousPage)()
break
case 2:
runOnJS(prepareRefetch)()
runOnJS(callRefetch)()
break
}
},
[]
)
const headerPadding = useAnimatedStyle(
() => ({
paddingTop:
fetchingLatestIndex.current !== 0 ||
(isFetching && !isLoading && !isFetchingNextPage)
? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0)
}),
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
)
return (
<Animated.View style={headerPadding}>
<View style={styles.base}>
{isFetching ? (
<View style={styles.container2}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View>
) : (
<>
<View style={styles.container1}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
</Animated.View>
)
}
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