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,
{
page: Pages
hashtag?: string
list?: string
toot?: string
account?: string
hashtag?: Mastodon.Tag['name']
list?: Mastodon.List['id']
toot?: Mastodon.Status
account?: Mastodon.Account['id']
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { ColorDefinitions } from 'src/utils/styles/themes'
import { StyleConstants } from 'src/utils/styles/constants'
export interface Props {
iconFront?: string
iconFront?: any
iconFrontColor?: ColorDefinitions
title: 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 {
ActivityIndicator,
AppState,
FlatList,
StyleSheet,
Text,
View
} from 'react-native'
import React, { useCallback, useEffect, useRef } from 'react'
import { ActivityIndicator, AppState, FlatList, StyleSheet } from 'react-native'
import { setFocusHandler, useInfiniteQuery } from 'react-query'
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 { timelineFetch } from 'src/utils/fetches/timelineFetch'
import TimelineSeparator from './Timeline/Separator'
// Opening nesting hashtag pages
import TimelineEmpty from './Timeline/Empty'
export interface Props {
page: App.Pages
hashtag?: string
list?: string
toot?: string
toot?: Mastodon.Status
account?: string
disableRefresh?: boolean
scrollEnabled?: boolean
@ -48,15 +40,28 @@ const Timeline: React.FC<Props> = ({
const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }]
const {
isLoading,
isFetchingMore,
isError,
isSuccess,
isLoading,
isError,
isFetchingMore,
data,
fetchMore
fetchMore,
refetch
} = useInfiniteQuery(queryKey, timelineFetch)
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 flRenderItem = useCallback(({ item }) => {
@ -80,7 +85,7 @@ const Timeline: React.FC<Props> = ({
},
{ previous: true }
),
[disableRefresh, flattenData]
[flattenData]
)
const flOnEndReach = useCallback(
() =>
@ -89,37 +94,49 @@ const Timeline: React.FC<Props> = ({
direction: 'next',
id: flattenData[flattenData.length - 1].id
}),
[disableRefresh, flattenData]
[flattenData]
)
let content
if (!isSuccess) {
content = <ActivityIndicator />
} else if (isError) {
content = <Text>Error message</Text>
} else {
content = (
<>
<FlatList
data={flattenData}
onRefresh={flOnRefresh}
renderItem={flRenderItem}
onEndReached={flOnEndReach}
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 />}
</>
const flFooter = useCallback(() => {
if (isFetchingMore) {
return <ActivityIndicator />
} else {
return null
}
}, [isFetchingMore])
const onScrollToIndexFailed = useCallback(error => {
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })
setTimeout(
() =>
flRef.current?.scrollToIndex({ index: error.index, viewOffset: 100 }),
350
)
}
}, [])
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({

View File

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

View File

@ -32,7 +32,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const pressableToot = useCallback(
() =>
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'
// Show remote hashtag? Only when private, show local version?
export interface Props {
route: {
params: {
toot: string
toot: Mastodon.Status
}
}
}

View File

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