From 81a21d1d07374fb400c376406e48dbe0f1316fea Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sat, 12 Dec 2020 12:49:29 +0100 Subject: [PATCH] Scroll to toot working --- src/@types/app.d.ts | 8 +- src/components/Button/ButtonRound.tsx | 2 +- src/components/Button/ButtonRow.tsx | 4 +- src/components/Header/Left.tsx | 9 +- src/components/Header/Right.tsx | 12 +- src/components/Menu/Row.tsx | 2 +- src/components/NetworkState/Error.tsx | 20 ---- src/components/Timelines/Timeline.tsx | 109 ++++++++++-------- .../Timelines/Timeline/Conversation.tsx | 8 +- src/components/Timelines/Timeline/Default.tsx | 2 +- src/components/Timelines/Timeline/Empty.tsx | 49 ++++++++ src/screens/Shared/Toot.tsx | 4 +- src/utils/fetches/timelineFetch.ts | 27 ++--- 13 files changed, 140 insertions(+), 116 deletions(-) delete mode 100644 src/components/NetworkState/Error.tsx create mode 100644 src/components/Timelines/Timeline/Empty.tsx diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index d2abb9f5..3be57681 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -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'] } ] } diff --git a/src/components/Button/ButtonRound.tsx b/src/components/Button/ButtonRound.tsx index 90b7bd7b..4090ce21 100644 --- a/src/components/Button/ButtonRound.tsx +++ b/src/components/Button/ButtonRound.tsx @@ -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' } diff --git a/src/components/Button/ButtonRow.tsx b/src/components/Button/ButtonRow.tsx index 68a8e874..9ce07c94 100644 --- a/src/components/Button/ButtonRow.tsx +++ b/src/components/Button/ButtonRow.tsx @@ -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' } diff --git a/src/components/Header/Left.tsx b/src/components/Header/Left.tsx index de02ed6f..c4dc5c70 100644 --- a/src/components/Header/Left.tsx +++ b/src/components/Header/Left.tsx @@ -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 = ({ 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 diff --git a/src/components/Header/Right.tsx b/src/components/Header/Right.tsx index 817da3aa..8f901917 100644 --- a/src/components/Header/Right.tsx +++ b/src/components/Header/Right.tsx @@ -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 = ({ @@ -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 diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index af87f247..04ad55f9 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -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 diff --git a/src/components/NetworkState/Error.tsx b/src/components/NetworkState/Error.tsx deleted file mode 100644 index 55d0dcfc..00000000 --- a/src/components/NetworkState/Error.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { StyleSheet, Text, View } from 'react-native' - -const NetworkStateError = () => { - return ( - - 加载错误 - - ) -} - -const styles = StyleSheet.create({ - base: { - flex: 1, - // justifyContent: 'center', - alignItems: 'center' - } -}) - -export default NetworkStateError diff --git a/src/components/Timelines/Timeline.tsx b/src/components/Timelines/Timeline.tsx index 42619123..2436ced0 100644 --- a/src/components/Timelines/Timeline.tsx +++ b/src/components/Timelines/Timeline.tsx @@ -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 = ({ 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(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 = ({ }, { previous: true } ), - [disableRefresh, flattenData] + [flattenData] ) const flOnEndReach = useCallback( () => @@ -89,37 +94,49 @@ const Timeline: React.FC = ({ direction: 'next', id: flattenData[flattenData.length - 1].id }), - [disableRefresh, flattenData] + [flattenData] ) - - let content - if (!isSuccess) { - content = - } else if (isError) { - content = Error message - } else { - content = ( - <> - - {isFetchingMore && } - + const flFooter = useCallback(() => { + if (isFetchingMore) { + return + } 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 {content} + return ( + 0} + ListEmptyComponent={ + + } + {...(toot && isSuccess && { onScrollToIndexFailed })} + /> + ) } const styles = StyleSheet.create({ diff --git a/src/components/Timelines/Timeline/Conversation.tsx b/src/components/Timelines/Timeline/Conversation.tsx index c8aadddb..b1606ae9 100644 --- a/src/components/Timelines/Timeline/Conversation.tsx +++ b/src/components/Timelines/Timeline/Conversation.tsx @@ -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 = ({ item }) => { return ( - + = ({ item }) => { } > {item.last_status ? ( - = ({ item, queryKey }) => { const pressableToot = useCallback( () => navigation.navigate('Screen-Shared-Toot', { - toot: actualStatus.id + toot: actualStatus }), [] ) diff --git a/src/components/Timelines/Timeline/Empty.tsx b/src/components/Timelines/Timeline/Empty.tsx new file mode 100644 index 00000000..54ba7085 --- /dev/null +++ b/src/components/Timelines/Timeline/Empty.tsx @@ -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 = ({ isLoading, isError, refetch }) => { + const { theme } = useTheme() + + return ( + + {isLoading && } + {isError && ( + <> + + 加载错误 + refetch()} /> + + )} + + ) +} + +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 diff --git a/src/screens/Shared/Toot.tsx b/src/screens/Shared/Toot.tsx index ead1d1b5..7f36efe4 100644 --- a/src/screens/Shared/Toot.tsx +++ b/src/screens/Shared/Toot.tsx @@ -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 } } } diff --git a/src/utils/fetches/timelineFetch.ts b/src/utils/fetches/timelineFetch.ts index a7ba129a..725624af 100644 --- a/src/utils/fetches/timelineFetch.ts +++ b/src/utils/fetches/timelineFetch.ts @@ -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: