1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into candidate

This commit is contained in:
xmflsct
2023-01-07 02:05:04 +01:00
5 changed files with 176 additions and 184 deletions

View File

@@ -1,12 +1,17 @@
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { InfiniteData, useQueryClient } from '@tanstack/react-query' import { InfiniteData, useQueryClient } from '@tanstack/react-query'
import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline' import { PagedResponse } from '@utils/api/helpers'
import {
queryFunctionTimeline,
QueryKeyTimeline,
useTimelineQuery
} from '@utils/queryHooks/timeline'
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, useState } from 'react' import React, { RefObject, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native' import { FlatList, Platform, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
import Animated, { import Animated, {
Extrapolate, Extrapolate,
@@ -26,7 +31,7 @@ export interface Props {
disableRefresh?: boolean disableRefresh?: boolean
} }
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2
export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2) 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) export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2)
@@ -44,86 +49,17 @@ const TimelineRefresh: React.FC<Props> = ({
return null return null
} }
const fetchingLatestIndex = useRef(0) const PREV_PER_BATCH = 1
const refetchActive = useRef(false) const prevActive = useRef<boolean>(false)
const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>()
const prevStatusId = useRef<Mastodon.Status['id']>()
const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } = const queryClient = useQueryClient()
useTimelineQuery({ const { refetch, isFetching } = useTimelineQuery({ ...queryKey[1] })
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
...(firstPage.links.prev.isOffset
? { offset: firstPage.links.prev.id }
: { min_id: firstPage.links.prev.id }),
// 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 { t } = useTranslation('componentTimeline')
const { colors } = useTheme() 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 [textRight, setTextRight] = useState(0)
const arrowY = useAnimatedStyle(() => ({ const arrowY = useAnimatedStyle(() => ({
transform: [ transform: [
@@ -145,14 +81,6 @@ const TimelineRefresh: React.FC<Props> = ({
})) }))
const arrowStage = useSharedValue(0) const arrowStage = useSharedValue(0)
const onLayout = useCallback(
({ nativeEvent }: LayoutChangeEvent) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
},
[textRight]
)
useAnimatedReaction( useAnimatedReaction(
() => { () => {
if (isFetching) { if (isFetching) {
@@ -190,8 +118,81 @@ const TimelineRefresh: React.FC<Props> = ({
}, },
[isFetching] [isFetching]
) )
const wrapperStartLatest = () => {
fetchingLatestIndex.current = 1 const runFetchPrevious = async () => {
if (prevActive.current) return
const firstPage =
queryClient.getQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey)?.pages[0]
prevActive.current = true
prevStatusId.current = firstPage?.body[0].id
await queryFunctionTimeline({
queryKey,
pageParam: firstPage?.links?.prev && {
...(firstPage.links.prev.isOffset
? { offset: firstPage.links.prev.id }
: { min_id: firstPage.links.prev.id })
},
meta: {}
}).then(res => {
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
prevCache.current = res.body.slice(0, -PREV_PER_BATCH)
return { ...old, pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages] }
})
})
}
useEffect(() => {
const loop = async () => {
for await (const _ of Array(Math.ceil((prevCache.current?.length || 0) / PREV_PER_BATCH))) {
await new Promise(promise => setTimeout(promise, 32))
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) {
return { ...page, body: [...insert, ...page.body] }
} else {
return page
}
} else {
return page
}
})
}
})
break
}
prevActive.current = false
}
loop()
}, [prevCache.current])
const runFetchLatest = async () => {
queryClient.invalidateQueries(queryKey)
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
} }
useAnimatedReaction( useAnimatedReaction(
@@ -202,95 +203,84 @@ const TimelineRefresh: React.FC<Props> = ({
fetchingType.value = 0 fetchingType.value = 0
switch (data) { switch (data) {
case 1: case 1:
runOnJS(wrapperStartLatest)() runOnJS(runFetchPrevious)()
runOnJS(clearFirstPage)() return
runOnJS(fetchPreviousPage)()
break
case 2: case 2:
runOnJS(prepareRefetch)() runOnJS(runFetchLatest)()
runOnJS(callRefetch)() return
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 ( return (
<Animated.View style={headerPadding}> <Animated.View
<View style={styles.base}> style={{
{isFetching ? ( position: 'absolute',
<View style={styles.container2}> top: 0,
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> left: 0,
</View> right: 0,
) : ( height: CONTAINER_HEIGHT * 2,
<> alignItems: 'center'
<View style={styles.container1}> }}
<Text >
style={[styles.explanation, { color: colors.primaryDefault }]} {prevActive.current || isFetching ? (
onLayout={onLayout} <View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
children={t('refresh.fetchPreviousPage')} <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
/> </View>
<Animated.View ) : (
style={[ <>
{ <View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
position: 'absolute', <Text
left: textRight + StyleConstants.Spacing.S style={{
}, fontSize: StyleConstants.Font.Size.S,
arrowY, lineHeight: CONTAINER_HEIGHT,
arrowTop color: colors.primaryDefault
]} }}
children={ onLayout={({ nativeEvent }) => {
<Icon if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
name='ArrowLeft' setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
} }
/> }}
</View> children={t('refresh.fetchPreviousPage')}
<View style={styles.container2}> />
<Text <Animated.View
style={[styles.explanation, { color: colors.primaryDefault }]} style={[
onLayout={onLayout} {
children={t('refresh.refetch')} position: 'absolute',
/> left: textRight + StyleConstants.Spacing.S
</View> },
</> arrowY,
)} arrowTop
</View> ]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
}
/>
</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>
</>
)}
</Animated.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 export default TimelineRefresh

View File

@@ -129,13 +129,11 @@ const Timeline: React.FC<Props> = ({
/> />
) )
} }
maintainVisibleContentPosition={ {...(!isLoading && {
isFetching maintainVisibleContentPosition: {
? { minIndexForVisible: 0
minIndexForVisible: 0 }
} })}
: undefined
}
{...androidRefreshControl} {...androidRefreshControl}
{...customProps} {...customProps}
/> />

View File

@@ -74,7 +74,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
const match = urlMatcher(toot.url || toot.uri) const match = urlMatcher(toot.url || toot.uri)
const highlightIndex = useRef<number>(0) const highlightIndex = useRef<number>(0)
const query = useQuery<{ pages: { body: (Mastodon.Status & { _key?: 'cached' })[] }[] }>( const query = useQuery<{ pages: { body: (Mastodon.Status & { key?: 'cached' })[] }[] }>(
queryKey.local, queryKey.local,
async () => { async () => {
const context = await apiInstance<{ const context = await apiInstance<{
@@ -106,7 +106,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
} }
}, },
{ {
initialData: { pages: [{ body: [{ ...toot, _level: 0, _key: 'cached' }] }] }, initialData: { pages: [{ body: [{ ...toot, _level: 0, key: 'cached' }] }] },
enabled: !toot._remote, enabled: !toot._remote,
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
@@ -178,6 +178,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}, },
{ {
enabled: enabled:
query.isFetched &&
['public', 'unlisted'].includes(toot.visibility) && ['public', 'unlisted'].includes(toot.visibility) &&
match?.domain !== getAccountStorage.string('auth.domain'), match?.domain !== getAccountStorage.string('auth.domain'),
staleTime: 0, staleTime: 0,
@@ -204,7 +205,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
local => local.uri === remote.uri local => local.uri === remote.uri
) )
if (localMatch) { if (localMatch) {
delete localMatch._key delete localMatch.key
return localMatch return localMatch
} else { } else {
return { return {
@@ -262,7 +263,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
ref={flRef} ref={flRef}
scrollEventThrottle={16} scrollEventThrottle={16}
windowSize={7} windowSize={7}
data={query.data.pages?.[0].body} data={query.data?.pages?.[0].body}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
const prev = query.data.pages[0].body[index - 1]?._level || 0 const prev = query.data.pages[0].body[index - 1]?._level || 0
const curr = item._level const curr = item._level

View File

@@ -67,7 +67,7 @@ const handleError =
type LinkFormat = { id: string; isOffset: boolean } type LinkFormat = { id: string; isOffset: boolean }
export type PagedResponse<T = unknown> = { export type PagedResponse<T = unknown> = {
body: T body: T
links: { prev?: LinkFormat; next?: LinkFormat } links?: { prev?: LinkFormat; next?: LinkFormat }
} }
export { ctx, handleError, userAgent } export { ctx, handleError, userAgent }

View File

@@ -52,7 +52,10 @@ export type QueryKeyTimeline = [
) )
] ]
const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyTimeline>) => { export const queryFunctionTimeline = async ({
queryKey,
pageParam
}: QueryFunctionContext<QueryKeyTimeline>) => {
const page = queryKey[1] const page = queryKey[1]
let params: { [key: string]: string } = { limit: 40, ...pageParam } let params: { [key: string]: string } = { limit: 40, ...pageParam }
@@ -165,7 +168,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
}) })
return { return {
body: uniqBy([...res1.body, ...res2.body], 'id'), body: uniqBy([...res1.body, ...res2.body], 'id'),
...(res2.links.next && { links: { next: res2.links.next } }) ...(res2.links?.next && { links: { next: res2.links.next } })
} }
} }
} else { } else {
@@ -220,7 +223,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
} }
type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never
export type TimelineData = Unpromise<ReturnType<typeof queryFunction>> export type TimelineData = Unpromise<ReturnType<typeof queryFunctionTimeline>>
const useTimelineQuery = ({ const useTimelineQuery = ({
options, options,
...queryKeyParams ...queryKeyParams
@@ -228,7 +231,7 @@ const useTimelineQuery = ({
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError> options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>
}) => { }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }] const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunction, { return useInfiniteQuery(queryKey, queryFunctionTimeline, {
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,