Added trending in the "public" tab
This commit is contained in:
xmflsct 2022-12-11 01:08:38 +01:00
parent 1ece7b3fe3
commit 44379504eb
23 changed files with 508 additions and 598 deletions

View File

@ -61,7 +61,6 @@
"expo-video-thumbnails": "^7.0.0", "expo-video-thumbnails": "^7.0.0",
"expo-web-browser": "~12.0.0", "expo-web-browser": "~12.0.0",
"i18next": "^22.0.6", "i18next": "^22.0.6",
"li": "^1.3.0",
"linkify-it": "^4.0.1", "linkify-it": "^4.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",

1
src/@types/app.d.ts vendored
View File

@ -3,6 +3,7 @@ declare namespace App {
| 'Following' | 'Following'
| 'Local' | 'Local'
| 'LocalPublic' | 'LocalPublic'
| 'Trending'
| 'Notifications' | 'Notifications'
| 'Hashtag' | 'Hashtag'
| 'List' | 'List'

View File

@ -1,6 +1,5 @@
declare module 'gl-react-blurhash' declare module 'gl-react-blurhash'
declare module 'htmlparser2-without-node-native' declare module 'htmlparser2-without-node-native'
declare module 'li'
declare module 'react-native-feather' declare module 'react-native-feather'
declare module 'react-native-htmlview' declare module 'react-native-htmlview'
declare module 'react-native-toast-message' declare module 'react-native-toast-message'

View File

@ -1,6 +1,5 @@
import { RootState } from '@root/store' import { RootState } from '@root/store'
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import li from 'li'
import { ctx, handleError, userAgent } from './helpers' import { ctx, handleError, userAgent } from './helpers'
export type Params = { export type Params = {
@ -15,9 +14,10 @@ export type Params = {
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'> extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
} }
type LinkFormat = { id: string; isOffset: boolean }
export type InstanceResponse<T = unknown> = { export type InstanceResponse<T = unknown> = {
body: T body: T
links: { prev?: string; next?: string } links: { prev?: LinkFormat; next?: LinkFormat }
} }
const apiInstance = async <T = unknown>({ const apiInstance = async <T = unknown>({
@ -74,17 +74,27 @@ const apiInstance = async <T = unknown>({
...extras ...extras
}) })
.then(response => { .then(response => {
let prev let links: {
let next prev?: { id: string; isOffset: boolean }
next?: { id: string; isOffset: boolean }
} = {}
if (response.headers?.link) { if (response.headers?.link) {
const headersLinks = li.parse(response.headers?.link) const linksParsed = response.headers.link.matchAll(
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1] new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1] )
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({ return Promise.resolve({ body: response.data, links })
body: response.data,
links: { prev, next }
})
}) })
.catch(handleError()) .catch(handleError())
} }

View File

@ -6,17 +6,11 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef } from 'react' import React, { RefObject, useCallback, useRef } from 'react'
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
import Animated, { import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty' import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer' import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, { import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
@ -26,8 +20,7 @@ export interface Props {
disableRefresh?: boolean disableRefresh?: boolean
disableInfinity?: boolean disableInfinity?: boolean
lookback?: Extract<App.Pages, 'Following' | 'Local' | 'LocalPublic'> lookback?: Extract<App.Pages, 'Following' | 'Local' | 'LocalPublic'>
customProps: Partial<FlatListProps<any>> & customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'>
Pick<FlatListProps<any>, 'renderItem'>
} }
const Timeline: React.FC<Props> = ({ const Timeline: React.FC<Props> = ({
@ -39,30 +32,24 @@ const Timeline: React.FC<Props> = ({
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const { const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } =
data, useTimelineQuery({
refetch, ...queryKey[1],
isFetching, options: {
isLoading, notifyOnChangeProps: Platform.select({
fetchNextPage, ios: ['dataUpdatedAt', 'isFetching'],
isFetchingNextPage android: ['dataUpdatedAt', 'isFetching', 'isLoading']
} = useTimelineQuery({ }),
...queryKey[1], getNextPageParam: lastPage =>
options: { lastPage?.links?.next && {
notifyOnChangeProps: Platform.select({ ...(lastPage.links.next.isOffset
ios: ['dataUpdatedAt', 'isFetching'], ? { offset: lastPage.links.next.id }
android: ['dataUpdatedAt', 'isFetching', 'isLoading'] : { max_id: lastPage.links.next.id })
}), }
getNextPageParam: lastPage => }
lastPage?.links?.next && { })
max_id: lastPage.links.next
}
}
})
const flattenData = data?.pages const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
? data.pages?.flatMap(page => [...page.body])
: []
const onEndReached = useCallback( const onEndReached = useCallback(
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(), () => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
@ -134,10 +121,7 @@ const Timeline: React.FC<Props> = ({
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.75} onEndReachedThreshold={0.75}
ListFooterComponent={ ListFooterComponent={
<TimelineFooter <TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
} }
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />} ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={({ leadingItem }) => ItemSeparatorComponent={({ leadingItem }) =>
@ -145,9 +129,7 @@ const Timeline: React.FC<Props> = ({
<ComponentSeparator extraMarginLeft={0} /> <ComponentSeparator extraMarginLeft={0} />
) : ( ) : (
<ComponentSeparator <ComponentSeparator
extraMarginLeft={ extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
StyleConstants.Avatar.M + StyleConstants.Spacing.S
}
/> />
) )
} }

View File

@ -21,7 +21,11 @@ const TimelineFooter = React.memo(
enabled: !disableInfinity, enabled: !disableInfinity,
notifyOnChangeProps: ['hasNextPage'], notifyOnChangeProps: ['hasNextPage'],
getNextPageParam: lastPage => 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 <Trans
i18nKey='componentTimeline:end.message' i18nKey='componentTimeline:end.message'
components={[ components={[
<Icon <Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
name='Coffee'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
/>
]} ]}
/> />
</CustomText> </CustomText>

View File

@ -1,10 +1,6 @@
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline'
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef, useState } from 'react' import React, { RefObject, useCallback, useRef, useState } from 'react'
@ -31,14 +27,8 @@ export interface Props {
} }
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
export const SEPARATION_Y_1 = -( export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2)
CONTAINER_HEIGHT / 2 + export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 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> = ({ const TimelineRefresh: React.FC<Props> = ({
flRef, flRef,
@ -57,87 +47,77 @@ const TimelineRefresh: React.FC<Props> = ({
const fetchingLatestIndex = useRef(0) const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false) const refetchActive = useRef(false)
const { const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } =
refetch, useTimelineQuery({
isFetching, ...queryKey[1],
isLoading, options: {
fetchPreviousPage, getPreviousPageParam: firstPage =>
hasPreviousPage, firstPage?.links?.prev && {
isFetchingNextPage ...(firstPage.links.prev.isOffset
} = useTimelineQuery({ ? { offset: firstPage.links.prev.id }
...queryKey[1], : { max_id: firstPage.links.prev.id }),
options: { // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
getPreviousPageParam: firstPage => limit: '3'
firstPage?.links?.prev && { },
min_id: firstPage.links.prev, select: data => {
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 if (refetchActive.current) {
limit: '3' data.pageParams = [data.pageParams[0]]
data.pages = [data.pages[0]]
refetchActive.current = false
}
return data
}, },
select: data => { onSuccess: () => {
if (refetchActive.current) { if (fetchingLatestIndex.current > 0) {
data.pageParams = [data.pageParams[0]] if (fetchingLatestIndex.current > 5) {
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 {
clearFirstPage() clearFirstPage()
fetchingLatestIndex.current = 0 fetchingLatestIndex.current = 0
} else {
if (hasPreviousPage) {
fetchPreviousPage()
fetchingLatestIndex.current++
} else {
clearFirstPage()
fetchingLatestIndex.current = 0
}
} }
} }
} }
} }
} })
})
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const clearFirstPage = () => { const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
queryKey, if (data?.pages[0] && data.pages[0].body.length === 0) {
data => { return {
if (data?.pages[0] && data.pages[0].body.length === 0) { pages: data.pages.slice(1),
return { pageParams: data.pageParams.slice(1)
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
} }
} else {
return data
} }
) })
} }
const prepareRefetch = () => { const prepareRefetch = () => {
refetchActive.current = true refetchActive.current = true
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
queryKey, if (data) {
data => { data.pageParams = [undefined]
if (data) { const newFirstPage: TimelineData = { body: [] }
data.pageParams = [undefined] for (let page of data.pages) {
const newFirstPage: TimelineData = { body: [] } // @ts-ignore
for (let page of data.pages) { newFirstPage.body.push(...page.body)
// @ts-ignore if (newFirstPage.body.length > 10) break
newFirstPage.body.push(...page.body)
if (newFirstPage.body.length > 10) break
}
data.pages = [newFirstPage]
} }
data.pages = [newFirstPage]
return data
} }
)
return data
})
} }
const callRefetch = async () => { const callRefetch = async () => {
await refetch() await refetch()
@ -161,10 +141,7 @@ const TimelineRefresh: React.FC<Props> = ({
] ]
})) }))
const arrowTop = useAnimatedStyle(() => ({ const arrowTop = useAnimatedStyle(() => ({
marginTop: marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0)
scrollY.value < SEPARATION_Y_2
? withTiming(CONTAINER_HEIGHT)
: withTiming(0)
})) }))
const arrowStage = useSharedValue(0) const arrowStage = useSharedValue(0)
@ -241,8 +218,7 @@ const TimelineRefresh: React.FC<Props> = ({
const headerPadding = useAnimatedStyle( const headerPadding = useAnimatedStyle(
() => ({ () => ({
paddingTop: paddingTop:
fetchingLatestIndex.current !== 0 || fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage)
(isFetching && !isLoading && !isFetchingNextPage)
? withTiming(StyleConstants.Spacing.M * 2.5) ? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0) : withTiming(0)
}), }),
@ -254,10 +230,7 @@ const TimelineRefresh: React.FC<Props> = ({
<View style={styles.base}> <View style={styles.base}>
{isFetching ? ( {isFetching ? (
<View style={styles.container2}> <View style={styles.container2}>
<Circle <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View> </View>
) : ( ) : (
<> <>

View File

@ -6,8 +6,9 @@
"public": { "public": {
"name": "", "name": "",
"segments": { "segments": {
"left": "Federated", "federated": "Federated",
"right": "Local" "local": "Local",
"trending": "Trending"
} }
}, },
"notifications": { "notifications": {

View File

@ -3,16 +3,10 @@ import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import { import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
RootStackScreenProps,
ScreenTabsStackParamList
} from '@utils/navigation/navigators'
import { getPreviousTab } from '@utils/slices/contextsSlice'
import {
getInstanceAccount,
getInstanceActive
} from '@utils/slices/instancesSlice'
import { getVersionUpdate, retrieveVersionLatest } from '@utils/slices/appSlice' 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 { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo } from 'react' import React, { useCallback, useEffect, useMemo } from 'react'
import { Platform } from 'react-native' import { Platform } from 'react-native'
@ -125,11 +119,7 @@ const ScreenTabs = React.memo(
> >
<Tab.Screen name='Tab-Local' component={TabLocal} /> <Tab.Screen name='Tab-Local' component={TabLocal} />
<Tab.Screen name='Tab-Public' component={TabPublic} /> <Tab.Screen name='Tab-Public' component={TabPublic} />
<Tab.Screen <Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} />
name='Tab-Compose'
component={composeComponent}
listeners={composeListeners}
/>
<Tab.Screen name='Tab-Notifications' component={TabNotifications} /> <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
<Tab.Screen <Tab.Screen
name='Tab-Me' name='Tab-Me'

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -27,7 +27,9 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
options: { options: {
getNextPageParam: lastPage => getNextPageParam: lastPage =>
lastPage?.links?.next && { lastPage?.links?.next && {
max_id: lastPage.links.next ...(lastPage.links.next.isOffset
? { offset: lastPage.links.next.id }
: { max_id: lastPage.links.next.id })
} }
} }
}) })

View 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

View File

@ -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

View 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

View 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

View File

@ -23,8 +23,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({ const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({
...queryKey[1], ...queryKey[1],
options: { options: {
getPreviousPageParam: firstPage => getPreviousPageParam: firstPage => firstPage.links?.prev && { min_id: firstPage.links.next },
firstPage.links?.prev && { since_id: firstPage.links.next },
getNextPageParam: lastPage => lastPage.links?.next && { max_id: lastPage.links.next } getNextPageParam: lastPage => lastPage.links?.next && { max_id: lastPage.links.next }
} }
}) })

View File

@ -20,9 +20,11 @@ const queryFunction = async () => {
return res.body return res.body
} }
const useListsQuery = ({ options }: { options?: UseQueryOptions<Mastodon.List[], AxiosError> }) => { const useListsQuery = (
params: { options?: UseQueryOptions<Mastodon.List[], AxiosError> } | void
) => {
const queryKey: QueryKeyLists = ['Lists'] const queryKey: QueryKeyLists = ['Lists']
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, params?.options)
} }
type MutationVarsLists = type MutationVarsLists =

View File

@ -40,6 +40,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
}) })
case 'Local': case 'Local':
console.log('local', params)
return apiInstance<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
url: 'timelines/public', url: 'timelines/public',
@ -56,6 +57,14 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
params params
}) })
case 'Trending':
console.log('trending', params)
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: 'trends/statuses',
params
})
case 'Notifications': case 'Notifications':
const rootStore = store.getState() const rootStore = store.getState()
const notificationsFilter = getInstanceNotificationsFilter(rootStore) 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 --- // --- Separator ---
enum MapPropertyToUrl { enum MapPropertyToUrl {
@ -460,4 +422,4 @@ const useTimelineMutation = ({
}) })
} }
export { prefetchTimelineQuery, useTimelineQuery, useTimelineMutation } export { useTimelineQuery, useTimelineMutation }

View File

@ -2,7 +2,7 @@ import { InstanceResponse } from '@api/instance'
export const infinitePageParams = { export const infinitePageParams = {
getPreviousPageParam: (firstPage: InstanceResponse<any>) => getPreviousPageParam: (firstPage: InstanceResponse<any>) =>
firstPage.links?.prev && { since_id: firstPage.links.next }, firstPage.links?.prev && { min_id: firstPage.links.next },
getNextPageParam: (lastPage: InstanceResponse<any>) => getNextPageParam: (lastPage: InstanceResponse<any>) =>
lastPage.links?.next && { max_id: lastPage.links.next } lastPage.links?.next && { max_id: lastPage.links.next }
} }

View File

@ -8144,11 +8144,6 @@ leven@^3.1.0:
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== 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: lie@3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"