diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 9d41b3fd..2dbde09f 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -1,11 +1,11 @@ import ComponentSeparator from '@components/Separator' import { useScrollToTop } from '@react-navigation/native' -import { useAppDispatch } from '@root/store' -import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { - getInstanceActive, - updateInstanceTimelineLookback -} from '@utils/slices/instancesSlice' + QueryKeyTimeline, + TimelineData, + useTimelineQuery +} from '@utils/queryHooks/timeline' +import { getInstanceActive } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { RefObject, useCallback, useRef } from 'react' @@ -14,22 +14,12 @@ import { FlatListProps, Platform, RefreshControl, - StyleSheet, - ViewabilityConfigCallbackPairs + StyleSheet } from 'react-native' -import Animated, { - useAnimatedScrollHandler, - useSharedValue -} from 'react-native-reanimated' +import { InfiniteData, 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> @@ -46,17 +36,19 @@ const Timeline: React.FC = ({ queryKey, disableRefresh = false, disableInfinity = false, - lookback, customProps }) => { const { colors } = useTheme() + const queryClient = useQueryClient() const { data, refetch, isFetching, isLoading, + fetchPreviousPage, fetchNextPage, + isFetchingPreviousPage, isFetchingNextPage } = useTimelineQuery({ ...queryKey[1], @@ -65,6 +57,12 @@ const Timeline: React.FC = ({ 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 @@ -93,26 +91,6 @@ const Timeline: React.FC = ({ ) const flRef = useRef(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: { @@ -128,27 +106,6 @@ const Timeline: React.FC = ({ } }) - const dispatch = useAppDispatch() - const viewabilityPairs = useRef([ - { - viewabilityConfig: { - minimumViewTime: 10, - viewAreaCoveragePercentThreshold: 10 - }, - onViewableItemsChanged: ({ viewableItems }) => { - lookback && - dispatch( - updateInstanceTimelineLookback({ - [lookback]: { - queryKey, - ids: viewableItems.map(item => item.key).slice(0, 3) - } - }) - ) - } - } - ]) - useScrollToTop(flRef) useSelector(getInstanceActive, (prev, next) => { if (prev !== next) { @@ -158,43 +115,44 @@ const Timeline: React.FC = ({ }) return ( - <> - - + + } + ListEmptyComponent={} + ItemSeparatorComponent={ItemSeparatorComponent} + maintainVisibleContentPosition={{ minIndexForVisible: 0 }} + refreshing={isFetchingPreviousPage} + onRefresh={() => { + if (!disableRefresh && !isFetchingPreviousPage) { + 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 + } + } + ) + fetchPreviousPage() } - ListEmptyComponent={} - ItemSeparatorComponent={ItemSeparatorComponent} - maintainVisibleContentPosition={{ - minIndexForVisible: 0 - }} - {...(lookback && { - viewabilityConfigCallbackPairs: viewabilityPairs.current - })} - {...androidRefreshControl} - {...customProps} - /> - + }} + {...androidRefreshControl} + {...customProps} + /> ) } diff --git a/src/components/Timeline/Lookback.tsx b/src/components/Timeline/Lookback.tsx deleted file mode 100644 index aaa9ee2e..00000000 --- a/src/components/Timeline/Lookback.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import CustomText from '@components/Text' -import { StyleConstants } from '@utils/styles/constants' -import { useTheme } from '@utils/styles/ThemeManager' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { View } from 'react-native' - -const TimelineLookback = React.memo( - () => { - const { t } = useTranslation('componentTimeline') - const { colors } = useTheme() - - return ( - - - {t('lookback.message')} - - - ) - }, - () => true -) - -export default TimelineLookback diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx deleted file mode 100644 index 71b0d266..00000000 --- a/src/components/Timeline/Refresh.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import haptics from '@components/haptics' -import Icon from '@components/Icon' -import CustomText from '@components/Text' -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, 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> - queryKey: QueryKeyTimeline - scrollY: Animated.SharedValue - 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 = ({ - 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: '5' - }, - 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 | 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: [ - { - 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 ( - - - {isFetching ? ( - - - - ) : ( - <> - - - - } - /> - - - - - - )} - - - ) -} - -export default TimelineRefresh