mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	
							
								
								
									
										4
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Props> = ({
 | 
			
		||||
        {
 | 
			
		||||
          backgroundColor: colors.backgroundDefault,
 | 
			
		||||
          borderTopColor: colors.border,
 | 
			
		||||
          borderTopWidth: StyleSheet.hairlineWidth,
 | 
			
		||||
          borderTopWidth: 1,
 | 
			
		||||
          marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
 | 
			
		||||
          marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Props> = ({
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
 | 
			
		||||
 | 
			
		||||
  const flRef = useRef<FlatList>(null)
 | 
			
		||||
 | 
			
		||||
  const scrollY = useSharedValue(0)
 | 
			
		||||
@@ -112,7 +111,7 @@ const Timeline: React.FC<Props> = ({
 | 
			
		||||
        scrollEventThrottle={16}
 | 
			
		||||
        onScroll={onScroll}
 | 
			
		||||
        windowSize={7}
 | 
			
		||||
        data={flattenData}
 | 
			
		||||
        data={flattenPages(data)}
 | 
			
		||||
        initialNumToRender={6}
 | 
			
		||||
        maxToRenderPerBatch={3}
 | 
			
		||||
        onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
 | 
			
		||||
  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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> = ({
 | 
			
		||||
  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<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
 | 
			
		||||
 | 
			
		||||
  const mutation = useListAccountsMutation({
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      haptics('Light')
 | 
			
		||||
@@ -53,7 +52,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FlatList
 | 
			
		||||
      data={flattenData}
 | 
			
		||||
      data={flattenPages(data)}
 | 
			
		||||
      renderItem={({ item, index }) => (
 | 
			
		||||
        <ComponentAccount
 | 
			
		||||
          key={index}
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
 | 
			
		||||
              fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
 | 
			
		||||
              lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
 | 
			
		||||
              color: (fontSize || 0) === size ? colors.primaryDefault : colors.secondary,
 | 
			
		||||
              borderWidth: StyleSheet.hairlineWidth,
 | 
			
		||||
              borderWidth: 1,
 | 
			
		||||
              borderColor: colors.border
 | 
			
		||||
            }}
 | 
			
		||||
            fontWeight={(fontSize || 0) === size ? 'Bold' : undefined}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useEffect, useRef } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native'
 | 
			
		||||
import { KeyboardAvoidingView, Platform, View } from 'react-native'
 | 
			
		||||
import { ScrollView } from 'react-native-gesture-handler'
 | 
			
		||||
 | 
			
		||||
const TabMeSwitch: React.FC = () => {
 | 
			
		||||
@@ -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
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Props> = ({ 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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
  navigation,
 | 
			
		||||
@@ -16,6 +17,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
    params: { toot, rootQueryKey }
 | 
			
		||||
  }
 | 
			
		||||
}) => {
 | 
			
		||||
  const { colors } = useTheme()
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -25,55 +27,23 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
    })
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
 | 
			
		||||
 | 
			
		||||
  const flRef = useRef<FlatList>(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<Mastodon.Status[]>()
 | 
			
		||||
  const highlightIndex = useRef<number>(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<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [scrolled.current, replyLevels.current])
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const heights = useRef<(number | undefined)[]>([])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Timeline
 | 
			
		||||
      flRef={flRef}
 | 
			
		||||
      queryKey={queryKey}
 | 
			
		||||
      queryOptions={{ staleTime: 0, refetchOnMount: true }}
 | 
			
		||||
      customProps={{
 | 
			
		||||
        ItemSeparatorComponent: ({ leadingItem }) => {
 | 
			
		||||
          const levels = {
 | 
			
		||||
            current:
 | 
			
		||||
              replyLevels.current.find(reply => reply.id === leadingItem.in_reply_to_id)?.level || 0
 | 
			
		||||
          }
 | 
			
		||||
          return (
 | 
			
		||||
    <FlatList
 | 
			
		||||
      ref={flRef}
 | 
			
		||||
      scrollEventThrottle={16}
 | 
			
		||||
      windowSize={7}
 | 
			
		||||
      data={data?.body}
 | 
			
		||||
      renderItem={({ item, index }) => {
 | 
			
		||||
        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 (
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              paddingLeft:
 | 
			
		||||
                index > (data?.highlightIndex || 0)
 | 
			
		||||
                  ? Math.min(item._level, MAX_LEVEL) * StyleConstants.Spacing.S
 | 
			
		||||
                  : undefined
 | 
			
		||||
            }}
 | 
			
		||||
            onLayout={({
 | 
			
		||||
              nativeEvent: {
 | 
			
		||||
                layout: { height }
 | 
			
		||||
              }
 | 
			
		||||
            }) => (heights.current[index] = height)}
 | 
			
		||||
          >
 | 
			
		||||
            <TimelineDefault
 | 
			
		||||
              item={item}
 | 
			
		||||
              queryKey={queryKey}
 | 
			
		||||
              rootQueryKey={rootQueryKey}
 | 
			
		||||
              highlighted={toot.id === item.id}
 | 
			
		||||
              isConversation={toot.id !== item.id}
 | 
			
		||||
            />
 | 
			
		||||
            {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 (
 | 
			
		||||
                        <Svg key={i} style={{ position: 'absolute' }}>
 | 
			
		||||
                          <Path
 | 
			
		||||
                            d={
 | 
			
		||||
                              `M ${curr * StyleConstants.Spacing.S + ARC} ${
 | 
			
		||||
                                StyleConstants.Spacing.M + StyleConstants.Avatar.XS / 2
 | 
			
		||||
                              } ` +
 | 
			
		||||
                              `a ${ARC} ${ARC} 0 0 0 -${ARC} ${ARC} ` +
 | 
			
		||||
                              `v 999`
 | 
			
		||||
                            }
 | 
			
		||||
                            strokeWidth={1}
 | 
			
		||||
                            stroke={colors.border}
 | 
			
		||||
                            strokeOpacity={0.6}
 | 
			
		||||
                          />
 | 
			
		||||
                        </Svg>
 | 
			
		||||
                      )
 | 
			
		||||
                    } else {
 | 
			
		||||
                      if (i >= curr - 2) return null
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Svg key={i} style={{ position: 'absolute' }}>
 | 
			
		||||
                          <Path
 | 
			
		||||
                            d={
 | 
			
		||||
                              `M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
 | 
			
		||||
                              `v ${
 | 
			
		||||
                                (heights.current[index] || 999) -
 | 
			
		||||
                                (StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 -
 | 
			
		||||
                                StyleConstants.Avatar.XS / 2
 | 
			
		||||
                              } ` +
 | 
			
		||||
                              `a ${ARC} ${ARC} 0 0 0 ${ARC} ${ARC}`
 | 
			
		||||
                            }
 | 
			
		||||
                            strokeWidth={1}
 | 
			
		||||
                            stroke={colors.border}
 | 
			
		||||
                            strokeOpacity={0.6}
 | 
			
		||||
                          />
 | 
			
		||||
                        </Svg>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    if (i >= next - 1) {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Svg key={i} style={{ position: 'absolute' }}>
 | 
			
		||||
                          <Path
 | 
			
		||||
                            d={
 | 
			
		||||
                              `M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
 | 
			
		||||
                              `v ${
 | 
			
		||||
                                (heights.current[index] || 999) -
 | 
			
		||||
                                (StyleConstants.Spacing.S * 1.5 +
 | 
			
		||||
                                  StyleConstants.Font.Size.L * 1.35) /
 | 
			
		||||
                                  2
 | 
			
		||||
                              } ` +
 | 
			
		||||
                              `h ${ARC}`
 | 
			
		||||
                            }
 | 
			
		||||
                            strokeWidth={1}
 | 
			
		||||
                            stroke={colors.border}
 | 
			
		||||
                            strokeOpacity={0.6}
 | 
			
		||||
                          />
 | 
			
		||||
                        </Svg>
 | 
			
		||||
                      )
 | 
			
		||||
                    } else {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Svg key={i} style={{ position: 'absolute' }}>
 | 
			
		||||
                          <Path
 | 
			
		||||
                            d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 999`}
 | 
			
		||||
                            strokeWidth={1}
 | 
			
		||||
                            stroke={colors.border}
 | 
			
		||||
                            strokeOpacity={0.6}
 | 
			
		||||
                          />
 | 
			
		||||
                        </Svg>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                })
 | 
			
		||||
              : null}
 | 
			
		||||
            {/* <CustomText
 | 
			
		||||
              children={data?.body[index - 1]?._level}
 | 
			
		||||
              style={{ position: 'absolute', top: 4, left: 4, color: colors.red }}
 | 
			
		||||
            />
 | 
			
		||||
            <CustomText
 | 
			
		||||
              children={item._level}
 | 
			
		||||
              style={{ position: 'absolute', top: 20, left: 4, color: colors.yellow }}
 | 
			
		||||
            />
 | 
			
		||||
            <CustomText
 | 
			
		||||
              children={data?.body[index + 1]?._level}
 | 
			
		||||
              style={{ position: 'absolute', top: 36, left: 4, color: colors.green }}
 | 
			
		||||
            /> */}
 | 
			
		||||
          </View>
 | 
			
		||||
        )
 | 
			
		||||
      }}
 | 
			
		||||
      initialNumToRender={6}
 | 
			
		||||
      maxToRenderPerBatch={3}
 | 
			
		||||
      ItemSeparatorComponent={({ leadingItem }) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <>
 | 
			
		||||
            <ComponentSeparator
 | 
			
		||||
              extraMarginLeft={
 | 
			
		||||
                toot.id === leadingItem.id
 | 
			
		||||
                  ? 0
 | 
			
		||||
                  : StyleConstants.Avatar.XS +
 | 
			
		||||
                    StyleConstants.Spacing.S +
 | 
			
		||||
                    Math.max(0, levels.current - 1) * 8
 | 
			
		||||
                    Math.max(0, leadingItem._level - 1) * 8
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
        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 (
 | 
			
		||||
            <View
 | 
			
		||||
              style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}
 | 
			
		||||
            >
 | 
			
		||||
              <TimelineDefault
 | 
			
		||||
                item={item}
 | 
			
		||||
                queryKey={queryKey}
 | 
			
		||||
                rootQueryKey={rootQueryKey}
 | 
			
		||||
                highlighted={toot.id === item.id}
 | 
			
		||||
                isConversation={toot.id !== item.id}
 | 
			
		||||
              />
 | 
			
		||||
            </View>
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
        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) => (
 | 
			
		||||
                  <Svg key={i} style={{ position: 'absolute', top: -1 }}>
 | 
			
		||||
                    <Path
 | 
			
		||||
                      d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 1`}
 | 
			
		||||
                      strokeWidth={1}
 | 
			
		||||
                      stroke={colors.border}
 | 
			
		||||
                      strokeOpacity={0.6}
 | 
			
		||||
                    />
 | 
			
		||||
                  </Svg>
 | 
			
		||||
                ))
 | 
			
		||||
              : 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
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
 | 
			
		||||
      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 (
 | 
			
		||||
    <FlatList
 | 
			
		||||
      windowSize={7}
 | 
			
		||||
      data={flattenData}
 | 
			
		||||
      data={flattenPages(data)}
 | 
			
		||||
      style={{
 | 
			
		||||
        minHeight: '100%',
 | 
			
		||||
        paddingVertical: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
import haptics from '@components/haptics'
 | 
			
		||||
import {
 | 
			
		||||
    MutationOptions,
 | 
			
		||||
    QueryFunctionContext,
 | 
			
		||||
    useInfiniteQuery,
 | 
			
		||||
    UseInfiniteQueryOptions,
 | 
			
		||||
    useMutation
 | 
			
		||||
  MutationOptions,
 | 
			
		||||
  QueryFunctionContext,
 | 
			
		||||
  useInfiniteQuery,
 | 
			
		||||
  UseInfiniteQueryOptions,
 | 
			
		||||
  useMutation,
 | 
			
		||||
  useQuery,
 | 
			
		||||
  UseQueryOptions
 | 
			
		||||
} from '@tanstack/react-query'
 | 
			
		||||
import { PagedResponse } from '@utils/api/helpers'
 | 
			
		||||
import apiInstance from '@utils/api/instance'
 | 
			
		||||
@@ -17,6 +19,67 @@ import deleteItem from './timeline/deleteItem'
 | 
			
		||||
import editItem from './timeline/editItem'
 | 
			
		||||
import updateStatusProperty from './timeline/updateStatusProperty'
 | 
			
		||||
 | 
			
		||||
const queryFunctionToot = async ({ queryKey, meta }: QueryFunctionContext<QueryKeyTimeline>) => {
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  const id = queryKey[1].toot
 | 
			
		||||
  const target =
 | 
			
		||||
    (meta?.toot as Mastodon.Status) ||
 | 
			
		||||
    undefined ||
 | 
			
		||||
    (await apiInstance<Mastodon.Status>({
 | 
			
		||||
      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<Query
 | 
			
		||||
        url: `timelines/list/${page.list}`,
 | 
			
		||||
        params
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    case 'Toot':
 | 
			
		||||
      const res1_1 = await apiInstance<Mastodon.Status>({
 | 
			
		||||
        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 }
 | 
			
		||||
 
 | 
			
		||||
@@ -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<any>) =>
 | 
			
		||||
    lastPage.links?.next && { max_id: lastPage.links.next }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>
 | 
			
		||||
  data?.pages.map(page => page.body).flat() || []
 | 
			
		||||
 
 | 
			
		||||
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user