mirror of https://github.com/tooot-app/app
Scroll to toot working
This commit is contained in:
parent
0fa9f87f66
commit
81a21d1d07
|
@ -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']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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 if (isError) {
|
|
||||||
content = <Text>Error message</Text>
|
|
||||||
} else {
|
} else {
|
||||||
content = (
|
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 (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={flRef}
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
|
style={styles.flatList}
|
||||||
onRefresh={flOnRefresh}
|
onRefresh={flOnRefresh}
|
||||||
renderItem={flRenderItem}
|
renderItem={flRenderItem}
|
||||||
onEndReached={flOnEndReach}
|
onEndReached={flOnEndReach}
|
||||||
keyExtractor={flKeyExtrator}
|
keyExtractor={flKeyExtrator}
|
||||||
style={styles.flatList}
|
ListFooterComponent={flFooter}
|
||||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||||
ItemSeparatorComponent={flItemSeparatorComponent}
|
ItemSeparatorComponent={flItemSeparatorComponent}
|
||||||
refreshing={!disableRefresh && isLoading}
|
onEndReachedThreshold={!disableRefresh ? 0.75 : null}
|
||||||
onEndReachedThreshold={!disableRefresh ? 1 : null}
|
refreshing={!disableRefresh && isLoading && flattenData.length > 0}
|
||||||
// require getItemLayout
|
ListEmptyComponent={
|
||||||
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
|
<TimelineEmpty
|
||||||
|
isLoading={isLoading}
|
||||||
|
isError={isError}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{isFetchingMore && <ActivityIndicator />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||||
return <View>{content}</View>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue