diff --git a/package.json b/package.json index ec53b8dc..53bef058 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index 0db454c2..d3d4f400 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -3,6 +3,7 @@ declare namespace App { | 'Following' | 'Local' | 'LocalPublic' + | 'Trending' | 'Notifications' | 'Hashtag' | 'List' diff --git a/src/@types/untyped.d.ts b/src/@types/untyped.d.ts index e3b7bc1a..25b5f1b1 100644 --- a/src/@types/untyped.d.ts +++ b/src/@types/untyped.d.ts @@ -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' diff --git a/src/api/instance.ts b/src/api/instance.ts index a178a42c..ca094b4d 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -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 } +type LinkFormat = { id: string; isOffset: boolean } export type InstanceResponse = { body: T - links: { prev?: string; next?: string } + links: { prev?: LinkFormat; next?: LinkFormat } } const apiInstance = async ({ @@ -74,17 +74,27 @@ const apiInstance = async ({ ...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()) } diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 2f73d636..1b98a665 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -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 - customProps: Partial> & - Pick, 'renderItem'> + customProps: Partial> & Pick, 'renderItem'> } const Timeline: React.FC = ({ @@ -39,30 +32,24 @@ const Timeline: React.FC = ({ }) => { 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 = ({ onEndReached={onEndReached} onEndReachedThreshold={0.75} ListFooterComponent={ - + } ListEmptyComponent={} ItemSeparatorComponent={({ leadingItem }) => @@ -145,9 +129,7 @@ const Timeline: React.FC = ({ ) : ( ) } diff --git a/src/components/Timeline/Footer.tsx b/src/components/Timeline/Footer.tsx index ab4e1934..4ac3f2af 100644 --- a/src/components/Timeline/Footer.tsx +++ b/src/components/Timeline/Footer.tsx @@ -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( + ]} /> diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx index 8ddfd52f..9ac45e95 100644 --- a/src/components/Timeline/Refresh.tsx +++ b/src/components/Timeline/Refresh.tsx @@ -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 = ({ flRef, @@ -57,87 +47,77 @@ const TimelineRefresh: React.FC = ({ 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 | 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 | 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 | 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 | 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 = ({ ] })) 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 = ({ 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 = ({ {isFetching ? ( - + ) : ( <> diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index 409ac790..fe32807e 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -6,8 +6,9 @@ "public": { "name": "", "segments": { - "left": "Federated", - "right": "Local" + "federated": "Federated", + "local": "Local", + "trending": "Trending" } }, "notifications": { diff --git a/src/screens/Tabs.tsx b/src/screens/Tabs.tsx index d8f50cc9..f0b2ce9f 100644 --- a/src/screens/Tabs.tsx +++ b/src/screens/Tabs.tsx @@ -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( > - + () - -const TabLocal = React.memo( - ({ navigation }: ScreenTabsScreenProps<'Tab-Local'>) => { - const { t } = useTranslation('screenTabs') - - const { data: lists } = useListsQuery({}) - - const [queryKey, setQueryKey] = useState(['Timeline', { page: 'Following' }]) - - usePopToTop() - - return ( - - ( - - - 0} - content={ - queryKey[1].page === 'List' && queryKey[1].list?.length - ? lists?.find(list => list.id === queryKey[1].list)?.title - : t('tabs.local.name') - } - /> - - - - {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 => ( - - - - - )) - : undefined} - - - ), - headerRight: () => ( - navigation.navigate('Tab-Local', { screen: 'Tab-Shared-Search' })} - /> - ) - }} - children={() => ( - - }} - /> - )} - /> - {TabShared({ Stack })} - - ) - }, - () => true -) - -export default TabLocal diff --git a/src/screens/Tabs/Local/Root.tsx b/src/screens/Tabs/Local/Root.tsx new file mode 100644 index 00000000..f3da4ff4 --- /dev/null +++ b/src/screens/Tabs/Local/Root.tsx @@ -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> = ({ + navigation +}) => { + const { t } = useTranslation('screenTabs') + + const { data: lists } = useListsQuery() + + const [queryKey, setQueryKey] = useState(['Timeline', { page: 'Following' }]) + + useEffect(() => { + navigation.setOptions({ + headerTitle: () => ( + + + 0} + content={ + queryKey[1].page === 'List' && queryKey[1].list?.length + ? lists?.find(list => list.id === queryKey[1].list)?.title + : t('tabs.local.name') + } + /> + + + + {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 => ( + + + + + )) + : undefined} + + + ), + headerRight: () => ( + navigation.navigate('Tab-Shared-Search')} + /> + ) + }) + }, []) + + usePopToTop() + + return ( + + }} + /> + ) +} + +export default Root diff --git a/src/screens/Tabs/Local/index.tsx b/src/screens/Tabs/Local/index.tsx new file mode 100644 index 00000000..ea78dcbe --- /dev/null +++ b/src/screens/Tabs/Local/index.tsx @@ -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() + +const TabLocal: React.FC = () => { + return ( + + + {TabShared({ Stack })} + + ) +} + +export default TabLocal diff --git a/src/screens/Tabs/Me.tsx b/src/screens/Tabs/Me.tsx deleted file mode 100644 index 870bd7ca..00000000 --- a/src/screens/Tabs/Me.tsx +++ /dev/null @@ -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() - -const TabMe = React.memo( - () => { - const { t } = useTranslation('screenTabs') - - return ( - - - ({ - title: t('me.stacks.bookmarks.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.conversations.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.favourites.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.followedTags.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.list.name', { list: route.params.title }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.listAccounts.name', { list: params.title }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - - ({ - title: t('me.stacks.lists.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - - ({ - title: t('me.stacks.push.name'), - headerLeft: () => navigation.goBack()} /> - })} - /> - ({ - title: t('me.stacks.settings.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.fontSize.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - title: t('me.stacks.language.name'), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ - presentation: 'modal', - headerShown: true, - title: t('me.stacks.switch.name'), - headerLeft: () => ( - navigation.goBack()} /> - ) - })} - /> - - {TabShared({ Stack })} - - ) - }, - () => true -) - -export default TabMe diff --git a/src/screens/Tabs/Me/List/Accounts.tsx b/src/screens/Tabs/Me/List/Accounts.tsx index e3b35678..89465612 100644 --- a/src/screens/Tabs/Me/List/Accounts.tsx +++ b/src/screens/Tabs/Me/List/Accounts.tsx @@ -27,7 +27,9 @@ const TabMeListAccounts: React.FC> 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 }) } } }) diff --git a/src/screens/Tabs/Me/index.tsx b/src/screens/Tabs/Me/index.tsx new file mode 100644 index 00000000..b42e9ee7 --- /dev/null +++ b/src/screens/Tabs/Me/index.tsx @@ -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() + +const TabMe: React.FC = () => { + const { t } = useTranslation('screenTabs') + + return ( + + + ({ + title: t('me.stacks.bookmarks.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.conversations.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.favourites.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.followedTags.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.list.name', { list: route.params.title }), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.listAccounts.name', { list: params.title }), + headerLeft: () => navigation.pop(1)} /> + })} + /> + + ({ + title: t('me.stacks.lists.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + + ({ + title: t('me.stacks.push.name'), + headerLeft: () => navigation.goBack()} /> + })} + /> + ({ + title: t('me.stacks.settings.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.fontSize.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + title: t('me.stacks.language.name'), + headerLeft: () => navigation.pop(1)} /> + })} + /> + ({ + presentation: 'modal', + headerShown: true, + title: t('me.stacks.switch.name'), + headerLeft: () => navigation.goBack()} /> + })} + /> + + {TabShared({ Stack })} + + ) +} + +export default TabMe diff --git a/src/screens/Tabs/Public.tsx b/src/screens/Tabs/Public.tsx deleted file mode 100644 index 8865623e..00000000 --- a/src/screens/Tabs/Public.tsx +++ /dev/null @@ -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() - -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 - }[] = [ - { - title: t('tabs.public.segments.left'), - key: 'LocalPublic' - }, - { - title: t('tabs.public.segments.right'), - key: 'Local' - } - ] - const screenOptionsRoot = useMemo( - () => ({ - headerTitle: () => ( - p.title)} - selectedIndex={segment} - onChange={({ nativeEvent }) => setSegment(nativeEvent.selectedSegmentIndex)} - style={{ flexBasis: '65%' }} - /> - ), - headerRight: () => ( - 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 - } - }) => { - const queryKey: QueryKeyTimeline = ['Timeline', { page }] - return ( - - }} - /> - ) - }, - [] - ) - const children = useCallback( - () => ( - null} - onIndexChange={index => setSegment(index)} - navigationState={{ index: segment, routes }} - initialLayout={{ width: Dimensions.get('screen').width }} - /> - ), - [segment] - ) - - usePopToTop() - - return ( - - - {TabShared({ Stack })} - - ) - }, - () => true -) - -export default TabPublic diff --git a/src/screens/Tabs/Public/Root.tsx b/src/screens/Tabs/Public/Root.tsx new file mode 100644 index 00000000..4451ea11 --- /dev/null +++ b/src/screens/Tabs/Public/Root.tsx @@ -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 ( + + }} + /> + ) +} + +const renderScene = SceneMap({ + Local: Route, + LocalPublic: Route, + Trending: Route +}) + +const Root: React.FC> = ({ + 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: () => ( + title)} + selectedIndex={segment} + onChange={({ nativeEvent }) => setSegment(nativeEvent.selectedSegmentIndex)} + style={{ flexBasis: '65%' }} + /> + ), + headerRight: () => ( + navigation.navigate('Tab-Shared-Search')} + /> + ) + }) + }, [mode, segment]) + + usePopToTop() + + return ( + null} + onIndexChange={index => setSegment(index)} + navigationState={{ index: segment, routes }} + initialLayout={{ width: Dimensions.get('screen').width }} + /> + ) +} + +export default Root diff --git a/src/screens/Tabs/Public/index.tsx b/src/screens/Tabs/Public/index.tsx new file mode 100644 index 00000000..f857db98 --- /dev/null +++ b/src/screens/Tabs/Public/index.tsx @@ -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() + +const TabPublic: React.FC = () => { + return ( + + + {TabShared({ Stack })} + + ) +} + +export default TabPublic diff --git a/src/screens/Tabs/Shared/Users.tsx b/src/screens/Tabs/Shared/Users.tsx index d1a6fdf7..53aa4b25 100644 --- a/src/screens/Tabs/Shared/Users.tsx +++ b/src/screens/Tabs/Shared/Users.tsx @@ -23,8 +23,7 @@ const TabSharedUsers: React.FC> = 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 } } }) diff --git a/src/utils/queryHooks/lists.ts b/src/utils/queryHooks/lists.ts index e23c38f1..0f7f0b81 100644 --- a/src/utils/queryHooks/lists.ts +++ b/src/utils/queryHooks/lists.ts @@ -20,9 +20,11 @@ const queryFunction = async () => { return res.body } -const useListsQuery = ({ options }: { options?: UseQueryOptions }) => { +const useListsQuery = ( + params: { options?: UseQueryOptions } | void +) => { const queryKey: QueryKeyLists = ['Lists'] - return useQuery(queryKey, queryFunction, options) + return useQuery(queryKey, queryFunction, params?.options) } type MutationVarsLists = diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index 3c285b50..17f811df 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -40,6 +40,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext({ method: 'get', url: 'timelines/public', @@ -56,6 +57,14 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext({ + 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 => { - 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({ - 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 } diff --git a/src/utils/queryHooks/utils.ts b/src/utils/queryHooks/utils.ts index 25e12ae1..5b57931b 100644 --- a/src/utils/queryHooks/utils.ts +++ b/src/utils/queryHooks/utils.ts @@ -2,7 +2,7 @@ import { InstanceResponse } from '@api/instance' export const infinitePageParams = { getPreviousPageParam: (firstPage: InstanceResponse) => - firstPage.links?.prev && { since_id: firstPage.links.next }, + firstPage.links?.prev && { min_id: firstPage.links.next }, getNextPageParam: (lastPage: InstanceResponse) => lastPage.links?.next && { max_id: lastPage.links.next } } diff --git a/yarn.lock b/yarn.lock index 325dcbf9..e40a5cab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"