Scroll to toot working

This commit is contained in:
Zhiyuan Zheng 2020-12-12 12:49:29 +01:00
parent 0fa9f87f66
commit 81a21d1d07
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
13 changed files with 140 additions and 116 deletions

8
src/@types/app.d.ts vendored
View File

@ -19,10 +19,10 @@ declare namespace App {
Pages, Pages,
{ {
page: Pages page: Pages
hashtag?: string hashtag?: Mastodon.Tag['name']
list?: string list?: Mastodon.List['id']
toot?: string toot?: Mastodon.Status
account?: string account?: Mastodon.Account['id']
} }
] ]
} }

View File

@ -7,7 +7,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
export interface Props { export interface Props {
styles: any styles: any
onPress: () => void onPress: () => void
icon: string icon: any
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
coordinate?: 'center' | 'default' coordinate?: 'center' | 'default'
} }

View File

@ -12,13 +12,13 @@ type PropsBase = {
export interface PropsText extends PropsBase { export interface PropsText extends PropsBase {
text: string text: string
icon?: string icon?: any
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
} }
export interface PropsIcon extends PropsBase { export interface PropsIcon extends PropsBase {
text?: string text?: string
icon: string icon: any
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
} }

View File

@ -8,7 +8,7 @@ import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
onPress: () => void onPress: () => void
text?: string text?: string
icon?: string icon?: any
} }
const HeaderLeft: React.FC<Props> = ({ onPress, text, icon }) => { const HeaderLeft: React.FC<Props> = ({ onPress, text, icon }) => {
@ -38,9 +38,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(HeaderLeft, (prev, next) => { export default HeaderLeft
let skipUpdate = true
skipUpdate = prev.text === next.text
skipUpdate = prev.icon === next.icon
return skipUpdate
})

View File

@ -12,12 +12,12 @@ type PropsBase = {
export interface PropsText extends PropsBase { export interface PropsText extends PropsBase {
text: string text: string
icon?: string icon?: any
} }
export interface PropsIcon extends PropsBase { export interface PropsIcon extends PropsBase {
text?: string text?: string
icon: string icon: any
} }
const HeaderRight: React.FC<PropsText | PropsIcon> = ({ const HeaderRight: React.FC<PropsText | PropsIcon> = ({
@ -60,10 +60,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(HeaderRight, (prev, next) => { export default HeaderRight
let skipUpdate = true
skipUpdate = prev.disabled === next.disabled
skipUpdate = prev.text === next.text
skipUpdate = prev.icon === next.icon
return skipUpdate
})

View File

@ -7,7 +7,7 @@ import { ColorDefinitions } from 'src/utils/styles/themes'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
iconFront?: string iconFront?: any
iconFrontColor?: ColorDefinitions iconFrontColor?: ColorDefinitions
title: string title: string
content?: string content?: string

View File

@ -1,20 +0,0 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
const NetworkStateError = () => {
return (
<View style={styles.base}>
<Text></Text>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
// justifyContent: 'center',
alignItems: 'center'
}
})
export default NetworkStateError

View File

@ -1,12 +1,5 @@
import React, { useCallback } from 'react' import React, { useCallback, useEffect, useRef } from 'react'
import { import { ActivityIndicator, AppState, FlatList, StyleSheet } from 'react-native'
ActivityIndicator,
AppState,
FlatList,
StyleSheet,
Text,
View
} from 'react-native'
import { setFocusHandler, useInfiniteQuery } from 'react-query' import { setFocusHandler, useInfiniteQuery } from 'react-query'
import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications' import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications'
@ -14,14 +7,13 @@ import TimelineDefault from 'src/components/Timelines/Timeline/Default'
import TimelineConversation from 'src/components/Timelines/Timeline/Conversation' import TimelineConversation from 'src/components/Timelines/Timeline/Conversation'
import { timelineFetch } from 'src/utils/fetches/timelineFetch' import { timelineFetch } from 'src/utils/fetches/timelineFetch'
import TimelineSeparator from './Timeline/Separator' import TimelineSeparator from './Timeline/Separator'
import TimelineEmpty from './Timeline/Empty'
// Opening nesting hashtag pages
export interface Props { export interface Props {
page: App.Pages page: App.Pages
hashtag?: string hashtag?: string
list?: string list?: string
toot?: string toot?: Mastodon.Status
account?: string account?: string
disableRefresh?: boolean disableRefresh?: boolean
scrollEnabled?: boolean scrollEnabled?: boolean
@ -48,15 +40,28 @@ const Timeline: React.FC<Props> = ({
const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }] const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }]
const { const {
isLoading,
isFetchingMore,
isError,
isSuccess, isSuccess,
isLoading,
isError,
isFetchingMore,
data, data,
fetchMore fetchMore,
refetch
} = useInfiniteQuery(queryKey, timelineFetch) } = useInfiniteQuery(queryKey, timelineFetch)
const flattenData = data ? data.flatMap(d => [...d?.toots]) : [] const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : [] const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
const flRef = useRef<FlatList>(null)
useEffect(() => {
if (toot && isSuccess) {
setTimeout(() => {
flRef.current?.scrollToIndex({
index: flattenPointer[0],
viewOffset: 100
})
}, 500)
}
}, [isSuccess])
const flKeyExtrator = useCallback(({ id }) => id, []) const flKeyExtrator = useCallback(({ id }) => id, [])
const flRenderItem = useCallback(({ item }) => { const flRenderItem = useCallback(({ item }) => {
@ -80,7 +85,7 @@ const Timeline: React.FC<Props> = ({
}, },
{ previous: true } { previous: true }
), ),
[disableRefresh, flattenData] [flattenData]
) )
const flOnEndReach = useCallback( const flOnEndReach = useCallback(
() => () =>
@ -89,37 +94,49 @@ const Timeline: React.FC<Props> = ({
direction: 'next', direction: 'next',
id: flattenData[flattenData.length - 1].id id: flattenData[flattenData.length - 1].id
}), }),
[disableRefresh, flattenData] [flattenData]
) )
const flFooter = useCallback(() => {
let content if (isFetchingMore) {
if (!isSuccess) { return <ActivityIndicator />
content = <ActivityIndicator /> } else {
} else if (isError) { return null
content = <Text>Error message</Text> }
} else { }, [isFetchingMore])
content = ( const onScrollToIndexFailed = useCallback(error => {
<> const offset = error.averageItemLength * error.index
<FlatList flRef.current?.scrollToOffset({ offset })
data={flattenData} setTimeout(
onRefresh={flOnRefresh} () =>
renderItem={flRenderItem} flRef.current?.scrollToIndex({ index: error.index, viewOffset: 100 }),
onEndReached={flOnEndReach} 350
keyExtractor={flKeyExtrator}
style={styles.flatList}
scrollEnabled={scrollEnabled} // For timeline in Account view
ItemSeparatorComponent={flItemSeparatorComponent}
refreshing={!disableRefresh && isLoading}
onEndReachedThreshold={!disableRefresh ? 1 : null}
// require getItemLayout
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
/>
{isFetchingMore && <ActivityIndicator />}
</>
) )
} }, [])
return <View>{content}</View> return (
<FlatList
ref={flRef}
data={flattenData}
style={styles.flatList}
onRefresh={flOnRefresh}
renderItem={flRenderItem}
onEndReached={flOnEndReach}
keyExtractor={flKeyExtrator}
ListFooterComponent={flFooter}
scrollEnabled={scrollEnabled} // For timeline in Account view
ItemSeparatorComponent={flItemSeparatorComponent}
onEndReachedThreshold={!disableRefresh ? 0.75 : null}
refreshing={!disableRefresh && isLoading && flattenData.length > 0}
ListEmptyComponent={
<TimelineEmpty
isLoading={isLoading}
isError={isError}
refetch={refetch}
/>
}
{...(toot && isSuccess && { onScrollToIndexFailed })}
/>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -2,9 +2,9 @@ import React, { useMemo } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Avatar from './Shared/Avatar' import TimelineAvatar from './Shared/Avatar'
import HeaderConversation from './Shared/HeaderConversation' import HeaderConversation from './Shared/HeaderConversation'
import Content from './Shared/Content' import TimelineContent from './Shared/Content'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
@ -18,7 +18,7 @@ const TimelineConversation: React.FC<Props> = ({ item }) => {
return ( return (
<View style={styles.statusView}> <View style={styles.statusView}>
<View style={styles.status}> <View style={styles.status}>
<Avatar uri={item.accounts[0].avatar} id={item.accounts[0].id} /> <TimelineAvatar uri={item.accounts[0].avatar} id={item.accounts[0].id} />
<View style={styles.details}> <View style={styles.details}>
<HeaderConversation <HeaderConversation
account={item.accounts[0]} account={item.accounts[0]}
@ -34,7 +34,7 @@ const TimelineConversation: React.FC<Props> = ({ item }) => {
} }
> >
{item.last_status ? ( {item.last_status ? (
<Content <TimelineContent
content={item.last_status.content} content={item.last_status.content}
emojis={item.last_status.emojis} emojis={item.last_status.emojis}
mentions={item.last_status.mentions} mentions={item.last_status.mentions}

View File

@ -32,7 +32,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const pressableToot = useCallback( const pressableToot = useCallback(
() => () =>
navigation.navigate('Screen-Shared-Toot', { navigation.navigate('Screen-Shared-Toot', {
toot: actualStatus.id toot: actualStatus
}), }),
[] []
) )

View File

@ -0,0 +1,49 @@
import { Feather } from '@expo/vector-icons'
import React from 'react'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import { ButtonRow } from 'src/components/Button'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
export interface Props {
isLoading: boolean
isError: boolean
refetch: () => void
}
const TimelineEmpty: React.FC<Props> = ({ isLoading, isError, refetch }) => {
const { theme } = useTheme()
return (
<View style={styles.base}>
{isLoading && <ActivityIndicator />}
{isError && (
<>
<Feather
name='frown'
size={StyleConstants.Font.Size.L}
color={theme.primary}
/>
<Text style={[styles.error, { color: theme.primary }]}></Text>
<ButtonRow text='重试' onPress={() => refetch()} />
</>
)}
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
minHeight: '100%',
justifyContent: 'center',
alignItems: 'center'
},
error: {
fontSize: StyleConstants.Font.Size.M,
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L
}
})
export default TimelineEmpty

View File

@ -2,12 +2,10 @@ import React from 'react'
import Timeline from 'src/components/Timelines/Timeline' import Timeline from 'src/components/Timelines/Timeline'
// Show remote hashtag? Only when private, show local version?
export interface Props { export interface Props {
route: { route: {
params: { params: {
toot: string toot: Mastodon.Status
} }
} }
} }

View File

@ -12,14 +12,14 @@ export const timelineFetch = async (
list, list,
toot toot
}: { }: {
page: string page: App.Pages
params?: { params?: {
[key: string]: string | number | boolean [key: string]: string | number | boolean
} }
account?: string hashtag?: Mastodon.Tag['name']
hashtag?: string list?: Mastodon.List['id']
list?: string toot?: Mastodon.Status
toot?: string account?: Mastodon.Account['id']
}, },
pagination: { pagination: {
direction: 'prev' | 'next' direction: 'prev' | 'next'
@ -173,23 +173,14 @@ export const timelineFetch = async (
return Promise.resolve({ toots: res.body, pointer: null }) return Promise.resolve({ toots: res.body, pointer: null })
case 'Toot': case 'Toot':
const current = await client({ res = await client({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `statuses/${toot}` url: `statuses/${toot!.id}/context`
})
const context = await client({
method: 'get',
instance: 'local',
url: `statuses/${toot}/context`
}) })
return Promise.resolve({ return Promise.resolve({
toots: [ toots: [...res.body.ancestors, toot, ...res.body.descendants],
...context.body.ancestors, pointer: res.body.ancestors.length
current.body,
...context.body.descendants
],
pointer: context.body.ancestors.length
}) })
default: default: