Merge branch 'main' into candidate

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

View File

@ -1,12 +1,17 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
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 { 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 { 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 Animated, {
Extrapolate,
@ -26,7 +31,7 @@ export interface Props {
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_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2)
@ -44,86 +49,17 @@ const TimelineRefresh: React.FC<Props> = ({
return null
}
const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false)
const PREV_PER_BATCH = 1
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 } =
useTimelineQuery({
...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 queryClient = useQueryClient()
const { refetch, isFetching } = useTimelineQuery({ ...queryKey[1] })
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: [
@ -145,14 +81,6 @@ const TimelineRefresh: React.FC<Props> = ({
}))
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(
() => {
if (isFetching) {
@ -190,8 +118,81 @@ const TimelineRefresh: React.FC<Props> = ({
},
[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(
@ -202,95 +203,84 @@ const TimelineRefresh: React.FC<Props> = ({
fetchingType.value = 0
switch (data) {
case 1:
runOnJS(wrapperStartLatest)()
runOnJS(clearFirstPage)()
runOnJS(fetchPreviousPage)()
break
runOnJS(runFetchPrevious)()
return
case 2:
runOnJS(prepareRefetch)()
runOnJS(callRefetch)()
break
runOnJS(runFetchLatest)()
return
}
},
[]
)
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}
/>
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
}}
>
{prevActive.current || 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 }}>
<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)
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
}}
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' }}>
<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>
)
}
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

View File

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

View File

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

View File

@ -67,7 +67,7 @@ const handleError =
type LinkFormat = { id: string; isOffset: boolean }
export type PagedResponse<T = unknown> = {
body: T
links: { prev?: LinkFormat; next?: LinkFormat }
links?: { prev?: LinkFormat; next?: LinkFormat }
}
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]
let params: { [key: string]: string } = { limit: 40, ...pageParam }
@ -165,7 +168,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
})
return {
body: uniqBy([...res1.body, ...res2.body], 'id'),
...(res2.links.next && { links: { next: res2.links.next } })
...(res2.links?.next && { links: { next: res2.links.next } })
}
}
} 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
export type TimelineData = Unpromise<ReturnType<typeof queryFunction>>
export type TimelineData = Unpromise<ReturnType<typeof queryFunctionTimeline>>
const useTimelineQuery = ({
options,
...queryKeyParams
@ -228,7 +231,7 @@ const useTimelineQuery = ({
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunction, {
return useInfiniteQuery(queryKey, queryFunctionTimeline, {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,