Merge pull request #285 from tooot-app/main

Final test for v4
This commit is contained in:
xmflsct 2022-05-12 00:11:47 +02:00 committed by GitHub
commit b94af5d8bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 472 deletions

View File

@ -31,7 +31,7 @@ PODS:
- EXJSONUtils - EXJSONUtils
- EXNotifications (0.15.2): - EXNotifications (0.15.2):
- ExpoModulesCore - ExpoModulesCore
- Expo (45.0.2): - Expo (45.0.3):
- ExpoModulesCore - ExpoModulesCore
- ExpoCrypto (10.2.0): - ExpoCrypto (10.2.0):
- ExpoModulesCore - ExpoModulesCore
@ -849,7 +849,7 @@ SPEC CHECKSUMS:
EXJSONUtils: 2a74b8f40f1523cc3f92af99c91aa78201737a77 EXJSONUtils: 2a74b8f40f1523cc3f92af99c91aa78201737a77
EXManifests: 0c6134b7b6f3236a93a778c3f44ba1cfb3f9fa3d EXManifests: 0c6134b7b6f3236a93a778c3f44ba1cfb3f9fa3d
EXNotifications: ea9fc56d27d1fee229489c5d8f452c7f367c237e EXNotifications: ea9fc56d27d1fee229489c5d8f452c7f367c237e
Expo: 27c7466cea6c8be416d8e50a0c0faa48cf8b4d1a Expo: 353cd6d2154838c0c8bb1072d3ac89eb535a2ef4
ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17 ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17
ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a
ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8 ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8

View File

@ -46,7 +46,7 @@
"@sentry/react-native": "3.4.2", "@sentry/react-native": "3.4.2",
"@sharcoux/slider": "6.0.3", "@sharcoux/slider": "6.0.3",
"axios": "0.27.2", "axios": "0.27.2",
"expo": "45.0.2", "expo": "45.0.3",
"expo-auth-session": "3.6.0", "expo-auth-session": "3.6.0",
"expo-av": "11.2.3", "expo-av": "11.2.3",
"expo-constants": "^13.1.1", "expo-constants": "^13.1.1",
@ -68,7 +68,7 @@
"expo-updates": "0.13.1", "expo-updates": "0.13.1",
"expo-video-thumbnails": "6.3.0", "expo-video-thumbnails": "6.3.0",
"expo-web-browser": "10.2.0", "expo-web-browser": "10.2.0",
"i18next": "21.8.0", "i18next": "21.8.1",
"li": "1.3.0", "li": "1.3.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"react": "17.0.2", "react": "17.0.2",
@ -118,7 +118,7 @@
"babel-plugin-module-resolver": "4.1.0", "babel-plugin-module-resolver": "4.1.0",
"babel-plugin-transform-remove-console": "6.9.4", "babel-plugin-transform-remove-console": "6.9.4",
"chalk": "4.1.2", "chalk": "4.1.2",
"dotenv": "16.0.0", "dotenv": "16.0.1",
"patch-package": "6.4.7", "patch-package": "6.4.7",
"postinstall-postinstall": "2.1.0", "postinstall-postinstall": "2.1.0",
"react-native-clean-project": "4.0.1", "react-native-clean-project": "4.0.1",

View File

