mirror of
https://github.com/tooot-app/app
synced 2025-02-15 19:30:55 +01:00
Scroll to toot working
This commit is contained in:
parent
0fa9f87f66
commit
81a21d1d07
8
src/@types/app.d.ts
vendored
8
src/@types/app.d.ts
vendored
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
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({
|
||||
|
@ -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}
|
||||
|
@ -32,7 +32,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
const pressableToot = useCallback(
|
||||
() =>
|
||||
navigation.navigate('Screen-Shared-Toot', {
|
||||
toot: actualStatus.id
|
||||
toot: actualStatus
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
49
src/components/Timelines/Timeline/Empty.tsx
Normal file
49
src/components/Timelines/Timeline/Empty.tsx
Normal 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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user