mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Updates
This commit is contained in:
@ -162,12 +162,12 @@ const ComponentInstance: React.FC<Props> = ({
|
||||
type='text'
|
||||
content={t('server.button.local')}
|
||||
onPress={processUpdate}
|
||||
disabled={!instanceQuery.data?.uri || !agreed}
|
||||
disabled={!instanceQuery.data?.uri}
|
||||
loading={instanceQuery.isFetching || appsQuery.isFetching}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<EULA agreed={agreed} setAgreed={setAgreed} />
|
||||
{/* <EULA agreed={agreed} setAgreed={setAgreed} /> */}
|
||||
|
||||
<View>
|
||||
<Placeholder
|
||||
|
@ -82,19 +82,26 @@ const Timeline: React.FC<Props> = ({
|
||||
...queryKeyParams,
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage.links?.prev && {
|
||||
firstPage?.links?.prev && {
|
||||
min_id: firstPage.links.prev,
|
||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
||||
limit: '6'
|
||||
limit: '5'
|
||||
},
|
||||
|
||||
getNextPageParam: lastPage =>
|
||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
||||
lastPage?.links?.next && { max_id: lastPage.links.next }
|
||||
}
|
||||
})
|
||||
|
||||
const flattenData = data?.pages ? data.pages.flatMap(d => [...d.body]) : []
|
||||
|
||||
// Auto go back when toot page is empty
|
||||
const navigation = useNavigation()
|
||||
useEffect(() => {
|
||||
if (toot && isSuccess && flattenData.length === 0) {
|
||||
navigation.goBack()
|
||||
}
|
||||
}, [isSuccess, flattenData.length])
|
||||
// Toot page auto scroll to selected toot
|
||||
const flRef = useRef<FlatList<any>>(null)
|
||||
const scrolled = useRef(false)
|
||||
@ -119,13 +126,6 @@ const Timeline: React.FC<Props> = ({
|
||||
350
|
||||
)
|
||||
}, [])
|
||||
// Auto go back when toot page is empty
|
||||
const navigation = useNavigation()
|
||||
useEffect(() => {
|
||||
if (toot && isSuccess && flattenData.length === 0) {
|
||||
navigation.goBack()
|
||||
}
|
||||
}, [isSuccess, flattenData.length])
|
||||
|
||||
const keyExtractor = useCallback(({ id }) => id, [])
|
||||
const renderItem = useCallback(
|
||||
@ -233,26 +233,57 @@ const Timeline: React.FC<Props> = ({
|
||||
scrollY.value = nativeEvent.contentOffset.y
|
||||
}, [])
|
||||
const onResponderRelease = useCallback(() => {
|
||||
if (
|
||||
scrollY.value <= -StyleConstants.Spacing.XL &&
|
||||
isFetchingLatest === 0 &&
|
||||
!disableRefresh
|
||||
) {
|
||||
haptics('Light')
|
||||
setIsFetchingLatest(1)
|
||||
flRef.current?.scrollToOffset({
|
||||
animated: true,
|
||||
offset: 1
|
||||
})
|
||||
if (!disableRefresh) {
|
||||
const separation01 = -(
|
||||
(StyleConstants.Spacing.M * 2.5) / 2 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
const separation02 = -(
|
||||
StyleConstants.Spacing.M * 2.5 * 1.5 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
if (
|
||||
scrollY.value <= separation02 &&
|
||||
!isFetching &&
|
||||
isFetchingLatest === 0
|
||||
) {
|
||||
haptics('Light')
|
||||
queryClient.setQueryData<InfiniteData<any> | undefined>(
|
||||
queryKey,
|
||||
data => {
|
||||
if (data?.pages[0].body.length === 0) {
|
||||
return {
|
||||
pages: data.pages.slice(1),
|
||||
pageParams: data.pageParams.slice(1)
|
||||
}
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
)
|
||||
refetch()
|
||||
} else if (
|
||||
scrollY.value <= separation01 &&
|
||||
!isFetching &&
|
||||
isFetchingLatest === 0
|
||||
) {
|
||||
haptics('Light')
|
||||
setIsFetchingLatest(1)
|
||||
flRef.current?.scrollToOffset({
|
||||
animated: true,
|
||||
offset: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [scrollY.value, isFetchingLatest, disableRefresh])
|
||||
}, [scrollY.value, isFetching, isFetchingLatest, disableRefresh])
|
||||
const headerPadding = useAnimatedStyle(() => {
|
||||
if (isFetchingLatest !== 0) {
|
||||
return { paddingTop: withTiming(StyleConstants.Spacing.XL) }
|
||||
} else {
|
||||
return { paddingTop: withTiming(0) }
|
||||
return {
|
||||
paddingTop:
|
||||
isFetchingLatest !== 0 || (isFetching && !isLoading)
|
||||
? withTiming(StyleConstants.Spacing.M * 2.5)
|
||||
: withTiming(0)
|
||||
}
|
||||
}, [isFetchingLatest])
|
||||
}, [isFetchingLatest, isFetching, isLoading])
|
||||
const ListHeaderComponent = useMemo(
|
||||
() => <Animated.View style={headerPadding} />,
|
||||
[]
|
||||
@ -267,7 +298,7 @@ const Timeline: React.FC<Props> = ({
|
||||
colors={[theme.primary]}
|
||||
progressBackgroundColor={theme.background}
|
||||
refreshing={isFetching || isLoading}
|
||||
// onRefresh={() => refetch()}
|
||||
onRefresh={() => refetch()}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@ -276,8 +307,14 @@ const Timeline: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineRefresh isLoading={isLoading} disable={disableRefresh} />
|
||||
<TimelineRefresh
|
||||
scrollY={scrollY}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
disable={disableRefresh}
|
||||
/>
|
||||
<FlatList
|
||||
scrollEventThrottle={16}
|
||||
onScroll={onScroll}
|
||||
onResponderRelease={onResponderRelease}
|
||||
ref={flRef}
|
||||
|
@ -13,6 +13,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -75,7 +76,6 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
disabled={queryKey && queryKey[1].toot !== undefined}
|
||||
>
|
||||
{item.reblog ? (
|
||||
<TimelineActioned action='reblog' account={item.account} />
|
||||
@ -143,11 +143,13 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
status={actualStatus}
|
||||
accts={([actualStatus.account] as Mastodon.Account[] &
|
||||
Mastodon.Mention[])
|
||||
.concat(actualStatus.mentions)
|
||||
.filter(d => d.id !== localAccount?.id)
|
||||
.map(d => d.acct)}
|
||||
accts={uniqBy(
|
||||
([actualStatus.account] as Mastodon.Account[] &
|
||||
Mastodon.Mention[])
|
||||
.concat(actualStatus.mentions)
|
||||
.filter(d => d.id !== localAccount?.id),
|
||||
d => d.id
|
||||
).map(d => d.acct)}
|
||||
reblog={item.reblog ? true : false}
|
||||
/>
|
||||
</View>
|
||||
|
@ -13,6 +13,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -126,11 +127,13 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
status={notification.status}
|
||||
accts={([notification.status.account] as Mastodon.Account[] &
|
||||
Mastodon.Mention[])
|
||||
.concat(notification.status.mentions)
|
||||
.filter(d => d.id !== localAccount?.id)
|
||||
.map(d => d.acct)}
|
||||
accts={uniqBy(
|
||||
([notification.status.account] as Mastodon.Account[] &
|
||||
Mastodon.Mention[])
|
||||
.concat(notification.status.mentions)
|
||||
.filter(d => d.id !== localAccount?.id),
|
||||
d => d.id
|
||||
).map(d => d.acct)}
|
||||
reblog={false}
|
||||
/>
|
||||
</View>
|
||||
|
@ -1,25 +1,164 @@
|
||||
import haptics from '@components/haptics'
|
||||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { 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'
|
||||
|
||||
export interface Props {
|
||||
scrollY: Animated.SharedValue<number>
|
||||
isLoading: boolean
|
||||
isFetching: boolean
|
||||
disable?: boolean
|
||||
}
|
||||
|
||||
const TimelineRefresh: React.FC<Props> = ({ isLoading, disable = false }) => {
|
||||
const { theme } = useTheme()
|
||||
return !isLoading && !disable ? (
|
||||
<View
|
||||
style={styles.base}
|
||||
children={
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
|
||||
|
||||
const TimelineRefresh = React.memo(
|
||||
({ scrollY, isLoading, isFetching, disable = false }: Props) => {
|
||||
if (disable || isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
|
||||
const separation01 = -(
|
||||
CONTAINER_HEIGHT / 2 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
const separation02 = -(
|
||||
CONTAINER_HEIGHT * 1.5 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
const [textRight, setTextRight] = useState(0)
|
||||
const arrowY = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollY.value,
|
||||
[0, separation01],
|
||||
[
|
||||
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
|
||||
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
|
||||
],
|
||||
Extrapolate.CLAMP
|
||||
)
|
||||
}
|
||||
]
|
||||
}))
|
||||
const arrowTop = useAnimatedStyle(() => ({
|
||||
marginTop:
|
||||
scrollY.value < separation02
|
||||
? 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 < separation01) {
|
||||
arrowStage.value = 1
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case 1:
|
||||
if (scrollY.value < separation02) {
|
||||
arrowStage.value = 2
|
||||
return true
|
||||
}
|
||||
if (scrollY.value > separation01) {
|
||||
arrowStage.value = 0
|
||||
return false
|
||||
}
|
||||
return false
|
||||
case 2:
|
||||
if (scrollY.value > separation02) {
|
||||
arrowStage.value = 1
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
data => {
|
||||
if (data) {
|
||||
runOnJS(haptics)('Light')
|
||||
}
|
||||
},
|
||||
[isFetching]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{isFetching ? (
|
||||
<View style={styles.container2}>
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.container1}>
|
||||
<Text
|
||||
style={[styles.explanation, { color: theme.primary }]}
|
||||
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={theme.primary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container2}>
|
||||
<Text
|
||||
style={[styles.explanation, { color: theme.primary }]}
|
||||
onLayout={onLayout}
|
||||
children={t('refresh.refetch')}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.isLoading === next.isLoading && prev.isFetching === next.isFetching
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
@ -27,9 +166,18 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: StyleConstants.Spacing.XL,
|
||||
justifyContent: 'center',
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
|
Reference in New Issue
Block a user