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-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
View File

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

View File

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

View File

@ -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())
}

View File

@ -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}
/>
)
}

View File

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

View File

@ -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>
) : (
<>

View File

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

View File

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

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: {
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 })
}
}
})

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({
...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 }
}
})

View File

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

View File

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

View File

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

View File

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