From b677c4b7ce56b66b32354e8b3935492014e0fc23 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 31 Dec 2022 00:07:28 +0100 Subject: [PATCH] Fix #558 #602 --- index.js | 4 - src/components/Separator.tsx | 4 +- src/components/Timeline/Shared/Actions.tsx | 3 +- src/components/Timeline/Shared/Card.tsx | 4 +- src/components/Timeline/index.tsx | 5 +- src/components/contextMenu/status.ts | 2 +- src/screens/Compose/Root/Footer/Reply.tsx | 4 +- src/screens/Tabs/Me/FollowedTags.tsx | 4 +- src/screens/Tabs/Me/List/Accounts.tsx | 7 +- src/screens/Tabs/Me/SettingsFontsize.tsx | 2 +- src/screens/Tabs/Me/Switch.tsx | 4 +- .../Tabs/Shared/Account/Attachments.tsx | 10 +- src/screens/Tabs/Shared/Toot.tsx | 290 +++++++++++------- src/screens/Tabs/Shared/Users.tsx | 4 +- src/utils/queryHooks/timeline.ts | 99 ++++-- src/utils/queryHooks/utils.ts | 4 + src/utils/styles/themes.ts | 6 +- 17 files changed, 290 insertions(+), 166 deletions(-) diff --git a/index.js b/index.js index d292a7a1..bfa0404e 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,4 @@ import { registerRootComponent } from 'expo' - import App from './src/App' -// registerRootComponent calls AppRegistry.registerComponent('main', () => App); -// It also ensures that whether you load the app in the Expo client or in a native build, -// the environment is set up appropriately registerRootComponent(App) diff --git a/src/components/Separator.tsx b/src/components/Separator.tsx index b0d8802c..7d3dae51 100644 --- a/src/components/Separator.tsx +++ b/src/components/Separator.tsx @@ -1,7 +1,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' -import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native' +import { StyleProp, View, ViewStyle } from 'react-native' export interface Props { extraMarginLeft?: number @@ -23,7 +23,7 @@ const ComponentSeparator: React.FC = ({ { backgroundColor: colors.backgroundDefault, borderTopColor: colors.border, - borderTopWidth: StyleSheet.hairlineWidth, + borderTopWidth: 1, marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft, marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight } diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx index 46fb1455..503bd97a 100644 --- a/src/components/Timeline/Shared/Actions.tsx +++ b/src/components/Timeline/Shared/Actions.tsx @@ -329,8 +329,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', - minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3, - marginHorizontal: StyleConstants.Spacing.S + paddingVertical: StyleConstants.Spacing.S * 1.5 } }) diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx index d5769d94..8155e109 100644 --- a/src/components/Timeline/Shared/Card.tsx +++ b/src/components/Timeline/Shared/Card.tsx @@ -10,7 +10,7 @@ import { useStatusQuery } from '@utils/queryHooks/status' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext, useEffect, useState } from 'react' -import { Pressable, StyleSheet, View } from 'react-native' +import { Pressable, View } from 'react-native' import { Circle } from 'react-native-animated-spinkit' import TimelineDefault from '../Default' import StatusContext from './Context' @@ -192,7 +192,7 @@ const TimelineCard: React.FC = () => { flex: 1, flexDirection: 'row', marginTop: StyleConstants.Spacing.M, - borderWidth: StyleSheet.hairlineWidth, + borderWidth: 1, borderRadius: StyleConstants.Spacing.S, overflow: 'hidden', borderColor: colors.border diff --git a/src/components/Timeline/index.tsx b/src/components/Timeline/index.tsx index c9c48dfc..7121652c 100644 --- a/src/components/Timeline/index.tsx +++ b/src/components/Timeline/index.tsx @@ -2,6 +2,7 @@ import ComponentSeparator from '@components/Separator' import { useScrollToTop } from '@react-navigation/native' import { UseInfiniteQueryOptions } from '@tanstack/react-query' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' +import { flattenPages } from '@utils/queryHooks/utils' import { useGlobalStorageListener } from '@utils/storage/actions' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' @@ -54,8 +55,6 @@ const Timeline: React.FC = ({ } }) - const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] - const flRef = useRef(null) const scrollY = useSharedValue(0) @@ -112,7 +111,7 @@ const Timeline: React.FC = ({ scrollEventThrottle={16} onScroll={onScroll} windowSize={7} - data={flattenData} + data={flattenPages(data)} initialNumToRender={6} maxToRenderPerBatch={3} onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()} diff --git a/src/components/contextMenu/status.ts b/src/components/contextMenu/status.ts index 36e3f56d..33b149e7 100644 --- a/src/components/contextMenu/status.ts +++ b/src/components/contextMenu/status.ts @@ -203,7 +203,7 @@ const menuStatus = ({ }), disabled: false, destructive: false, - hidden: !ownAccount && !status.mentions.filter(mention => mention.id === accountId).length + hidden: !ownAccount && !status.mentions?.filter(mention => mention.id === accountId).length }, title: t('componentContextMenu:status.mute.action', { defaultValue: 'false', diff --git a/src/screens/Compose/Root/Footer/Reply.tsx b/src/screens/Compose/Root/Footer/Reply.tsx index 5da1b68b..c151dcc2 100644 --- a/src/screens/Compose/Root/Footer/Reply.tsx +++ b/src/screens/Compose/Root/Footer/Reply.tsx @@ -2,7 +2,7 @@ import TimelineDefault from '@components/Timeline/Default' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext } from 'react' -import { StyleSheet, View } from 'react-native' +import { View } from 'react-native' import ComposeContext from '../../utils/createContext' const ComposeReply: React.FC = () => { @@ -16,7 +16,7 @@ const ComposeReply: React.FC = () => { style={{ flex: 1, flexDirection: 'row', - borderWidth: StyleSheet.hairlineWidth, + borderWidth: 1, borderRadius: StyleConstants.Spacing.S, overflow: 'hidden', borderColor: colors.border, diff --git a/src/screens/Tabs/Me/FollowedTags.tsx b/src/screens/Tabs/Me/FollowedTags.tsx index 085d7f75..c97c9c99 100644 --- a/src/screens/Tabs/Me/FollowedTags.tsx +++ b/src/screens/Tabs/Me/FollowedTags.tsx @@ -5,6 +5,7 @@ import { displayMessage } from '@components/Message' import ComponentSeparator from '@components/Separator' import { TabMeStackScreenProps } from '@utils/navigation/navigators' import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags' +import { flattenPages } from '@utils/queryHooks/utils' import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native-gesture-handler' @@ -15,7 +16,8 @@ const TabMeFollowedTags: React.FC> const { t } = useTranslation(['common', 'screenTabs']) const { data, fetchNextPage, refetch } = useFollowedTagsQuery() - const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : [] + const flattenData = flattenPages(data) + useEffect(() => { if (flattenData.length === 0) { navigation.goBack() diff --git a/src/screens/Tabs/Me/List/Accounts.tsx b/src/screens/Tabs/Me/List/Accounts.tsx index ec1fced2..0ee1d0ef 100644 --- a/src/screens/Tabs/Me/List/Accounts.tsx +++ b/src/screens/Tabs/Me/List/Accounts.tsx @@ -9,6 +9,7 @@ import { useListAccountsMutation, useListAccountsQuery } from '@utils/queryHooks/lists' +import { flattenPages } from '@utils/queryHooks/utils' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' @@ -18,7 +19,7 @@ import { FlatList, View } from 'react-native' const TabMeListAccounts: React.FC> = ({ route: { params } }) => { - const { colors, theme } = useTheme() + const { colors } = useTheme() const { t } = useTranslation(['common', 'screenTabs']) const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }] @@ -34,8 +35,6 @@ const TabMeListAccounts: React.FC> } }) - const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] - const mutation = useListAccountsMutation({ onSuccess: () => { haptics('Light') @@ -53,7 +52,7 @@ const TabMeListAccounts: React.FC> return ( ( { @@ -49,7 +49,7 @@ const TabMeSwitch: React.FC = () => { marginTop: StyleConstants.Spacing.S, paddingTop: StyleConstants.Spacing.M, marginHorizontal: StyleConstants.Spacing.Global.PagePadding, - borderTopWidth: StyleSheet.hairlineWidth, + borderTopWidth: 1, borderTopColor: colors.border }} > diff --git a/src/screens/Tabs/Shared/Account/Attachments.tsx b/src/screens/Tabs/Shared/Account/Attachments.tsx index 09a1bc9a..68f2e89f 100644 --- a/src/screens/Tabs/Shared/Account/Attachments.tsx +++ b/src/screens/Tabs/Shared/Account/Attachments.tsx @@ -4,6 +4,7 @@ import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { useTimelineQuery } from '@utils/queryHooks/timeline' +import { flattenPages } from '@utils/queryHooks/utils' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' @@ -32,12 +33,9 @@ const AccountAttachments: React.FC = ({ account }) => { only_media: true }) - const flattenData = data?.pages - ? data.pages - .flatMap(d => [...d.body]) - .filter(status => !(status as Mastodon.Status).sensitive) - .splice(0, DISPLAY_AMOUNT) - : [] + const flattenData = flattenPages(data) + .filter(status => !(status as Mastodon.Status).sensitive) + .splice(0, DISPLAY_AMOUNT) const styleContainer = useAnimatedStyle(() => { if (flattenData.length) { diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index 069bec51..8b0149cf 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -1,14 +1,15 @@ import { HeaderLeft } from '@components/Header' import ComponentSeparator from '@components/Separator' -import Timeline from '@components/Timeline' +import CustomText from '@components/Text' import TimelineDefault from '@components/Timeline/Default' -import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query' import { TabSharedStackScreenProps } from '@utils/navigation/navigators' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { QueryKeyTimeline, useTootQuery } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' -import React, { useEffect, useRef, useState } from 'react' +import { useTheme } from '@utils/styles/ThemeManager' +import React, { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, View } from 'react-native' +import { Path, Svg } from 'react-native-svg' const TabSharedToot: React.FC> = ({ navigation, @@ -16,6 +17,7 @@ const TabSharedToot: React.FC> = ({ params: { toot, rootQueryKey } } }) => { + const { colors } = useTheme() const { t } = useTranslation('screenTabs') useEffect(() => { @@ -25,55 +27,23 @@ const TabSharedToot: React.FC> = ({ }) }, []) - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }] - const flRef = useRef(null) - - const [itemsLength, setItemsLength] = useState(0) const scrolled = useRef(false) - const queryClient = useQueryClient() - const observer = new InfiniteQueryObserver(queryClient, { - queryKey, - enabled: false - }) - const replyLevels = useRef<{ id: string; level: number }[]>([]) - const data = useRef() - const highlightIndex = useRef(0) - useEffect(() => { - return observer.subscribe(result => { - if (result.isSuccess) { - const flattenData = result.data?.pages - ? // @ts-ignore - result.data.pages.flatMap(d => [...d.body]) - : [] - // Auto go back when toot page is empty - if (flattenData.length < 1) { + const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }] + const { data } = useTootQuery({ + ...queryKey[1], + options: { + meta: { toot }, + onSuccess: data => { + if (data.body.length < 1) { navigation.goBack() return } - data.current = flattenData - highlightIndex.current = flattenData.findIndex(({ id }) => id === toot.id) - for (const [index, status] of flattenData.entries()) { - if (status.id === toot.id) continue - if (status.in_reply_to_id === toot.id) continue - - if (!replyLevels.current.find(reply => reply.id === status.in_reply_to_id)) { - const prevLevel = - replyLevels.current.find(reply => reply.id === flattenData[index - 1].in_reply_to_id) - ?.level || 0 - replyLevels.current.push({ - id: status.in_reply_to_id, - level: prevLevel + 1 - }) - } - } - - setItemsLength(flattenData.length) if (!scrolled.current) { scrolled.current = true - const pointer = flattenData.findIndex(({ id }) => id === toot.id) + const pointer = data.body.findIndex(({ id }) => id === toot.id) if (pointer < 1) return const length = flRef.current?.props.data?.length if (!length) return @@ -91,78 +61,188 @@ const TabSharedToot: React.FC> = ({ } } } - }) - }, [scrolled.current, replyLevels.current]) + } + }) + + const heights = useRef<(number | undefined)[]>([]) return ( - { - const levels = { - current: - replyLevels.current.find(reply => reply.id === leadingItem.in_reply_to_id)?.level || 0 - } - return ( + { + const MAX_LEVEL = 10 + const ARC = StyleConstants.Avatar.XS / 4 + + const prev = data?.body[index - 1]?._level || 0 + const curr = item._level + const next = data?.body[index + 1]?._level || 0 + + return ( + (data?.highlightIndex || 0) + ? Math.min(item._level, MAX_LEVEL) * StyleConstants.Spacing.S + : undefined + }} + onLayout={({ + nativeEvent: { + layout: { height } + } + }) => (heights.current[index] = height)} + > + + {curr > 1 || next > 1 + ? [...new Array(curr)].map((_, i) => { + if (i > MAX_LEVEL) return null + + const lastLine = curr === i + 1 + if (lastLine) { + if (curr === prev + 1 || curr === next - 1) { + if (curr > next) { + return null + } + return ( + + + + ) + } else { + if (i >= curr - 2) return null + return ( + + + + ) + } + } else { + if (i >= next - 1) { + return ( + + + + ) + } else { + return ( + + + + ) + } + } + }) + : null} + {/* + + */} + + ) + }} + initialNumToRender={6} + maxToRenderPerBatch={3} + ItemSeparatorComponent={({ leadingItem }) => { + return ( + <> - ) - }, - renderItem: ({ item, index }) => { - const levels = { - previous: - replyLevels.current.find( - reply => reply.id === data.current?.[index - 1]?.in_reply_to_id - )?.level || 0, - current: - replyLevels.current.find(reply => reply.id === item.in_reply_to_id)?.level || 0, - next: - replyLevels.current.find( - reply => reply.id === data.current?.[index + 1]?.in_reply_to_id - )?.level || 0 - } - - return ( - - - - ) - }, - onScrollToIndexFailed: error => { - const offset = error.averageItemLength * error.index - flRef.current?.scrollToOffset({ offset }) - try { - error.index < itemsLength && - setTimeout( - () => - flRef.current?.scrollToIndex({ - index: error.index, - viewOffset: 100 - }), - 500 - ) - } catch {} - } + {leadingItem._level > 1 + ? [...new Array(leadingItem._level - 1)].map((_, i) => ( + + + + )) + : null} + + ) + }} + onScrollToIndexFailed={error => { + const offset = error.averageItemLength * error.index + flRef.current?.scrollToOffset({ offset }) + try { + error.index < (data?.body.length || 0) && + setTimeout( + () => + flRef.current?.scrollToIndex({ + index: error.index, + viewOffset: 100 + }), + 500 + ) + } catch {} }} - disableRefresh - disableInfinity /> ) } diff --git a/src/screens/Tabs/Shared/Users.tsx b/src/screens/Tabs/Shared/Users.tsx index ff791ac4..7f6a118e 100644 --- a/src/screens/Tabs/Shared/Users.tsx +++ b/src/screens/Tabs/Shared/Users.tsx @@ -7,6 +7,7 @@ import apiInstance from '@utils/api/instance' import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { SearchResult } from '@utils/queryHooks/search' import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users' +import { flattenPages } from '@utils/queryHooks/utils' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useEffect, useState } from 'react' @@ -39,14 +40,13 @@ const TabSharedUsers: React.FC> = getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id } } }) - const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : [] const [isSearching, setIsSearching] = useState(false) return ( ) => { + // @ts-ignore + const id = queryKey[1].toot + const target = + (meta?.toot as Mastodon.Status) || + undefined || + (await apiInstance({ + method: 'get', + url: `statuses/${id}` + }).then(res => res.body)) + const context = await apiInstance<{ + ancestors: Mastodon.Status[] + descendants: Mastodon.Status[] + }>({ + method: 'get', + url: `statuses/${id}/context` + }) + + const statuses: (Mastodon.Status & { _level?: number })[] = [ + ...context.body.ancestors, + target, + ...context.body.descendants + ] + + const highlightIndex = context.body.ancestors.length + + for (const [index, status] of statuses.entries()) { + if (index < highlightIndex || status.id === id) { + statuses[index]._level = 0 + continue + } + + const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level + statuses[index]._level = (repliedLevel || 0) + 1 + } + + return { body: statuses, highlightIndex } +} + +const useTootQuery = ({ + options, + ...queryKeyParams +}: QueryKeyTimeline[1] & { + options?: UseQueryOptions< + { + body: (Mastodon.Status & { _level: number })[] + highlightIndex: number + }, + AxiosError + > +}) => { + const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }] + return useQuery(queryKey, queryFunctionToot, { + staleTime: 0, + refetchOnMount: true, + ...options + }) +} + +/* ----- */ + export type QueryKeyTimeline = [ 'Timeline', ( @@ -36,16 +99,16 @@ export type QueryKeyTimeline = [ page: 'List' list: Mastodon.List['id'] } - | { - page: 'Toot' - toot: Mastodon.Status['id'] - } | { page: 'Account' account: Mastodon.Account['id'] exclude_reblogs: boolean only_media: boolean } + | { + page: 'Toot' + toot: Mastodon.Status['id'] + } ) ] @@ -209,22 +272,6 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext({ - method: 'get', - url: `statuses/${page.toot}` - }) - const res2_1 = await apiInstance<{ - ancestors: Mastodon.Status[] - descendants: Mastodon.Status[] - }>({ - method: 'get', - url: `statuses/${page.toot}/context` - }) - return { - body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants] - } default: return Promise.reject() } @@ -454,4 +501,4 @@ const useTimelineMutation = ({ }) } -export { useTimelineQuery, useTimelineMutation } +export { useTootQuery, useTimelineQuery, useTimelineMutation } diff --git a/src/utils/queryHooks/utils.ts b/src/utils/queryHooks/utils.ts index 9a38f971..a4455f9f 100644 --- a/src/utils/queryHooks/utils.ts +++ b/src/utils/queryHooks/utils.ts @@ -1,3 +1,4 @@ +import { InfiniteData } from '@tanstack/react-query' import { PagedResponse } from '@utils/api/helpers' export const infinitePageParams = { @@ -6,3 +7,6 @@ export const infinitePageParams = { getNextPageParam: (lastPage: PagedResponse) => lastPage.links?.next && { max_id: lastPage.links.next } } + +export const flattenPages = (data: InfiniteData> | undefined): T[] | [] => + data?.pages.map(page => page.body).flat() || [] diff --git a/src/utils/styles/themes.ts b/src/utils/styles/themes.ts index 92d02121..fe6b05c4 100644 --- a/src/utils/styles/themes.ts +++ b/src/utils/styles/themes.ts @@ -89,9 +89,9 @@ const themeColors: { }, border: { - light: 'rgba(25, 25, 25, 0.3)', - dark_lighter: 'rgba(255, 255, 255, 0.3)', - dark_darker: 'rgba(255, 255, 255, 0.3)' + light: 'rgb(180, 180, 180)', + dark_lighter: 'rgb(90, 90, 90)', + dark_darker: 'rgb(90, 90, 90)' }, shimmerDefault: {