@ -1,11 +1,11 @@
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native' import { useScrollToTop } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { import {
getInstanceActive, QueryKeyTimeline,
updateInstanceTimelineLookback TimelineData,
} from '@utils/slices/instancesSlice' useTimelineQuery
} from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef } from 'react' import React, { RefObject, useCallback, useRef } from 'react'
@ -14,22 +14,12 @@ import {
FlatListProps, FlatListProps,
Platform, Platform,
RefreshControl, RefreshControl,
StyleSheet, StyleSheet
ViewabilityConfigCallbackPairs
} from 'react-native' } from 'react-native'
import Animated, { import { InfiniteData, useQueryClient } from 'react-query'
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty' import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer' import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, {
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props { export interface Props {
flRef?: RefObject<FlatList<any>> flRef?: RefObject<FlatList<any>>
@ -46,17 +36,19 @@ const Timeline: React.FC<Props> = ({
queryKey, queryKey,
disableRefresh = false, disableRefresh = false,
disableInfinity = false, disableInfinity = false,
lookback,
customProps customProps
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const queryClient = useQueryClient()
const { const {
data, data,
refetch, refetch,
isFetching, isFetching,
isLoading, isLoading,
fetchPreviousPage,
fetchNextPage, fetchNextPage,
isFetchingPreviousPage,
isFetchingNextPage isFetchingNextPage
} = useTimelineQuery({ } = useTimelineQuery({
...queryKey[1], ...queryKey[1],
@ -65,6 +57,12 @@ const Timeline: React.FC<Props> = ({
ios: ['dataUpdatedAt', 'isFetching'], ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading'] 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 => getNextPageParam: lastPage =>
lastPage?.links?.next && { lastPage?.links?.next && {
max_id: lastPage.links.next max_id: lastPage.links.next
@ -93,26 +91,6 @@ const Timeline: React.FC<Props> = ({
) )
const flRef = useRef<FlatList>(null) 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({ const androidRefreshControl = Platform.select({
android: { android: {
@ -128,27 +106,6 @@ const Timeline: React.FC<Props> = ({
} }
}) })
const dispatch = useAppDispatch()
const viewabilityPairs = useRef<ViewabilityConfigCallbackPairs>([
{
viewabilityConfig: {
minimumViewTime: 10,
viewAreaCoveragePercentThreshold: 10
},
onViewableItemsChanged: ({ viewableItems }) => {
lookback &&
dispatch(
updateInstanceTimelineLookback({
[lookback]: {
queryKey,
ids: viewableItems.map(item => item.key).slice(0, 3)
}
})
)
}
}
])
useScrollToTop(flRef) useScrollToTop(flRef)
useSelector(getInstanceActive, (prev, next) => { useSelector(getInstanceActive, (prev, next) => {
if (prev !== next) { if (prev !== next) {
@ -158,43 +115,44 @@ const Timeline: React.FC<Props> = ({
}) })
return ( return (
<> <FlatList
<TimelineRefresh ref={customFLRef || flRef}
flRef={flRef} scrollEventThrottle={16}
queryKey={queryKey} windowSize={7}
scrollY={scrollY} data={flattenData}
fetchingType={fetchingType} initialNumToRender={6}
disableRefresh={disableRefresh} maxToRenderPerBatch={3}
/> style={styles.flatList}
<AnimatedFlatList onEndReached={onEndReached}
ref={customFLRef || flRef} onEndReachedThreshold={0.75}
scrollEventThrottle={16} ListFooterComponent={
onScroll={onScroll} <TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
windowSize={7} }
data={flattenData} ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
initialNumToRender={6} ItemSeparatorComponent={ItemSeparatorComponent}
maxToRenderPerBatch={3} maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
style={styles.flatList} refreshing={isFetchingPreviousPage}
onEndReached={onEndReached} onRefresh={() => {
onEndReachedThreshold={0.75} if (!disableRefresh && !isFetchingPreviousPage) {
ListFooterComponent={ queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
<TimelineFooter queryKey,
queryKey={queryKey} data => {
disableInfinity={disableInfinity} 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={<TimelineEmpty queryKey={queryKey} />} }}
ItemSeparatorComponent={ItemSeparatorComponent} {...androidRefreshControl}
maintainVisibleContentPosition={{ {...customProps}
minIndexForVisible: 0 />
}}
{...(lookback && {
viewabilityConfigCallbackPairs: viewabilityPairs.current
})}
{...androidRefreshControl}
{...customProps}
/>
</>
) )
} }

View File

@ -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 (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
padding: StyleConstants.Spacing.S,
backgroundColor: colors.backgroundDefault
}}
>
<CustomText fontStyle='S' style={{ color: colors.primaryDefault }}>
{t('lookback.message')}
</CustomText>
</View>
)
},
() => true
)
export default TimelineLookback

View File

@ -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<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: '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<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={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
}}
>
{isFetching ? (
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View>
) : (
<>
<View
style={{
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
}}
>
<CustomText
fontStyle='S'
style={{
lineHeight: CONTAINER_HEIGHT,
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={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}
>
<CustomText
fontStyle='S'
style={{
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
</Animated.View>
)
}
export default TimelineRefresh

View File

@ -3902,10 +3902,10 @@ domutils@^2.8.0:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domhandler "^4.2.0" domhandler "^4.2.0"
dotenv@16.0.0: dotenv@16.0.1:
version "16.0.0" version "16.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
@ -4372,10 +4372,10 @@ expo-web-browser@10.2.0, expo-web-browser@~10.2.0:
dependencies: dependencies:
compare-urls "^2.0.0" compare-urls "^2.0.0"
expo@45.0.2: expo@45.0.3:
version "45.0.2" version "45.0.3"
resolved "https://registry.yarnpkg.com/expo/-/expo-45.0.2.tgz#d773d1b6f427bcdbfcb3da26274b35b2749c632c" resolved "https://registry.yarnpkg.com/expo/-/expo-45.0.3.tgz#7682b4dd29c6ec9de440bc151c3aa43ec392ec6e"
integrity sha512-WRVNTEc2ZqstGwVIxCAfJUTsmhFVAWR4vNaF4kTDEQa2J5GHbFuSQbENYrTeLldLc5Q/60HX3xfxUu8pqqdjGA== integrity sha512-xI22uW8ekA5152mcN1bgXLFOFO0a2eafHr0Kgq80BvV6MyYbkZS3M8trtjo9Cs6LVMMVgyc0MWI/2cd5sGjMgg==
dependencies: dependencies:
"@babel/runtime" "^7.14.0" "@babel/runtime" "^7.14.0"
"@expo/cli" "0.1.4" "@expo/cli" "0.1.4"
@ -5028,10 +5028,10 @@ https-proxy-agent@^5.0.0:
agent-base "6" agent-base "6"
debug "4" debug "4"
i18next@21.8.0: i18next@21.8.1:
version "21.8.0" version "21.8.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.0.tgz#2ad46a882fe1b793e499c47a934bb115b7c618b3" resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.1.tgz#44e083ade3dfe08c8099882c560d3c7294cdf679"
integrity sha512-opNd7cQj0PDlUX15hPjtzReRxy5/Rn405YvHTBEm1nf1YJhsqYFFFhHMwuU4NEHZNlrepHk5uK+CJbFtB+KO3w== integrity sha512-uulZBD5kLME7Ucz8pFwpC7jXDjq4BHkio3b6GBw1ykXNHA8rIAk1S2t6zb2Ripf9OMetMkysiDRlPlCVqjvQeg==
dependencies: dependencies:
"@babel/runtime" "^7.17.2" "@babel/runtime" "^7.17.2"