mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	| @@ -61,7 +61,6 @@ | ||||
|     "expo-video-thumbnails": "^7.0.0", | ||||
|     "expo-web-browser": "~12.0.0", | ||||
|     "i18next": "^22.0.6", | ||||
|     "li": "^1.3.0", | ||||
|     "linkify-it": "^4.0.1", | ||||
|     "lodash": "^4.17.21", | ||||
|     "react": "^18.2.0", | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/@types/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/@types/app.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ declare namespace App { | ||||
|     | 'Following' | ||||
|     | 'Local' | ||||
|     | 'LocalPublic' | ||||
|     | 'Trending' | ||||
|     | 'Notifications' | ||||
|     | 'Hashtag' | ||||
|     | 'List' | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/@types/untyped.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/@types/untyped.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,5 @@ | ||||
| declare module 'gl-react-blurhash' | ||||
| declare module 'htmlparser2-without-node-native' | ||||
| declare module 'li' | ||||
| declare module 'react-native-feather' | ||||
| declare module 'react-native-htmlview' | ||||
| declare module 'react-native-toast-message' | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { RootState } from '@root/store' | ||||
| import axios, { AxiosRequestConfig } from 'axios' | ||||
| import li from 'li' | ||||
| import { ctx, handleError, userAgent } from './helpers' | ||||
|  | ||||
| export type Params = { | ||||
| @@ -15,9 +14,10 @@ export type Params = { | ||||
|   extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'> | ||||
| } | ||||
|  | ||||
| type LinkFormat = { id: string; isOffset: boolean } | ||||
| export type InstanceResponse<T = unknown> = { | ||||
|   body: T | ||||
|   links: { prev?: string; next?: string } | ||||
|   links: { prev?: LinkFormat; next?: LinkFormat } | ||||
| } | ||||
|  | ||||
| const apiInstance = async <T = unknown>({ | ||||
| @@ -74,17 +74,27 @@ const apiInstance = async <T = unknown>({ | ||||
|     ...extras | ||||
|   }) | ||||
|     .then(response => { | ||||
|       let prev | ||||
|       let next | ||||
|       let links: { | ||||
|         prev?: { id: string; isOffset: boolean } | ||||
|         next?: { id: string; isOffset: boolean } | ||||
|       } = {} | ||||
|  | ||||
|       if (response.headers?.link) { | ||||
|         const headersLinks = li.parse(response.headers?.link) | ||||
|         prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1] | ||||
|         next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1] | ||||
|         const linksParsed = response.headers.link.matchAll( | ||||
|           new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi') | ||||
|         ) | ||||
|         for (const link of linksParsed) { | ||||
|           switch (link[3]) { | ||||
|             case 'prev': | ||||
|               links.prev = { id: link[2], isOffset: link[1].includes('offset') } | ||||
|               break | ||||
|             case 'next': | ||||
|               links.next = { id: link[2], isOffset: link[1].includes('offset') } | ||||
|               break | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return Promise.resolve({ | ||||
|         body: response.data, | ||||
|         links: { prev, next } | ||||
|       }) | ||||
|       return Promise.resolve({ body: response.data, links }) | ||||
|     }) | ||||
|     .catch(handleError()) | ||||
| } | ||||
|   | ||||
| @@ -6,17 +6,11 @@ import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useCallback, useRef } from 'react' | ||||
| import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' | ||||
| import Animated, { | ||||
|   useAnimatedScrollHandler, | ||||
|   useSharedValue | ||||
| } from 'react-native-reanimated' | ||||
| import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' | ||||
| import { useSelector } from 'react-redux' | ||||
| import TimelineEmpty from './Timeline/Empty' | ||||
| import TimelineFooter from './Timeline/Footer' | ||||
| import TimelineRefresh, { | ||||
|   SEPARATION_Y_1, | ||||
|   SEPARATION_Y_2 | ||||
| } from './Timeline/Refresh' | ||||
| import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh' | ||||
|  | ||||
| const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) | ||||
|  | ||||
| @@ -26,8 +20,7 @@ export interface Props { | ||||
|   disableRefresh?: boolean | ||||
|   disableInfinity?: boolean | ||||
|   lookback?: Extract<App.Pages, 'Following' | 'Local' | 'LocalPublic'> | ||||
|   customProps: Partial<FlatListProps<any>> & | ||||
|     Pick<FlatListProps<any>, 'renderItem'> | ||||
|   customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'> | ||||
| } | ||||
|  | ||||
| const Timeline: React.FC<Props> = ({ | ||||
| @@ -39,30 +32,24 @@ const Timeline: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const { | ||||
|     data, | ||||
|     refetch, | ||||
|     isFetching, | ||||
|     isLoading, | ||||
|     fetchNextPage, | ||||
|     isFetchingNextPage | ||||
|   } = useTimelineQuery({ | ||||
|     ...queryKey[1], | ||||
|     options: { | ||||
|       notifyOnChangeProps: Platform.select({ | ||||
|         ios: ['dataUpdatedAt', 'isFetching'], | ||||
|         android: ['dataUpdatedAt', 'isFetching', 'isLoading'] | ||||
|       }), | ||||
|       getNextPageParam: lastPage => | ||||
|         lastPage?.links?.next && { | ||||
|           max_id: lastPage.links.next | ||||
|         } | ||||
|     } | ||||
|   }) | ||||
|   const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } = | ||||
|     useTimelineQuery({ | ||||
|       ...queryKey[1], | ||||
|       options: { | ||||
|         notifyOnChangeProps: Platform.select({ | ||||
|           ios: ['dataUpdatedAt', 'isFetching'], | ||||
|           android: ['dataUpdatedAt', 'isFetching', 'isLoading'] | ||||
|         }), | ||||
|         getNextPageParam: lastPage => | ||||
|           lastPage?.links?.next && { | ||||
|             ...(lastPage.links.next.isOffset | ||||
|               ? { offset: lastPage.links.next.id } | ||||
|               : { max_id: lastPage.links.next.id }) | ||||
|           } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|   const flattenData = data?.pages | ||||
|     ? data.pages?.flatMap(page => [...page.body]) | ||||
|     : [] | ||||
|   const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] | ||||
|  | ||||
|   const onEndReached = useCallback( | ||||
|     () => !disableInfinity && !isFetchingNextPage && fetchNextPage(), | ||||
| @@ -134,10 +121,7 @@ const Timeline: React.FC<Props> = ({ | ||||
|         onEndReached={onEndReached} | ||||
|         onEndReachedThreshold={0.75} | ||||
|         ListFooterComponent={ | ||||
|           <TimelineFooter | ||||
|             queryKey={queryKey} | ||||
|             disableInfinity={disableInfinity} | ||||
|           /> | ||||
|           <TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} /> | ||||
|         } | ||||
|         ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />} | ||||
|         ItemSeparatorComponent={({ leadingItem }) => | ||||
| @@ -145,9 +129,7 @@ const Timeline: React.FC<Props> = ({ | ||||
|             <ComponentSeparator extraMarginLeft={0} /> | ||||
|           ) : ( | ||||
|             <ComponentSeparator | ||||
|               extraMarginLeft={ | ||||
|                 StyleConstants.Avatar.M + StyleConstants.Spacing.S | ||||
|               } | ||||
|               extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S} | ||||
|             /> | ||||
|           ) | ||||
|         } | ||||
|   | ||||
| @@ -21,7 +21,11 @@ const TimelineFooter = React.memo( | ||||
|         enabled: !disableInfinity, | ||||
|         notifyOnChangeProps: ['hasNextPage'], | ||||
|         getNextPageParam: lastPage => | ||||
|           lastPage?.links?.next && { max_id: lastPage.links.next } | ||||
|           lastPage?.links?.next && { | ||||
|             ...(lastPage.links.next.isOffset | ||||
|               ? { offset: lastPage.links.next.id } | ||||
|               : { max_id: lastPage.links.next.id }) | ||||
|           } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
| @@ -43,11 +47,7 @@ const TimelineFooter = React.memo( | ||||
|             <Trans | ||||
|               i18nKey='componentTimeline:end.message' | ||||
|               components={[ | ||||
|                 <Icon | ||||
|                   name='Coffee' | ||||
|                   size={StyleConstants.Font.Size.S} | ||||
|                   color={colors.secondary} | ||||
|                 /> | ||||
|                 <Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} /> | ||||
|               ]} | ||||
|             /> | ||||
|           </CustomText> | ||||
|   | ||||
| @@ -1,10 +1,6 @@ | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| import { | ||||
|   QueryKeyTimeline, | ||||
|   TimelineData, | ||||
|   useTimelineQuery | ||||
| } from '@utils/queryHooks/timeline' | ||||
| import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useCallback, useRef, useState } from 'react' | ||||
| @@ -31,14 +27,8 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 | ||||
| export const SEPARATION_Y_1 = -( | ||||
|   CONTAINER_HEIGHT / 2 + | ||||
|   StyleConstants.Font.Size.S / 2 | ||||
| ) | ||||
| export const SEPARATION_Y_2 = -( | ||||
|   CONTAINER_HEIGHT * 1.5 + | ||||
|   StyleConstants.Font.Size.S / 2 | ||||
| ) | ||||
| export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2) | ||||
| export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2) | ||||
|  | ||||
| const TimelineRefresh: React.FC<Props> = ({ | ||||
|   flRef, | ||||
| @@ -57,87 +47,77 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|   const fetchingLatestIndex = useRef(0) | ||||
|   const refetchActive = useRef(false) | ||||
|  | ||||
|   const { | ||||
|     refetch, | ||||
|     isFetching, | ||||
|     isLoading, | ||||
|     fetchPreviousPage, | ||||
|     hasPreviousPage, | ||||
|     isFetchingNextPage | ||||
|   } = useTimelineQuery({ | ||||
|     ...queryKey[1], | ||||
|     options: { | ||||
|       getPreviousPageParam: firstPage => | ||||
|         firstPage?.links?.prev && { | ||||
|           min_id: firstPage.links.prev, | ||||
|           // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 | ||||
|           limit: '3' | ||||
|   const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } = | ||||
|     useTimelineQuery({ | ||||
|       ...queryKey[1], | ||||
|       options: { | ||||
|         getPreviousPageParam: firstPage => | ||||
|           firstPage?.links?.prev && { | ||||
|             ...(firstPage.links.prev.isOffset | ||||
|               ? { offset: firstPage.links.prev.id } | ||||
|               : { max_id: firstPage.links.prev.id }), | ||||
|             // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 | ||||
|             limit: '3' | ||||
|           }, | ||||
|         select: data => { | ||||
|           if (refetchActive.current) { | ||||
|             data.pageParams = [data.pageParams[0]] | ||||
|             data.pages = [data.pages[0]] | ||||
|             refetchActive.current = false | ||||
|           } | ||||
|           return data | ||||
|         }, | ||||
|       select: data => { | ||||
|         if (refetchActive.current) { | ||||
|           data.pageParams = [data.pageParams[0]] | ||||
|           data.pages = [data.pages[0]] | ||||
|           refetchActive.current = false | ||||
|         } | ||||
|         return data | ||||
|       }, | ||||
|       onSuccess: () => { | ||||
|         if (fetchingLatestIndex.current > 0) { | ||||
|           if (fetchingLatestIndex.current > 5) { | ||||
|             clearFirstPage() | ||||
|             fetchingLatestIndex.current = 0 | ||||
|           } else { | ||||
|             if (hasPreviousPage) { | ||||
|               fetchPreviousPage() | ||||
|               fetchingLatestIndex.current++ | ||||
|             } else { | ||||
|         onSuccess: () => { | ||||
|           if (fetchingLatestIndex.current > 0) { | ||||
|             if (fetchingLatestIndex.current > 5) { | ||||
|               clearFirstPage() | ||||
|               fetchingLatestIndex.current = 0 | ||||
|             } else { | ||||
|               if (hasPreviousPage) { | ||||
|                 fetchPreviousPage() | ||||
|                 fetchingLatestIndex.current++ | ||||
|               } else { | ||||
|                 clearFirstPage() | ||||
|                 fetchingLatestIndex.current = 0 | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|     }) | ||||
|  | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const queryClient = useQueryClient() | ||||
|   const clearFirstPage = () => { | ||||
|     queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( | ||||
|       queryKey, | ||||
|       data => { | ||||
|         if (data?.pages[0] && data.pages[0].body.length === 0) { | ||||
|           return { | ||||
|             pages: data.pages.slice(1), | ||||
|             pageParams: data.pageParams.slice(1) | ||||
|           } | ||||
|         } else { | ||||
|           return data | ||||
|     queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => { | ||||
|       if (data?.pages[0] && data.pages[0].body.length === 0) { | ||||
|         return { | ||||
|           pages: data.pages.slice(1), | ||||
|           pageParams: data.pageParams.slice(1) | ||||
|         } | ||||
|       } else { | ||||
|         return data | ||||
|       } | ||||
|     ) | ||||
|     }) | ||||
|   } | ||||
|   const prepareRefetch = () => { | ||||
|     refetchActive.current = true | ||||
|     queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( | ||||
|       queryKey, | ||||
|       data => { | ||||
|         if (data) { | ||||
|           data.pageParams = [undefined] | ||||
|           const newFirstPage: TimelineData = { body: [] } | ||||
|           for (let page of data.pages) { | ||||
|             // @ts-ignore | ||||
|             newFirstPage.body.push(...page.body) | ||||
|             if (newFirstPage.body.length > 10) break | ||||
|           } | ||||
|           data.pages = [newFirstPage] | ||||
|     queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => { | ||||
|       if (data) { | ||||
|         data.pageParams = [undefined] | ||||
|         const newFirstPage: TimelineData = { body: [] } | ||||
|         for (let page of data.pages) { | ||||
|           // @ts-ignore | ||||
|           newFirstPage.body.push(...page.body) | ||||
|           if (newFirstPage.body.length > 10) break | ||||
|         } | ||||
|  | ||||
|         return data | ||||
|         data.pages = [newFirstPage] | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|       return data | ||||
|     }) | ||||
|   } | ||||
|   const callRefetch = async () => { | ||||
|     await refetch() | ||||
| @@ -161,10 +141,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|     ] | ||||
|   })) | ||||
|   const arrowTop = useAnimatedStyle(() => ({ | ||||
|     marginTop: | ||||
|       scrollY.value < SEPARATION_Y_2 | ||||
|         ? withTiming(CONTAINER_HEIGHT) | ||||
|         : withTiming(0) | ||||
|     marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0) | ||||
|   })) | ||||
|  | ||||
|   const arrowStage = useSharedValue(0) | ||||
| @@ -241,8 +218,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|   const headerPadding = useAnimatedStyle( | ||||
|     () => ({ | ||||
|       paddingTop: | ||||
|         fetchingLatestIndex.current !== 0 || | ||||
|         (isFetching && !isLoading && !isFetchingNextPage) | ||||
|         fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage) | ||||
|           ? withTiming(StyleConstants.Spacing.M * 2.5) | ||||
|           : withTiming(0) | ||||
|     }), | ||||
| @@ -254,10 +230,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|       <View style={styles.base}> | ||||
|         {isFetching ? ( | ||||
|           <View style={styles.container2}> | ||||
|             <Circle | ||||
|               size={StyleConstants.Font.Size.L} | ||||
|               color={colors.secondary} | ||||
|             /> | ||||
|             <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> | ||||
|           </View> | ||||
|         ) : ( | ||||
|           <> | ||||
|   | ||||
| @@ -6,8 +6,9 @@ | ||||
|     "public": { | ||||
|       "name": "", | ||||
|       "segments": { | ||||
|         "left": "Federated", | ||||
|         "right": "Local" | ||||
|         "federated": "Federated", | ||||
|         "local": "Local", | ||||
|         "trending": "Trending" | ||||
|       } | ||||
|     }, | ||||
|     "notifications": { | ||||
|   | ||||
| @@ -3,16 +3,10 @@ import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' | ||||
| import { useAppDispatch } from '@root/store' | ||||
| import { | ||||
|   RootStackScreenProps, | ||||
|   ScreenTabsStackParamList | ||||
| } from '@utils/navigation/navigators' | ||||
| import { getPreviousTab } from '@utils/slices/contextsSlice' | ||||
| import { | ||||
|   getInstanceAccount, | ||||
|   getInstanceActive | ||||
| } from '@utils/slices/instancesSlice' | ||||
| import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' | ||||
| import { getVersionUpdate, retrieveVersionLatest } from '@utils/slices/appSlice' | ||||
| import { getPreviousTab } from '@utils/slices/contextsSlice' | ||||
| import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useMemo } from 'react' | ||||
| import { Platform } from 'react-native' | ||||
| @@ -125,11 +119,7 @@ const ScreenTabs = React.memo( | ||||
|       > | ||||
|         <Tab.Screen name='Tab-Local' component={TabLocal} /> | ||||
|         <Tab.Screen name='Tab-Public' component={TabPublic} /> | ||||
|         <Tab.Screen | ||||
|           name='Tab-Compose' | ||||
|           component={composeComponent} | ||||
|           listeners={composeListeners} | ||||
|         /> | ||||
|         <Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} /> | ||||
|         <Tab.Screen name='Tab-Notifications' component={TabNotifications} /> | ||||
|         <Tab.Screen | ||||
|           name='Tab-Me' | ||||
|   | ||||
| @@ -1,106 +0,0 @@ | ||||
| import { HeaderCenter, HeaderRight } from '@components/Header' | ||||
| import Timeline from '@components/Timeline' | ||||
| import TimelineDefault from '@components/Timeline/Default' | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { ScreenTabsScreenProps, TabLocalStackParamList } from '@utils/navigation/navigators' | ||||
| import usePopToTop from '@utils/navigation/usePopToTop' | ||||
| import { useListsQuery } from '@utils/queryHooks/lists' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import React, { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import * as DropdownMenu from 'zeego/dropdown-menu' | ||||
| import TabShared from './Shared' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabLocalStackParamList>() | ||||
|  | ||||
| const TabLocal = React.memo( | ||||
|   ({ navigation }: ScreenTabsScreenProps<'Tab-Local'>) => { | ||||
|     const { t } = useTranslation('screenTabs') | ||||
|  | ||||
|     const { data: lists } = useListsQuery({}) | ||||
|  | ||||
|     const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }]) | ||||
|  | ||||
|     usePopToTop() | ||||
|  | ||||
|     return ( | ||||
|       <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Local-Root' | ||||
|           options={{ | ||||
|             headerTitle: () => ( | ||||
|               <DropdownMenu.Root> | ||||
|                 <DropdownMenu.Trigger> | ||||
|                   <HeaderCenter | ||||
|                     dropdown={(lists?.length ?? 0) > 0} | ||||
|                     content={ | ||||
|                       queryKey[1].page === 'List' && queryKey[1].list?.length | ||||
|                         ? lists?.find(list => list.id === queryKey[1].list)?.title | ||||
|                         : t('tabs.local.name') | ||||
|                     } | ||||
|                   /> | ||||
|                 </DropdownMenu.Trigger> | ||||
|  | ||||
|                 <DropdownMenu.Content> | ||||
|                   {lists?.length | ||||
|                     ? [ | ||||
|                         { | ||||
|                           key: 'default', | ||||
|                           item: { | ||||
|                             onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]), | ||||
|                             disabled: queryKey[1].page === 'Following', | ||||
|                             destructive: false, | ||||
|                             hidden: false | ||||
|                           }, | ||||
|                           title: t('tabs.local.name'), | ||||
|                           icon: '' | ||||
|                         }, | ||||
|                         ...lists?.map(list => ({ | ||||
|                           key: list.id, | ||||
|                           item: { | ||||
|                             onSelect: () => | ||||
|                               setQueryKey(['Timeline', { page: 'List', list: list.id }]), | ||||
|                             disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id, | ||||
|                             destructive: false, | ||||
|                             hidden: false | ||||
|                           }, | ||||
|                           title: list.title, | ||||
|                           icon: '' | ||||
|                         })) | ||||
|                       ].map(menu => ( | ||||
|                         <DropdownMenu.Item key={menu.key} {...menu.item}> | ||||
|                           <DropdownMenu.ItemTitle children={menu.title} /> | ||||
|                           <DropdownMenu.ItemIcon iosIconName={menu.icon} /> | ||||
|                         </DropdownMenu.Item> | ||||
|                       )) | ||||
|                     : undefined} | ||||
|                 </DropdownMenu.Content> | ||||
|               </DropdownMenu.Root> | ||||
|             ), | ||||
|             headerRight: () => ( | ||||
|               <HeaderRight | ||||
|                 accessibilityLabel={t('common.search.accessibilityLabel')} | ||||
|                 accessibilityHint={t('common.search.accessibilityHint')} | ||||
|                 content='Search' | ||||
|                 onPress={() => navigation.navigate('Tab-Local', { screen: 'Tab-Shared-Search' })} | ||||
|               /> | ||||
|             ) | ||||
|           }} | ||||
|           children={() => ( | ||||
|             <Timeline | ||||
|               queryKey={queryKey} | ||||
|               lookback='Following' | ||||
|               customProps={{ | ||||
|                 renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} /> | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         {TabShared({ Stack })} | ||||
|       </Stack.Navigator> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| export default TabLocal | ||||
							
								
								
									
										96
									
								
								src/screens/Tabs/Local/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/screens/Tabs/Local/Root.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| import { HeaderCenter, HeaderRight } from '@components/Header' | ||||
| import Timeline from '@components/Timeline' | ||||
| import TimelineDefault from '@components/Timeline/Default' | ||||
| import { NativeStackScreenProps } from '@react-navigation/native-stack' | ||||
| import { TabLocalStackParamList } from '@utils/navigation/navigators' | ||||
| import usePopToTop from '@utils/navigation/usePopToTop' | ||||
| import { useListsQuery } from '@utils/queryHooks/lists' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import * as DropdownMenu from 'zeego/dropdown-menu' | ||||
|  | ||||
| const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({ | ||||
|   navigation | ||||
| }) => { | ||||
|   const { t } = useTranslation('screenTabs') | ||||
|  | ||||
|   const { data: lists } = useListsQuery() | ||||
|  | ||||
|   const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     navigation.setOptions({ | ||||
|       headerTitle: () => ( | ||||
|         <DropdownMenu.Root> | ||||
|           <DropdownMenu.Trigger> | ||||
|             <HeaderCenter | ||||
|               dropdown={(lists?.length ?? 0) > 0} | ||||
|               content={ | ||||
|                 queryKey[1].page === 'List' && queryKey[1].list?.length | ||||
|                   ? lists?.find(list => list.id === queryKey[1].list)?.title | ||||
|                   : t('tabs.local.name') | ||||
|               } | ||||
|             /> | ||||
|           </DropdownMenu.Trigger> | ||||
|  | ||||
|           <DropdownMenu.Content> | ||||
|             {lists?.length | ||||
|               ? [ | ||||
|                   { | ||||
|                     key: 'default', | ||||
|                     item: { | ||||
|                       onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]), | ||||
|                       disabled: queryKey[1].page === 'Following', | ||||
|                       destructive: false, | ||||
|                       hidden: false | ||||
|                     }, | ||||
|                     title: t('tabs.local.name'), | ||||
|                     icon: '' | ||||
|                   }, | ||||
|                   ...lists?.map(list => ({ | ||||
|                     key: list.id, | ||||
|                     item: { | ||||
|                       onSelect: () => setQueryKey(['Timeline', { page: 'List', list: list.id }]), | ||||
|                       disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id, | ||||
|                       destructive: false, | ||||
|                       hidden: false | ||||
|                     }, | ||||
|                     title: list.title, | ||||
|                     icon: '' | ||||
|                   })) | ||||
|                 ].map(menu => ( | ||||
|                   <DropdownMenu.Item key={menu.key} {...menu.item}> | ||||
|                     <DropdownMenu.ItemTitle children={menu.title} /> | ||||
|                     <DropdownMenu.ItemIcon iosIconName={menu.icon} /> | ||||
|                   </DropdownMenu.Item> | ||||
|                 )) | ||||
|               : undefined} | ||||
|           </DropdownMenu.Content> | ||||
|         </DropdownMenu.Root> | ||||
|       ), | ||||
|       headerRight: () => ( | ||||
|         <HeaderRight | ||||
|           accessibilityLabel={t('common.search.accessibilityLabel')} | ||||
|           accessibilityHint={t('common.search.accessibilityHint')} | ||||
|           content='Search' | ||||
|           onPress={() => navigation.navigate('Tab-Shared-Search')} | ||||
|         /> | ||||
|       ) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   usePopToTop() | ||||
|  | ||||
|   return ( | ||||
|     <Timeline | ||||
|       queryKey={queryKey} | ||||
|       lookback='Following' | ||||
|       customProps={{ | ||||
|         renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} /> | ||||
|       }} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Root | ||||
							
								
								
									
										18
									
								
								src/screens/Tabs/Local/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/screens/Tabs/Local/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { TabLocalStackParamList } from '@utils/navigation/navigators' | ||||
| import React from 'react' | ||||
| import TabShared from '../Shared' | ||||
| import Root from './Root' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabLocalStackParamList>() | ||||
|  | ||||
| const TabLocal: React.FC = () => { | ||||
|   return ( | ||||
|     <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|       <Stack.Screen name='Tab-Local-Root' component={Root} /> | ||||
|       {TabShared({ Stack })} | ||||
|     </Stack.Navigator> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabLocal | ||||
| @@ -1,164 +0,0 @@ | ||||
| import { HeaderLeft } from '@components/Header' | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { TabMeStackParamList } from '@utils/navigation/navigators' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import TabMeBookmarks from './Me/Bookmarks' | ||||
| import TabMeConversations from './Me/Cconversations' | ||||
| import TabMeFavourites from './Me/Favourites' | ||||
| import TabMeFollowedTags from './Me/FollowedTags' | ||||
| import TabMeList from './Me/List' | ||||
| import TabMeListAccounts from './Me/List/Accounts' | ||||
| import TabMeListEdit from './Me/List/Edit' | ||||
| import TabMeListList from './Me/List/List' | ||||
| import TabMeProfile from './Me/Profile' | ||||
| import TabMePush from './Me/Push' | ||||
| import TabMeRoot from './Me/Root' | ||||
| import TabMeSettings from './Me/Settings' | ||||
| import TabMeSettingsFontsize from './Me/SettingsFontsize' | ||||
| import TabMeSettingsLanguage from './Me/SettingsLanguage' | ||||
| import TabMeSwitch from './Me/Switch' | ||||
| import TabShared from './Shared' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabMeStackParamList>() | ||||
|  | ||||
| const TabMe = React.memo( | ||||
|   () => { | ||||
|     const { t } = useTranslation('screenTabs') | ||||
|  | ||||
|     return ( | ||||
|       <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Root' | ||||
|           component={TabMeRoot} | ||||
|           options={{ | ||||
|             headerShadowVisible: false, | ||||
|             headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, | ||||
|             headerShown: false | ||||
|           }} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Bookmarks' | ||||
|           component={TabMeBookmarks} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.bookmarks.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Conversations' | ||||
|           component={TabMeConversations} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.conversations.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Favourites' | ||||
|           component={TabMeFavourites} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.favourites.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-FollowedTags' | ||||
|           component={TabMeFollowedTags} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.followedTags.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-List' | ||||
|           component={TabMeList} | ||||
|           options={({ route, navigation }: any) => ({ | ||||
|             title: t('me.stacks.list.name', { list: route.params.title }), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-List-Accounts' | ||||
|           component={TabMeListAccounts} | ||||
|           options={({ navigation, route: { params } }) => ({ | ||||
|             title: t('me.stacks.listAccounts.name', { list: params.title }), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-List-Edit' | ||||
|           component={TabMeListEdit} | ||||
|           options={{ | ||||
|             gestureEnabled: false, | ||||
|             presentation: 'modal' | ||||
|           }} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-List-List' | ||||
|           component={TabMeListList} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.lists.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Profile' | ||||
|           component={TabMeProfile} | ||||
|           options={{ | ||||
|             headerShown: false, | ||||
|             presentation: 'modal' | ||||
|           }} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Push' | ||||
|           component={TabMePush} | ||||
|           options={({ navigation }) => ({ | ||||
|             title: t('me.stacks.push.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Settings' | ||||
|           component={TabMeSettings} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.settings.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Settings-Fontsize' | ||||
|           component={TabMeSettingsFontsize} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.fontSize.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Settings-Language' | ||||
|           component={TabMeSettingsLanguage} | ||||
|           options={({ navigation }: any) => ({ | ||||
|             title: t('me.stacks.language.name'), | ||||
|             headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|           })} | ||||
|         /> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Switch' | ||||
|           component={TabMeSwitch} | ||||
|           options={({ navigation }) => ({ | ||||
|             presentation: 'modal', | ||||
|             headerShown: true, | ||||
|             title: t('me.stacks.switch.name'), | ||||
|             headerLeft: () => ( | ||||
|               <HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} /> | ||||
|             ) | ||||
|           })} | ||||
|         /> | ||||
|  | ||||
|         {TabShared({ Stack })} | ||||
|       </Stack.Navigator> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| export default TabMe | ||||
| @@ -27,7 +27,9 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> | ||||
|     options: { | ||||
|       getNextPageParam: lastPage => | ||||
|         lastPage?.links?.next && { | ||||
|           max_id: lastPage.links.next | ||||
|           ...(lastPage.links.next.isOffset | ||||
|             ? { offset: lastPage.links.next.id } | ||||
|             : { max_id: lastPage.links.next.id }) | ||||
|         } | ||||
|     } | ||||
|   }) | ||||
|   | ||||
							
								
								
									
										159
									
								
								src/screens/Tabs/Me/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/screens/Tabs/Me/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| import { HeaderLeft } from '@components/Header' | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { TabMeStackParamList } from '@utils/navigation/navigators' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import TabMeBookmarks from './Bookmarks' | ||||
| import TabMeConversations from './Cconversations' | ||||
| import TabMeFavourites from './Favourites' | ||||
| import TabMeFollowedTags from './FollowedTags' | ||||
| import TabMeList from './List' | ||||
| import TabMeListAccounts from './List/Accounts' | ||||
| import TabMeListEdit from './List/Edit' | ||||
| import TabMeListList from './List/List' | ||||
| import TabMeProfile from './Profile' | ||||
| import TabMePush from './Push' | ||||
| import TabMeRoot from './Root' | ||||
| import TabMeSettings from './Settings' | ||||
| import TabMeSettingsFontsize from './SettingsFontsize' | ||||
| import TabMeSettingsLanguage from './SettingsLanguage' | ||||
| import TabMeSwitch from './Switch' | ||||
| import TabShared from '../Shared' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabMeStackParamList>() | ||||
|  | ||||
| const TabMe: React.FC = () => { | ||||
|   const { t } = useTranslation('screenTabs') | ||||
|  | ||||
|   return ( | ||||
|     <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Root' | ||||
|         component={TabMeRoot} | ||||
|         options={{ | ||||
|           headerShadowVisible: false, | ||||
|           headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, | ||||
|           headerShown: false | ||||
|         }} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Bookmarks' | ||||
|         component={TabMeBookmarks} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.bookmarks.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Conversations' | ||||
|         component={TabMeConversations} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.conversations.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Favourites' | ||||
|         component={TabMeFavourites} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.favourites.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-FollowedTags' | ||||
|         component={TabMeFollowedTags} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.followedTags.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-List' | ||||
|         component={TabMeList} | ||||
|         options={({ route, navigation }: any) => ({ | ||||
|           title: t('me.stacks.list.name', { list: route.params.title }), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-List-Accounts' | ||||
|         component={TabMeListAccounts} | ||||
|         options={({ navigation, route: { params } }) => ({ | ||||
|           title: t('me.stacks.listAccounts.name', { list: params.title }), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-List-Edit' | ||||
|         component={TabMeListEdit} | ||||
|         options={{ | ||||
|           gestureEnabled: false, | ||||
|           presentation: 'modal' | ||||
|         }} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-List-List' | ||||
|         component={TabMeListList} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.lists.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Profile' | ||||
|         component={TabMeProfile} | ||||
|         options={{ | ||||
|           headerShown: false, | ||||
|           presentation: 'modal' | ||||
|         }} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Push' | ||||
|         component={TabMePush} | ||||
|         options={({ navigation }) => ({ | ||||
|           title: t('me.stacks.push.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Settings' | ||||
|         component={TabMeSettings} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.settings.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Settings-Fontsize' | ||||
|         component={TabMeSettingsFontsize} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.fontSize.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Settings-Language' | ||||
|         component={TabMeSettingsLanguage} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           title: t('me.stacks.language.name'), | ||||
|           headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Switch' | ||||
|         component={TabMeSwitch} | ||||
|         options={({ navigation }) => ({ | ||||
|           presentation: 'modal', | ||||
|           headerShown: true, | ||||
|           title: t('me.stacks.switch.name'), | ||||
|           headerLeft: () => <HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} /> | ||||
|         })} | ||||
|       /> | ||||
|  | ||||
|       {TabShared({ Stack })} | ||||
|     </Stack.Navigator> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabMe | ||||
| @@ -1,110 +0,0 @@ | ||||
| import { HeaderRight } from '@components/Header' | ||||
| import Timeline from '@components/Timeline' | ||||
| import TimelineDefault from '@components/Timeline/Default' | ||||
| import SegmentedControl from '@react-native-community/segmented-control' | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { ScreenTabsScreenProps, TabPublicStackParamList } from '@utils/navigation/navigators' | ||||
| import usePopToTop from '@utils/navigation/usePopToTop' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Dimensions } from 'react-native' | ||||
| import { TabView } from 'react-native-tab-view' | ||||
| import TabShared from './Shared' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabPublicStackParamList>() | ||||
|  | ||||
| const TabPublic = React.memo( | ||||
|   ({ navigation }: ScreenTabsScreenProps<'Tab-Public'>) => { | ||||
|     const { t, i18n } = useTranslation('screenTabs') | ||||
|     const { mode, theme } = useTheme() | ||||
|  | ||||
|     const [segment, setSegment] = useState(0) | ||||
|     const pages: { | ||||
|       title: string | ||||
|       key: Extract<App.Pages, 'Local' | 'LocalPublic'> | ||||
|     }[] = [ | ||||
|       { | ||||
|         title: t('tabs.public.segments.left'), | ||||
|         key: 'LocalPublic' | ||||
|       }, | ||||
|       { | ||||
|         title: t('tabs.public.segments.right'), | ||||
|         key: 'Local' | ||||
|       } | ||||
|     ] | ||||
|     const screenOptionsRoot = useMemo( | ||||
|       () => ({ | ||||
|         headerTitle: () => ( | ||||
|           <SegmentedControl | ||||
|             appearance={mode} | ||||
|             values={pages.map(p => p.title)} | ||||
|             selectedIndex={segment} | ||||
|             onChange={({ nativeEvent }) => setSegment(nativeEvent.selectedSegmentIndex)} | ||||
|             style={{ flexBasis: '65%' }} | ||||
|           /> | ||||
|         ), | ||||
|         headerRight: () => ( | ||||
|           <HeaderRight | ||||
|             accessibilityLabel={t('common.search.accessibilityLabel')} | ||||
|             accessibilityHint={t('common.search.accessibilityHint')} | ||||
|             content='Search' | ||||
|             onPress={() => navigation.navigate('Tab-Public', { screen: 'Tab-Shared-Search' })} | ||||
|           /> | ||||
|         ) | ||||
|       }), | ||||
|       [theme, segment, i18n.language] | ||||
|     ) | ||||
|  | ||||
|     const routes = pages.map(p => ({ key: p.key })) | ||||
|  | ||||
|     const renderScene = useCallback( | ||||
|       ({ | ||||
|         route: { key: page } | ||||
|       }: { | ||||
|         route: { | ||||
|           key: Extract<App.Pages, 'Local' | 'LocalPublic'> | ||||
|         } | ||||
|       }) => { | ||||
|         const queryKey: QueryKeyTimeline = ['Timeline', { page }] | ||||
|         return ( | ||||
|           <Timeline | ||||
|             queryKey={queryKey} | ||||
|             lookback={page} | ||||
|             customProps={{ | ||||
|               renderItem: ({ item }: any) => <TimelineDefault item={item} queryKey={queryKey} /> | ||||
|             }} | ||||
|           /> | ||||
|         ) | ||||
|       }, | ||||
|       [] | ||||
|     ) | ||||
|     const children = useCallback( | ||||
|       () => ( | ||||
|         <TabView | ||||
|           lazy | ||||
|           swipeEnabled | ||||
|           renderScene={renderScene} | ||||
|           renderTabBar={() => null} | ||||
|           onIndexChange={index => setSegment(index)} | ||||
|           navigationState={{ index: segment, routes }} | ||||
|           initialLayout={{ width: Dimensions.get('screen').width }} | ||||
|         /> | ||||
|       ), | ||||
|       [segment] | ||||
|     ) | ||||
|  | ||||
|     usePopToTop() | ||||
|  | ||||
|     return ( | ||||
|       <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|         <Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} /> | ||||
|         {TabShared({ Stack })} | ||||
|       </Stack.Navigator> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| export default TabPublic | ||||
							
								
								
									
										84
									
								
								src/screens/Tabs/Public/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/screens/Tabs/Public/Root.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| import { HeaderRight } from '@components/Header' | ||||
| import Timeline from '@components/Timeline' | ||||
| import TimelineDefault from '@components/Timeline/Default' | ||||
| import SegmentedControl from '@react-native-community/segmented-control' | ||||
| import { NativeStackScreenProps } from '@react-navigation/native-stack' | ||||
| import { TabPublicStackParamList } from '@utils/navigation/navigators' | ||||
| import usePopToTop from '@utils/navigation/usePopToTop' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Dimensions } from 'react-native' | ||||
| import { SceneMap, TabView } from 'react-native-tab-view' | ||||
|  | ||||
| const Route = ({ route: { key: page } }: { route: any }) => { | ||||
|   const queryKey: QueryKeyTimeline = ['Timeline', { page }] | ||||
|   return ( | ||||
|     <Timeline | ||||
|       queryKey={queryKey} | ||||
|       disableRefresh={page === 'Trending'} | ||||
|       customProps={{ | ||||
|         renderItem: ({ item }: any) => <TimelineDefault item={item} queryKey={queryKey} /> | ||||
|       }} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const renderScene = SceneMap({ | ||||
|   Local: Route, | ||||
|   LocalPublic: Route, | ||||
|   Trending: Route | ||||
| }) | ||||
|  | ||||
| const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public-Root'>> = ({ | ||||
|   navigation | ||||
| }) => { | ||||
|   const { mode } = useTheme() | ||||
|   const { t } = useTranslation('screenTabs') | ||||
|  | ||||
|   const [segment, setSegment] = useState(0) | ||||
|   const [routes] = useState([ | ||||
|     { key: 'Local', title: t('tabs.public.segments.local') }, | ||||
|     { key: 'LocalPublic', title: t('tabs.public.segments.federated') }, | ||||
|     { key: 'Trending', title: t('tabs.public.segments.trending') } | ||||
|   ]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     navigation.setOptions({ | ||||
|       headerTitle: () => ( | ||||
|         <SegmentedControl | ||||
|           appearance={mode} | ||||
|           values={routes.map(({ title }) => title)} | ||||
|           selectedIndex={segment} | ||||
|           onChange={({ nativeEvent }) => setSegment(nativeEvent.selectedSegmentIndex)} | ||||
|           style={{ flexBasis: '65%' }} | ||||
|         /> | ||||
|       ), | ||||
|       headerRight: () => ( | ||||
|         <HeaderRight | ||||
|           accessibilityLabel={t('common.search.accessibilityLabel')} | ||||
|           accessibilityHint={t('common.search.accessibilityHint')} | ||||
|           content='Search' | ||||
|           onPress={() => navigation.navigate('Tab-Shared-Search')} | ||||
|         /> | ||||
|       ) | ||||
|     }) | ||||
|   }, [mode, segment]) | ||||
|  | ||||
|   usePopToTop() | ||||
|  | ||||
|   return ( | ||||
|     <TabView | ||||
|       lazy | ||||
|       swipeEnabled | ||||
|       renderScene={renderScene} | ||||
|       renderTabBar={() => null} | ||||
|       onIndexChange={index => setSegment(index)} | ||||
|       navigationState={{ index: segment, routes }} | ||||
|       initialLayout={{ width: Dimensions.get('screen').width }} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Root | ||||
							
								
								
									
										18
									
								
								src/screens/Tabs/Public/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/screens/Tabs/Public/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { TabPublicStackParamList } from '@utils/navigation/navigators' | ||||
| import React from 'react' | ||||
| import TabShared from '../Shared' | ||||
| import Root from './Root' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabPublicStackParamList>() | ||||
|  | ||||
| const TabPublic: React.FC = () => { | ||||
|   return ( | ||||
|     <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|       <Stack.Screen name='Tab-Public-Root' component={Root} /> | ||||
|       {TabShared({ Stack })} | ||||
|     </Stack.Navigator> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabPublic | ||||
| @@ -23,8 +23,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = | ||||
|   const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({ | ||||
|     ...queryKey[1], | ||||
|     options: { | ||||
|       getPreviousPageParam: firstPage => | ||||
|         firstPage.links?.prev && { since_id: firstPage.links.next }, | ||||
|       getPreviousPageParam: firstPage => firstPage.links?.prev && { min_id: firstPage.links.next }, | ||||
|       getNextPageParam: lastPage => lastPage.links?.next && { max_id: lastPage.links.next } | ||||
|     } | ||||
|   }) | ||||
|   | ||||
| @@ -20,9 +20,11 @@ const queryFunction = async () => { | ||||
|   return res.body | ||||
| } | ||||
|  | ||||
| const useListsQuery = ({ options }: { options?: UseQueryOptions<Mastodon.List[], AxiosError> }) => { | ||||
| const useListsQuery = ( | ||||
|   params: { options?: UseQueryOptions<Mastodon.List[], AxiosError> } | void | ||||
| ) => { | ||||
|   const queryKey: QueryKeyLists = ['Lists'] | ||||
|   return useQuery(queryKey, queryFunction, options) | ||||
|   return useQuery(queryKey, queryFunction, params?.options) | ||||
| } | ||||
|  | ||||
| type MutationVarsLists = | ||||
|   | ||||
| @@ -40,6 +40,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query | ||||
|       }) | ||||
|  | ||||
|     case 'Local': | ||||
|       console.log('local', params) | ||||
|       return apiInstance<Mastodon.Status[]>({ | ||||
|         method: 'get', | ||||
|         url: 'timelines/public', | ||||
| @@ -56,6 +57,14 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query | ||||
|         params | ||||
|       }) | ||||
|  | ||||
|     case 'Trending': | ||||
|       console.log('trending', params) | ||||
|       return apiInstance<Mastodon.Status[]>({ | ||||
|         method: 'get', | ||||
|         url: 'trends/statuses', | ||||
|         params | ||||
|       }) | ||||
|  | ||||
|     case 'Notifications': | ||||
|       const rootStore = store.getState() | ||||
|       const notificationsFilter = getInstanceNotificationsFilter(rootStore) | ||||
| @@ -206,53 +215,6 @@ const useTimelineQuery = ({ | ||||
|   }) | ||||
| } | ||||
|  | ||||
| const prefetchTimelineQuery = async ({ | ||||
|   ids, | ||||
|   queryKey | ||||
| }: { | ||||
|   ids: Mastodon.Status['id'][] | ||||
|   queryKey: QueryKeyTimeline | ||||
| }): Promise<Mastodon.Status['id'] | undefined> => { | ||||
|   let page: string = '' | ||||
|   let local: boolean = false | ||||
|   switch (queryKey[1].page) { | ||||
|     case 'Following': | ||||
|       page = 'home' | ||||
|       break | ||||
|     case 'Local': | ||||
|       page = 'public' | ||||
|       local = true | ||||
|       break | ||||
|     case 'LocalPublic': | ||||
|       page = 'public' | ||||
|       break | ||||
|   } | ||||
|  | ||||
|   for (const id of ids) { | ||||
|     const statuses = await apiInstance<Mastodon.Status[]>({ | ||||
|       method: 'get', | ||||
|       url: `timelines/${page}`, | ||||
|       params: { | ||||
|         min_id: id, | ||||
|         limit: 1, | ||||
|         ...(local && { local: 'true' }) | ||||
|       } | ||||
|     }) | ||||
|     if (statuses.body.length) { | ||||
|       await queryClient.prefetchInfiniteQuery(queryKey, props => | ||||
|         queryFunction({ | ||||
|           ...props, | ||||
|           queryKey, | ||||
|           pageParam: { | ||||
|             max_id: statuses.body[0].id | ||||
|           } | ||||
|         }) | ||||
|       ) | ||||
|       return id | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // --- Separator --- | ||||
|  | ||||
| enum MapPropertyToUrl { | ||||
| @@ -460,4 +422,4 @@ const useTimelineMutation = ({ | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export { prefetchTimelineQuery, useTimelineQuery, useTimelineMutation } | ||||
| export { useTimelineQuery, useTimelineMutation } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { InstanceResponse } from '@api/instance' | ||||
|  | ||||
| export const infinitePageParams = { | ||||
|   getPreviousPageParam: (firstPage: InstanceResponse<any>) => | ||||
|     firstPage.links?.prev && { since_id: firstPage.links.next }, | ||||
|     firstPage.links?.prev && { min_id: firstPage.links.next }, | ||||
|   getNextPageParam: (lastPage: InstanceResponse<any>) => | ||||
|     lastPage.links?.next && { max_id: lastPage.links.next } | ||||
| } | ||||
|   | ||||
| @@ -8144,11 +8144,6 @@ leven@^3.1.0: | ||||
|   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" | ||||
|   integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== | ||||
|  | ||||
| li@^1.3.0: | ||||
|   version "1.3.0" | ||||
|   resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" | ||||
|   integrity sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw== | ||||
|  | ||||
| lie@3.1.1: | ||||
|   version "3.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user