1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into candidate

This commit is contained in:
xmflsct
2022-12-11 02:12:40 +01:00
106 changed files with 1622 additions and 1806 deletions

View File

@ -1,5 +1,6 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Automatic setting detected language when tooting - Automatic setting detected language when tooting
- Remember public timeline type selection
- Added notification for admins - Added notification for admins
- Fix whole word filter matching - Fix whole word filter matching
- Fix tablet cannot delete toot drafts - Fix tablet cannot delete toot drafts

View File

@ -1,5 +1,6 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 自动识别发嘟语言 - 自动识别发嘟语言
- 记住上次公共时间轴选项
- 新增管理员推送通知 - 新增管理员推送通知
- 修复过滤整词功能 - 修复过滤整词功能
- 修复平板不能删除草稿 - 修复平板不能删除草稿

View File

@ -41,6 +41,7 @@
"@reduxjs/toolkit": "^1.9.1", "@reduxjs/toolkit": "^1.9.1",
"@sentry/react-native": "4.10.1", "@sentry/react-native": "4.10.1",
"@sharcoux/slider": "^6.1.1", "@sharcoux/slider": "^6.1.1",
"@tanstack/react-query": "^4.19.1",
"axios": "^0.27.2", "axios": "^0.27.2",
"expo": "^47.0.8", "expo": "^47.0.8",
"expo-auth-session": "^3.7.3", "expo-auth-session": "^3.7.3",
@ -60,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",
@ -89,7 +89,6 @@
"react-native-svg": "^13.6.0", "react-native-svg": "^13.6.0",
"react-native-swipe-list-view": "^3.2.9", "react-native-swipe-list-view": "^3.2.9",
"react-native-tab-view": "^3.3.2", "react-native-tab-view": "^3.3.2",
"react-query": "^3.39.2",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3", "rn-placeholder": "^3.0.3",

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

@ -23,7 +23,7 @@ import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { QueryClientProvider } from 'react-query' import { QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'

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

@ -3,8 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
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, { useCallback, useState } from 'react' import React, { PropsWithChildren, useCallback, useState } from 'react'
import { Dimensions, Pressable } from 'react-native' import { Dimensions, Pressable, View } from 'react-native'
import Sparkline from './Sparkline' import Sparkline from './Sparkline'
import CustomText from './Text' import CustomText from './Text'
@ -13,7 +13,11 @@ export interface Props {
onPress?: () => void onPress?: () => void
} }
const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress }) => { const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
hashtag,
onPress: customOnPress,
children
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -31,15 +35,11 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
padding padding
}} }}
onPress={customOnPress || onPress} onPress={customOnPress || onPress}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height - padding * 2 - 1)}
> >
<CustomText <CustomText
fontStyle='M' fontStyle='M'
@ -52,11 +52,22 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
> >
#{hashtag.name} #{hashtag.name}
</CustomText> </CustomText>
<Sparkline <View
data={hashtag.history.map(h => parseInt(h.uses)).reverse()} style={{ flexDirection: 'row', alignItems: 'center' }}
width={width} onLayout={({
height={height} nativeEvent: {
/> layout: { height }
}
}) => setHeight(height)}
>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
margin={children ? StyleConstants.Spacing.S : undefined}
/>
{children}
</View>
</Pressable> </Pressable>
) )
} }

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Text, View } from 'react-native' import { View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'

View File

@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']

View File

@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']

View File

@ -1,7 +1,6 @@
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy, minBy } from 'lodash' import { maxBy, minBy } from 'lodash'
import React from 'react' import React from 'react'
import { Platform } from 'react-native'
import Svg, { G, Path } from 'react-native-svg' import Svg, { G, Path } from 'react-native-svg'
export interface Props { export interface Props {
@ -69,7 +68,7 @@ const Sparkline: React.FC<Props> = ({ data, width, height, margin = 0 }) => {
const fillPoints = linePoints.concat(closePolyPoints) const fillPoints = linePoints.concat(closePolyPoints)
return ( return (
<Svg height={Platform.OS !== 'android' ? 'auto' : 24} width={width}> <Svg height={height} width={width} style={{ marginRight: margin }}>
<G> <G>
<Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} /> <Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} />
<Path <Path

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

@ -8,7 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import TimelineActions from './Shared/Actions' import TimelineActions from './Shared/Actions'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import StatusContext from './Shared/Context' import StatusContext from './Shared/Context'

View File

@ -125,6 +125,7 @@ const TimelineDefault: React.FC<Props> = ({
spoilerHidden, spoilerHidden,
copiableContent, copiableContent,
highlighted, highlighted,
inThread: queryKey?.[1].page === 'Toot',
disableDetails, disableDetails,
disableOnPress disableOnPress
}} }}

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

@ -108,7 +108,10 @@ const TimelineNotifications: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}} }}
> >
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} /> <TimelineContent
notificationOwnToot={['favourite', 'reblog'].includes(notification.type)}
setSpoilerExpanded={setSpoilerExpanded}
/>
<TimelinePoll /> <TimelinePoll />
<TimelineAttachment /> <TimelineAttachment />
<TimelineCard /> <TimelineCard />

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'
@ -20,7 +16,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming withTiming
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { InfiniteData, useQueryClient } from 'react-query' import { InfiniteData, useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
flRef: RefObject<FlatList<any>> flRef: RefObject<FlatList<any>>
@ -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

@ -17,7 +17,7 @@ import { uniqBy } from 'lodash'
import React, { useCallback, useContext, useMemo } from 'react' import React, { useCallback, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import StatusContext from './Context' import StatusContext from './Context'

View File

@ -6,11 +6,12 @@ import { useSelector } from 'react-redux'
import StatusContext from './Context' import StatusContext from './Context'
export interface Props { export interface Props {
notificationOwnToot?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>> setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
} }
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => { const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
const { status, highlighted, disableDetails } = useContext(StatusContext) const { status, highlighted, inThread, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null if (!status || typeof status.content !== 'string' || !status.content.length) return null
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
@ -38,7 +39,13 @@ const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1} numberOfLines={
instanceAccount.preferences['reading:expand:spoilers'] || inThread
? notificationOwnToot
? 2
: 999
: 1
}
expandHint={t('shared.content.expandHint')} expandHint={t('shared.content.expandHint')}
setSpoilerExpanded={setSpoilerExpanded} setSpoilerExpanded={setSpoilerExpanded}
highlighted={highlighted} highlighted={highlighted}
@ -53,7 +60,7 @@ const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={highlighted ? 999 : undefined} numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
disableDetails={disableDetails} disableDetails={disableDetails}
/> />
)} )}

View File

@ -16,6 +16,7 @@ type ContextType = {
}> }>
highlighted?: boolean highlighted?: boolean
inThread?: boolean
disableDetails?: boolean disableDetails?: boolean
disableOnPress?: boolean disableOnPress?: boolean
} }

View File

@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import StatusContext from './Context' import StatusContext from './Context'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'

View File

@ -16,7 +16,7 @@ import { maxBy } from 'lodash'
import React, { useCallback, useContext, useMemo, useState } from 'react' import React, { useCallback, useContext, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import StatusContext from './Context' import StatusContext from './Context'
const TimelinePoll: React.FC = () => { const TimelinePoll: React.FC = () => {

View File

@ -56,7 +56,7 @@ const TimelineTranslate = () => {
: settingsLanguage || Localization.locale || 'en' : settingsLanguage || Localization.locale || 'en'
const [enabled, setEnabled] = useState(false) const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage.language, source: detectedLanguage.language,
target: targetLanguage, target: targetLanguage,
text, text,
@ -111,7 +111,7 @@ const TimelineTranslate = () => {
<CustomText <CustomText
fontStyle='M' fontStyle='M'
style={{ style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue color: isFetching || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}} }}
> >
{isError {isError
@ -125,7 +125,7 @@ const TimelineTranslate = () => {
}) })
: t('shared.translate.default')} : t('shared.translate.default')}
</CustomText> </CustomText>
{isLoading ? ( {isFetching ? (
<Circle <Circle
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={colors.disabled} color={colors.disabled}

View File

@ -17,7 +17,7 @@ import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const menuAccount = ({ const menuAccount = ({

View File

@ -3,7 +3,7 @@ import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timelin
import { getInstanceUrl } from '@utils/slices/instancesSlice' import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const menuInstance = ({ const menuInstance = ({

View File

@ -12,7 +12,7 @@ import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instance
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const menuStatus = ({ const menuStatus = ({

View File

@ -1,2 +1,9 @@
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010 export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010
export const PERMISSION_MANAGE_USERS = 0x0000000000000400 export const PERMISSION_MANAGE_USERS = 0x0000000000000400
export const checkPermission = (permission: number, permissions?: string | number): boolean =>
permissions
? !!(
(typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission
)
: false

View File

@ -1,4 +1,4 @@
import { QueryClient } from 'react-query' import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } }) const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } })

View File

@ -5,6 +5,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"discard": "Discard", "discard": "Discard",
"continue": "Continue", "continue": "Continue",
"create": "Create",
"delete": "Delete", "delete": "Delete",
"done": "Done" "done": "Done"
}, },

View File

@ -2,19 +2,6 @@
"content": { "content": {
"altText": { "altText": {
"heading": "Alternative Text" "heading": "Alternative Text"
},
"notificationsFilter": {
"heading": "Show notification types",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Follow request",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Toot from subscribed users",
"update": "Reblog has been edited"
}
} }
} }
} }

View File

@ -6,8 +6,9 @@
"public": { "public": {
"name": "", "name": "",
"segments": { "segments": {
"left": "Federated", "federated": "Federated",
"right": "Local" "local": "Local",
"trending": "Trending"
} }
}, },
"notifications": { "notifications": {
@ -24,9 +25,22 @@
} }
}, },
"notifications": { "notifications": {
"filter": { "filters": {
"accessibilityLabel": "Filter", "accessibilityLabel": "Filter",
"accessibilityHint": "Filter shown notifications' types" "accessibilityHint": "Filter shown notifications' types",
"title": "Show notifications",
"options": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Follow request",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Toot from subscribed users",
"update": "Reblog has been edited",
"admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)",
"admin.report": "$t(screenTabs:me.push.admin.report.heading)"
}
} }
}, },
"me": { "me": {
@ -40,6 +54,9 @@
"favourites": { "favourites": {
"name": "Favourites" "name": "Favourites"
}, },
"followedTags": {
"name": "Followed hashtags"
},
"fontSize": { "fontSize": {
"name": "Toot Font Size" "name": "Toot Font Size"
}, },
@ -53,7 +70,7 @@
"name": "Users in list: {{list}}" "name": "Users in list: {{list}}"
}, },
"listAdd": { "listAdd": {
"name": "Add a List" "name": "Create a List"
}, },
"listEdit": { "listEdit": {
"name": "Edit List Details" "name": "Edit List Details"

View File

@ -15,7 +15,6 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import ActionsAltText from './Actions/AltText' import ActionsAltText from './Actions/AltText'
import ActionsNotificationsFilter from './Actions/NotificationsFilter'
const ScreenActions = ({ const ScreenActions = ({
route: { params }, route: { params },
@ -53,8 +52,6 @@ const ScreenActions = ({
const actions = () => { const actions = () => {
switch (params.type) { switch (params.type) {
case 'notifications_filter':
return <ActionsNotificationsFilter />
case 'alt_text': case 'alt_text':
return <ActionsAltText text={params.text} /> return <ActionsAltText text={params.text} />
} }

View File

@ -1,114 +0,0 @@
import Button from '@components/Button'
import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import { useNavigation } from '@react-navigation/native'
import {
checkInstanceFeature,
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useQueryClient } from 'react-query'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAppDispatch } from '@root/store'
const ActionsNotificationsFilter: React.FC = () => {
const navigation = useNavigation()
const { t } = useTranslation('screenActions')
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
const queryClient = useQueryClient()
const dispatch = useAppDispatch()
const instanceNotificationsFilter = useSelector(
getInstanceNotificationsFilter
)
if (!instanceNotificationsFilter) {
navigation.goBack()
return null
}
const hasTypeStatus = useSelector(
checkInstanceFeature('notification_type_status')
)
const hasTypeUpdate = useSelector(
checkInstanceFeature('notification_type_update')
)
const options = useMemo(() => {
return (
instanceNotificationsFilter &&
(
[
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
] as [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
]
)
.filter(type => {
switch (type) {
case 'status':
return hasTypeStatus
case 'update':
return hasTypeUpdate
default:
return true
}
})
.map(type => (
<MenuRow
key={type}
title={t(`content.notificationsFilter.content.${type}`)}
switchValue={instanceNotificationsFilter[type]}
switchOnValueChange={() =>
dispatch(
updateInstanceNotificationsFilter({
...instanceNotificationsFilter,
[type]: !instanceNotificationsFilter[type]
})
)
}
/>
))
)
}, [instanceNotificationsFilter, hasTypeStatus, hasTypeUpdate])
return (
<>
<MenuContainer>
<MenuHeader heading={t(`content.notificationsFilter.heading`)} />
{options}
</MenuContainer>
<Button
type='text'
content={t('common:buttons.apply')}
onPress={() => {
queryClient.resetQueries(queryKey)
}}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
/>
</>
)
}
export default ActionsNotificationsFilter

View File

@ -22,7 +22,7 @@ import { filter } from 'lodash'
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react' import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Keyboard, Platform } from 'react-native' import { Alert, Keyboard, Platform } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import ComposeDraftsList from './Compose/DraftsList' import ComposeDraftsList from './Compose/DraftsList'
import ComposeEditAttachment from './Compose/EditAttachment' import ComposeEditAttachment from './Compose/EditAttachment'
@ -407,12 +407,12 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
<Stack.Screen <Stack.Screen
name='Screen-Compose-DraftsList' name='Screen-Compose-DraftsList'
component={ComposeDraftsList} component={ComposeDraftsList}
options={{ headerShown: false, presentation: 'modal' }} options={{ presentation: 'modal' }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Compose-EditAttachment' name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment} component={ComposeEditAttachment}
options={{ headerShown: false, presentation: 'modal' }} options={{ presentation: 'modal' }}
/> />
</Stack.Navigator> </Stack.Navigator>
</ComposeContext.Provider> </ComposeContext.Provider>

View File

@ -1,49 +1,227 @@
import apiInstance from '@api/instance'
import { HeaderLeft } from '@components/Header' import { HeaderLeft } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useAppDispatch } from '@root/store'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React, { useCallback } from 'react' import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ComposeDraftsListRoot from './DraftsList/Root' import { Dimensions, Modal, Platform, Pressable, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
import ComposeContext from './utils/createContext'
import { formatText } from './utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
const Stack = createNativeStackNavigator() const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>> = ({
navigation,
const ComposeDraftsList: React.FC<
ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>
> = ({
route: { route: {
params: { timestamp } params: { timestamp }
}, }
navigation
}) => { }) => {
const { colors } = useTheme()
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
const children = useCallback( useEffect(() => {
() => <ComposeDraftsListRoot timestamp={timestamp} />, navigation.setOptions({
[] title: t('content.draftsList.header.title'),
) headerLeft: () => (
const headerLeft = useCallback( <HeaderLeft type='icon' content='ChevronDown' onPress={() => navigation.goBack()} />
() => ( )
<HeaderLeft })
type='icon' }, [])
content='ChevronDown'
onPress={() => navigation.goBack()} const { composeDispatch } = useContext(ComposeContext)
/> const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
), draft => draft.timestamp !== timestamp
[]
) )
const [checkingAttachments, setCheckingAttachments] = useState(false)
const dispatch = useAppDispatch()
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
return ( return (
<Stack.Navigator> <>
<Stack.Screen <View
name='Screen-Compose-EditAttachment-Root' style={{
children={children} flexDirection: 'row',
options={{ alignItems: 'center',
headerLeft, marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
title: t('content.draftsList.header.title'), padding: StyleConstants.Spacing.S,
headerShadowVisible: false borderColor: colors.border,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S
}} }}
>
<Icon
name='AlertTriangle'
color={colors.secondary}
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{t('content.draftsList.warning')}
</CustomText>
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}
renderItem={({ item }: { item: ComposeStateDraft }) => {
return (
<Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
style={{
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault
}}
onPress={async () => {
setCheckingAttachments(true)
let tempDraft = item
let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) {
await apiInstance<Mastodon.Attachment>({
method: 'get',
url: `media/${attachment.remote?.id}`
})
.then(res => {
if (res.body.id === attachment.remote?.id) {
tempUploads.push(attachment)
}
})
.catch(() => {})
}
tempDraft = {
...tempDraft,
attachments: { ...item.attachments, uploads: tempUploads }
}
}
tempDraft.spoiler?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.spoiler })
tempDraft.text?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.text })
composeDispatch({
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack()
}}
>
<View style={{ flex: 1 }}>
<HeaderSharedCreated created_at={item.timestamp} />
<CustomText
fontStyle='M'
numberOfLines={2}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
>
{item.text || item.spoiler || t('content.draftsList.content.textEmpty')}
</CustomText>
{item.attachments?.uploads.length ? (
<View
style={{
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.S
}}
>
{item.attachments.uploads.map((attachment, index) => (
<FastImage
key={index}
style={{
width:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
height:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
/>
))}
</View>
) : null}
</View>
</Pressable>
)
}}
renderHiddenItem={({ item }) => (
<Pressable
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
backgroundColor: colors.red
}}
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
children={
<View
style={{
flexBasis:
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
justifyContent: 'center',
alignItems: 'center'
}}
children={
<Icon
name='Trash'
size={StyleConstants.Font.Size.L}
color={colors.primaryOverlay}
/>
}
/>
}
/>
)}
disableRightSwipe={true}
rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()}
/>
</PanGestureHandler>
<Modal
transparent
animationType='fade'
visible={checkingAttachments}
children={
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.backgroundOverlayInvert
}}
children={
<CustomText
fontStyle='M'
children={t('content.draftsList.checkAttachment')}
style={{ color: colors.primaryOverlay }}
/>
}
/>
}
/> />
</Stack.Navigator> </>
) )
} }

View File

@ -1,223 +0,0 @@
import apiInstance from '@api/instance'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions, Image, Modal, Platform, Pressable, View } from 'react-native'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import { formatText } from '../utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from '../utils/types'
export interface Props {
timestamp: number
}
const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const { composeDispatch } = useContext(ComposeContext)
const { t } = useTranslation('screenCompose')
const navigation = useNavigation()
const dispatch = useAppDispatch()
const { colors, theme } = useTheme()
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp
)
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
const [checkingAttachments, setCheckingAttachments] = useState(false)
const renderItem = useCallback(
({ item }: { item: ComposeStateDraft }) => {
return (
<Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
style={{
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault
}}
onPress={async () => {
setCheckingAttachments(true)
let tempDraft = item
let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) {
await apiInstance<Mastodon.Attachment>({
method: 'get',
url: `media/${attachment.remote?.id}`
})
.then(res => {
if (res.body.id === attachment.remote?.id) {
tempUploads.push(attachment)
}
})
.catch(() => {})
}
tempDraft = {
...tempDraft,
attachments: { ...item.attachments, uploads: tempUploads }
}
}
tempDraft.spoiler?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.spoiler })
tempDraft.text?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.text })
composeDispatch({
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack()
}}
>
<View style={{ flex: 1 }}>
<HeaderSharedCreated created_at={item.timestamp} />
<CustomText
fontStyle='M'
numberOfLines={2}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
>
{item.text || item.spoiler || t('content.draftsList.content.textEmpty')}
</CustomText>
{item.attachments?.uploads.length ? (
<View
style={{
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.S
}}
>
{item.attachments.uploads.map((attachment, index) => (
<Image
key={index}
style={{
width:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
height:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
/>
))}
</View>
) : null}
</View>
</Pressable>
)
},
[theme]
)
return (
<>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S
}}
>
<Icon
name='AlertTriangle'
color={colors.secondary}
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{t('content.draftsList.warning')}
</CustomText>
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}
renderItem={renderItem}
renderHiddenItem={({ item }) => (
<Pressable
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
backgroundColor: colors.red
}}
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
children={
<View
style={{
flexBasis:
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 255, 0, 0.2)'
}}
children={
<Icon
name='Trash'
size={StyleConstants.Font.Size.L}
color={colors.primaryOverlay}
/>
}
/>
}
/>
)}
disableRightSwipe={true}
rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()}
/>
</PanGestureHandler>
<Modal
transparent
animationType='fade'
visible={checkingAttachments}
children={
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.backgroundOverlayInvert
}}
children={
<CustomText
fontStyle='M'
children={t('content.draftsList.checkAttachment')}
style={{ color: colors.primaryOverlay }}
/>
}
/>
}
/>
</>
)
}
export default ComposeDraftsListRoot

View File

@ -1,49 +1,90 @@
import { HeaderLeft } from '@components/Header' import apiInstance from '@api/instance'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React from 'react' import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native' import { Alert, KeyboardAvoidingView, Platform } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import ComposeEditAttachmentRoot from './EditAttachment/Root' import ComposeEditAttachmentRoot from './EditAttachment/Root'
import ComposeEditAttachmentSubmit from './EditAttachment/Submit' import ComposeContext from './utils/createContext'
const Stack = createNativeStackNavigator() const ComposeEditAttachment: React.FC<
ScreenComposeStackScreenProps<'Screen-Compose-EditAttachment'>
const ComposeEditAttachment: React.FC<ScreenComposeStackScreenProps< > = ({
'Screen-Compose-EditAttachment' navigation,
>> = ({
route: { route: {
params: { index } params: { index }
},
navigation
}) => {
const { t } = useTranslation('screenCompose')
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<Stack.Navigator>
<Stack.Screen
name='Screen-Compose-EditAttachment-Root'
children={() => <ComposeEditAttachmentRoot index={index} />}
options={{
headerLeft: () => <HeaderLeft
type='icon'
content='ChevronDown'
onPress={() => navigation.goBack()}
/>,
headerRight: () => <ComposeEditAttachmentSubmit index={index} />,
title: t('content.editAttachment.header.title')
}}
/>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
)
} }
}) => {
const { t } = useTranslation('screenCompose')
const { composeState } = useContext(ComposeContext)
const [isSubmitting, setIsSubmitting] = useState(false)
const theAttachment = composeState.attachments.uploads[index].remote!
useEffect(() => {
navigation.setOptions({
title: t('content.editAttachment.header.title'),
headerLeft: () => (
<HeaderLeft type='icon' content='ChevronDown' onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('content.editAttachment.header.right.accessibilityLabel')}
type='icon'
content='Save'
loading={isSubmitting}
onPress={() => {
setIsSubmitting(true)
const formData = new FormData()
if (theAttachment.description) {
formData.append('description', theAttachment.description)
}
if (theAttachment.meta?.focus?.x !== 0 || theAttachment.meta.focus.y !== 0) {
formData.append(
'focus',
`${theAttachment.meta?.focus?.x || 0},${-theAttachment.meta?.focus?.y || 0}`
)
}
theAttachment?.id &&
apiInstance<Mastodon.Attachment>({
method: 'put',
url: `media/${theAttachment.id}`,
body: formData
})
.then(() => {
haptics('Success')
navigation.goBack()
})
.catch(() => {
setIsSubmitting(false)
haptics('Error')
Alert.alert(t('content.editAttachment.header.right.failed.title'), undefined, [
{
text: t('content.editAttachment.header.right.failed.button'),
style: 'cancel'
}
])
})
}}
/>
)
})
}, [theAttachment])
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<ComposeEditAttachmentRoot index={index} />
</SafeAreaView>
</KeyboardAvoidingView>
)
}
export default ComposeEditAttachment export default ComposeEditAttachment

View File

@ -1,79 +0,0 @@
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { HeaderRight } from '@components/Header'
import { useNavigation } from '@react-navigation/native'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import ComposeContext from '../utils/createContext'
export interface Props {
index: number
}
const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
const { composeState } = useContext(ComposeContext)
const navigation = useNavigation()
const [isSubmitting, setIsSubmitting] = useState(false)
const { t } = useTranslation('screenCompose')
const theAttachment = composeState.attachments.uploads[index].remote!
return (
<HeaderRight
accessibilityLabel={t(
'content.editAttachment.header.right.accessibilityLabel'
)}
type='icon'
content='Save'
loading={isSubmitting}
onPress={() => {
setIsSubmitting(true)
const formData = new FormData()
if (theAttachment.description) {
formData.append('description', theAttachment.description)
}
if (
theAttachment.meta?.focus?.x !== 0 ||
theAttachment.meta.focus.y !== 0
) {
formData.append(
'focus',
`${theAttachment.meta?.focus?.x || 0},${
-theAttachment.meta?.focus?.y || 0
}`
)
}
theAttachment?.id &&
apiInstance<Mastodon.Attachment>({
method: 'put',
url: `media/${theAttachment.id}`,
body: formData
})
.then(() => {
haptics('Success')
navigation.goBack()
})
.catch(() => {
setIsSubmitting(false)
haptics('Error')
Alert.alert(
t('content.editAttachment.header.right.failed.title'),
undefined,
[
{
text: t(
'content.editAttachment.header.right.failed.button'
),
style: 'cancel'
}
]
)
})
}}
/>
)
}
export default ComposeEditAttachmentSubmit

View File

@ -171,21 +171,23 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
haptics('Success') haptics('Success')
}} }}
/> />
<Button {!composeState.attachments.disallowEditing ? (
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', { <Button
attachment: index + 1 accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
})} attachment: index + 1
type='icon' })}
content='Edit' type='icon'
spacing='M' content='Edit'
round spacing='M'
overlay round
onPress={() => { overlay
navigation.navigate('Screen-Compose-EditAttachment', { onPress={() => {
index navigation.navigate('Screen-Compose-EditAttachment', {
}) index
}} })
/> }}
/>
) : null}
</View> </View>
)} )}
</View> </View>

View File

@ -65,6 +65,7 @@ const composeParseState = (
}), }),
...(params.incomingStatus.media_attachments && { ...(params.incomingStatus.media_attachments && {
attachments: { attachments: {
...(params.type === 'edit' && { disallowEditing: true }),
sensitive: params.incomingStatus.sensitive, sensitive: params.incomingStatus.sensitive,
uploads: params.incomingStatus.media_attachments.map(media => ({ uploads: params.incomingStatus.media_attachments.map(media => ({
remote: media remote: media

View File

@ -1,7 +1,6 @@
import LinkifyIt from 'linkify-it' import LinkifyIt from 'linkify-it'
import { debounce, differenceWith, isEqual } from 'lodash' import { debounce, differenceWith, isEqual } from 'lodash'
import React, { Dispatch } from 'react' import React, { Dispatch } from 'react'
import { FetchOptions } from 'react-query/types/core/query'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ComposeAction, ComposeState } from './types' import { ComposeAction, ComposeState } from './types'
import { instanceConfigurationStatusCharsURL } from '../Root' import { instanceConfigurationStatusCharsURL } from '../Root'
@ -12,7 +11,6 @@ export interface Params {
textInput: ComposeState['textInputFocus']['current'] textInput: ComposeState['textInputFocus']['current']
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
content: string content: string
refetch?: (options?: FetchOptions | undefined) => Promise<any>
disableDebounce?: boolean disableDebounce?: boolean
} }

View File

@ -51,6 +51,7 @@ export type ComposeState = {
expire: '300' | '1800' | '3600' | '21600' | '86400' | '259200' | '604800' expire: '300' | '1800' | '3600' | '21600' | '86400' | '259200' | '604800'
} }
attachments: { attachments: {
disallowEditing?: boolean // https://github.com/mastodon/mastodon/pull/20878
sensitive: boolean sensitive: boolean
uploads: ExtendedAttachment[] uploads: ExtendedAttachment[]
} }
@ -59,8 +60,8 @@ export type ComposeState = {
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
textInputFocus: { textInputFocus: {
current: 'text' | 'spoiler' current: 'text' | 'spoiler'
refs: { text: RefObject<TextInput>, spoiler: RefObject<TextInput> } refs: { text: RefObject<TextInput>; spoiler: RefObject<TextInput> }
isFocused: { text: MutableRefObject<boolean>, spoiler: MutableRefObject<boolean> } isFocused: { text: MutableRefObject<boolean>; spoiler: MutableRefObject<boolean> }
} }
} }

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,192 +0,0 @@
import { HeaderCenter, 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 { Platform } from 'react-native'
import TabMeBookmarks from './Me/Bookmarks'
import TabMeConversations from './Me/Cconversations'
import TabMeFavourites from './Me/Favourites'
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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.favourites.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 }),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter
content={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 }),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.listsAdd.name')} />
}),
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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={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'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.switch.name')} />
}),
headerLeft: () => (
<HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
)
})}
/>
{TabShared({ Stack })}
</Stack.Navigator>
)
},
() => true
)
export default TabMe

View File

@ -0,0 +1,71 @@
import Button from '@components/Button'
import haptics from '@components/haptics'
import ComponentHashtag from '@components/Hashtag'
import { displayMessage } from '@components/Message'
import ComponentSeparator from '@components/Separator'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native-gesture-handler'
const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({
navigation
}) => {
const { t } = useTranslation('screenTabs')
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
useEffect(() => {
if (flattenData.length === 0) {
navigation.goBack()
}
}, [flattenData.length])
const mutation = useTagsMutation({
onSuccess: () => {
haptics('Light')
refetch()
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow')
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
return (
<FlatList
style={{ flex: 1 }}
data={flattenData}
renderItem={({ item }) => (
<ComponentHashtag
hashtag={item}
onPress={() => {}}
children={
<Button
type='text'
content={t('shared.hashtag.unfollow')}
onPress={() => mutation.mutate({ tag: item.name, to: !item.following })}
/>
}
/>
)}
onEndReached={() => fetchNextPage()}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={ComponentSeparator}
/>
)
}
export default TabMeFollowedTags

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

@ -13,7 +13,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Platform, ScrollView, TextInput } from 'react-native' import { Alert, Platform, ScrollView, TextInput } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
navigation, navigation,

View File

@ -7,14 +7,14 @@ import { useTranslation } from 'react-i18next'
const TabMeListList: React.FC<TabMeStackScreenProps<'Tab-Me-List-List'>> = ({ navigation }) => { const TabMeListList: React.FC<TabMeStackScreenProps<'Tab-Me-List-List'>> = ({ navigation }) => {
const { data } = useListsQuery({}) const { data } = useListsQuery({})
const { t } = useTranslation('screenTabs') const { t } = useTranslation()
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('me.stacks.listAdd.name')} type='text'
content='Plus' content={t('common:buttons.create')}
onPress={() => navigation.navigate('Tab-Me-List-Edit', { type: 'add' })} onPress={() => navigation.navigate('Tab-Me-List-Edit', { type: 'add' })}
/> />
) )

View File

@ -9,7 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import { menuListAccounts, menuListDelete, menuListEdit } from './menus' import { menuListAccounts, menuListDelete, menuListEdit } from './menus'

View File

@ -1,7 +1,7 @@
import navigationRef from '@helpers/navigationRef' import navigationRef from '@helpers/navigationRef'
import i18next from 'i18next' import i18next from 'i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { UseMutationResult } from 'react-query' import { UseMutationResult } from '@tanstack/react-query'
export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({ export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({
key: 'list-accounts', key: 'list-accounts',

View File

@ -21,7 +21,7 @@ const TabMeProfileRoot: React.FC<
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { data, isLoading } = useProfileQuery() const { data, isFetching } = useProfileQuery()
const { mutateAsync } = useProfileMutation() const { mutateAsync } = useProfileMutation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -31,7 +31,7 @@ const TabMeProfileRoot: React.FC<
<MenuRow <MenuRow
title={t('me.profile.root.name.title')} title={t('me.profile.root.name.title')}
content={data?.display_name} content={data?.display_name}
loading={isLoading} loading={isFetching}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => { onPress={() => {
data && data &&
@ -45,7 +45,7 @@ const TabMeProfileRoot: React.FC<
<MenuRow <MenuRow
title={t('me.profile.root.note.title')} title={t('me.profile.root.note.title')}
content={data?.source.note} content={data?.source.note}
loading={isLoading} loading={isFetching}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => { onPress={() => {
data && data &&
@ -63,7 +63,7 @@ const TabMeProfileRoot: React.FC<
}) })
: undefined : undefined
} }
loading={isLoading} loading={isFetching}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => { onPress={() => {
navigation.navigate('Tab-Me-Profile-Fields', { navigation.navigate('Tab-Me-Profile-Fields', {
@ -80,7 +80,7 @@ const TabMeProfileRoot: React.FC<
? t(`me.profile.root.visibility.options.${data?.source.privacy}`) ? t(`me.profile.root.visibility.options.${data?.source.privacy}`)
: undefined : undefined
} }
loading={isLoading} loading={isFetching}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
showActionSheetWithOptions( showActionSheetWithOptions(
@ -138,7 +138,7 @@ const TabMeProfileRoot: React.FC<
data: data?.source.sensitive === undefined ? true : !data.source.sensitive data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences())) }).then(() => dispatch(updateAccountPreferences()))
} }
loading={isLoading} loading={isFetching}
/> />
</MenuContainer> </MenuContainer>
<MenuContainer> <MenuContainer>
@ -158,7 +158,7 @@ const TabMeProfileRoot: React.FC<
data: data?.locked === undefined ? true : !data.locked data: data?.locked === undefined ? true : !data.locked
}) })
} }
loading={isLoading} loading={isFetching}
/> />
<MenuRow <MenuRow
title={t('me.profile.root.bot.title')} title={t('me.profile.root.bot.title')}
@ -176,7 +176,7 @@ const TabMeProfileRoot: React.FC<
data: data?.bot === undefined ? true : !data.bot data: data?.bot === undefined ? true : !data.bot
}) })
} }
loading={isLoading} loading={isFetching}
/> />
</MenuContainer> </MenuContainer>
</ScrollView> </ScrollView>

View File

@ -24,17 +24,14 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
<MenuRow <MenuRow
title={t(`me.profile.root.${type}.title`)} title={t(`me.profile.root.${type}.title`)}
description={t(`me.profile.root.${type}.description`)} description={t(`me.profile.root.${type}.description`)}
loading={query.isLoading || mutation.isLoading} loading={query.isFetching || mutation.isLoading}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={async () => { onPress={async () => {
const image = await mediaSelector({ const image = await mediaSelector({
mediaType: 'photo', mediaType: 'photo',
maximum: 1, maximum: 1,
showActionSheetWithOptions, showActionSheetWithOptions,
resize: resize: type === 'avatar' ? { width: 400, height: 400 } : { width: 1500, height: 500 }
type === 'avatar'
? { width: 400, height: 400 }
: { width: 1500, height: 500 }
}) })
if (image[0].uri) { if (image[0].uri) {
mutation.mutate({ mutation.mutate({

View File

@ -3,17 +3,13 @@ import Icon from '@components/Icon'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import browserPackage from '@helpers/browserPackage' import browserPackage from '@helpers/browserPackage'
import { checkPermission } from '@helpers/permissions'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment' import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice' import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { import { PUSH_ADMIN, PUSH_DEFAULT, setChannels } from '@utils/slices/instances/push/utils'
checkPushAdminPermission,
PUSH_ADMIN,
PUSH_DEFAULT,
setChannels
} from '@utils/slices/instances/push/utils'
import { updateInstancePush } from '@utils/slices/instances/updatePush' import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert' import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode' import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
@ -35,12 +31,7 @@ const TabMePush: React.FC = () => {
const instance = useSelector(getInstance) const instance = useSelector(getInstance)
const expoToken = useSelector(getExpoToken) const expoToken = useSelector(getExpoToken)
const [serverKeyAvailable, setServerKeyAvailable] = useState<boolean>() const appsQuery = useAppsQuery()
useAppsQuery({
options: {
onSuccess: data => setServerKeyAvailable(!!data.vapid_key)
}
})
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const instancePush = useSelector(getInstancePush) const instancePush = useSelector(getInstancePush)
@ -66,7 +57,7 @@ const TabMePush: React.FC = () => {
} }
} }
if (serverKeyAvailable) { if (appsQuery.data?.vapid_key) {
checkPush() checkPush()
if (isDevelopment) { if (isDevelopment) {
@ -80,7 +71,7 @@ const TabMePush: React.FC = () => {
return () => { return () => {
subscription.remove() subscription.remove()
} }
}, [serverKeyAvailable]) }, [appsQuery.data?.vapid_key])
const alerts = () => const alerts = () =>
instancePush?.alerts instancePush?.alerts
@ -93,7 +84,6 @@ const TabMePush: React.FC = () => {
switchOnValueChange={() => switchOnValueChange={() =>
dispatch( dispatch(
updateInstancePushAlert({ updateInstancePushAlert({
changed: alert,
alerts: { alerts: {
...instancePush?.alerts, ...instancePush?.alerts,
[alert]: instancePush?.alerts[alert] [alert]: instancePush?.alerts[alert]
@ -109,7 +99,7 @@ const TabMePush: React.FC = () => {
const adminAlerts = () => const adminAlerts = () =>
profileQuery.data?.role?.permissions profileQuery.data?.role?.permissions
? PUSH_ADMIN.map(({ type, permission }) => ? PUSH_ADMIN.map(({ type, permission }) =>
checkPushAdminPermission(permission, profileQuery.data.role?.permissions) ? ( checkPermission(permission, profileQuery.data.role?.permissions) ? (
<MenuRow <MenuRow
key={type} key={type}
title={t(`me.push.${type}.heading`)} title={t(`me.push.${type}.heading`)}
@ -118,7 +108,6 @@ const TabMePush: React.FC = () => {
switchOnValueChange={() => switchOnValueChange={() =>
dispatch( dispatch(
updateInstancePushAlert({ updateInstancePushAlert({
changed: type,
alerts: { alerts: {
...instancePush?.alerts, ...instancePush?.alerts,
[type]: instancePush?.alerts[type] [type]: instancePush?.alerts[type]
@ -133,7 +122,7 @@ const TabMePush: React.FC = () => {
return ( return (
<ScrollView> <ScrollView>
{!!serverKeyAvailable ? ( {!!appsQuery.data?.vapid_key ? (
<> <>
{!!pushAvailable ? ( {!!pushAvailable ? (
<> <>

View File

@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement' import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
import { getInstanceMePage, updateInstanceMePage } from '@utils/slices/instancesSlice' import { getInstanceMePage, updateInstanceMePage } from '@utils/slices/instancesSlice'
import { getInstancePush } from '@utils/slices/instancesSlice' import { getInstancePush } from '@utils/slices/instancesSlice'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
@ -16,39 +17,40 @@ const Collections: React.FC = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const mePage = useSelector(getInstanceMePage) const mePage = useSelector(getInstanceMePage)
const listsQuery = useListsQuery({ useFollowedTagsQuery({
options: { options: {
notifyOnChangeProps: ['data'] onSuccess: data =>
dispatch(
updateInstanceMePage({
followedTags: { shown: !!data?.pages?.[0].body?.length }
})
)
} }
}) })
useEffect(() => { useListsQuery({
if (listsQuery.isSuccess) { options: {
dispatch( onSuccess: data =>
updateInstanceMePage({ dispatch(
lists: { shown: listsQuery.data?.length ? true : false } updateInstanceMePage({
}) lists: { shown: !!data?.length }
) })
)
} }
}, [listsQuery.isSuccess, listsQuery.data?.length]) })
useAnnouncementQuery({
const announcementsQuery = useAnnouncementQuery({
showAll: true, showAll: true,
options: { options: {
notifyOnChangeProps: ['data'] onSuccess: data =>
dispatch(
updateInstanceMePage({
announcements: {
shown: !!data?.length ? true : false,
unread: data?.filter(announcement => !announcement.read).length
}
})
)
} }
}) })
useEffect(() => {
if (announcementsQuery.data) {
dispatch(
updateInstanceMePage({
announcements: {
shown: announcementsQuery.data.length ? true : false,
unread: announcementsQuery.data.filter(announcement => !announcement.read).length
}
})
)
}
}, [announcementsQuery.data])
const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global) const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global)
@ -80,6 +82,14 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-List-List')} onPress={() => navigation.navigate('Tab-Me-List-List')}
/> />
) : null} ) : null}
{mePage.followedTags.shown ? (
<MenuRow
iconFront='Hash'
iconBack='ChevronRight'
title={t('me.stacks.followedTags.name')}
onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
/>
) : null}
{mePage.announcements.shown ? ( {mePage.announcements.shown ? (
<MenuRow <MenuRow
iconFront='Clipboard' iconFront='Clipboard'

View File

@ -7,7 +7,7 @@ import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const Logout: React.FC = () => { const Logout: React.FC = () => {

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,73 +0,0 @@
import { HeaderCenter, HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications'
import navigationRef from '@helpers/navigationRef'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabNotificationsStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
const TabNotifications = React.memo(
() => {
const { t, i18n } = useTranslation('screenTabs')
const screenOptionsRoot = useMemo(
() => ({
title: t('tabs.notifications.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('tabs.notifications.name')} />
}),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('notifications.filter.accessibilityLabel')}
accessibilityHint={t('notifications.filter.accessibilityHint')}
content='Filter'
onPress={() =>
navigationRef.navigate('Screen-Actions', {
type: 'notifications_filter'
})
}
/>
)
}),
[i18n.language]
)
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
const children = useCallback(
() => (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => (
<TimelineNotifications notification={item} queryKey={queryKey} />
)
}}
/>
),
[]
)
usePopToTop()
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name='Tab-Notifications-Root'
children={children}
options={screenOptionsRoot}
/>
{TabShared({ Stack })}
</Stack.Navigator>
)
},
() => true
)
export default TabNotifications

View File

@ -0,0 +1,138 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import { MenuContainer, MenuRow } from '@components/Menu'
import {
checkPermission,
PERMISSION_MANAGE_REPORTS,
PERMISSION_MANAGE_USERS
} from '@helpers/permissions'
import { useAppDispatch } from '@root/store'
import { useQueryClient } from '@tanstack/react-query'
import { TabNotificationsStackScreenProps } from '@utils/navigation/navigators'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import {
checkInstanceFeature,
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { isEqual } from 'lodash'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
export const NOTIFICATIONS_FILTERS_DEFAULT: [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
] = ['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll', 'status', 'update']
export const NOTIFICATIONS_FILTERS_ADMIN: {
type: 'admin.sign_up' | 'admin.report'
permission: number
}[] = [
{ type: 'admin.sign_up', permission: PERMISSION_MANAGE_USERS },
{ type: 'admin.report', permission: PERMISSION_MANAGE_REPORTS }
]
const TabNotificationsFilters: React.FC<
TabNotificationsStackScreenProps<'Tab-Notifications-Filters'>
> = ({ navigation }) => {
const { t } = useTranslation('screenTabs')
const hasTypeStatus = useSelector(checkInstanceFeature('notification_type_status'))
const hasTypeUpdate = useSelector(checkInstanceFeature('notification_type_update'))
const dispatch = useAppDispatch()
const instanceNotificationsFilter = useSelector(getInstanceNotificationsFilter)
const [filters, setFilters] = useState(instanceNotificationsFilter)
const queryClient = useQueryClient()
useEffect(() => {
const changed = !isEqual(instanceNotificationsFilter, filters)
navigation.setOptions({
title: t('notifications.filters.title'),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => {
if (changed) {
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
{
text: t('common:buttons.discard'),
style: 'destructive',
onPress: () => navigation.goBack()
},
{
text: t('common:buttons.cancel'),
style: 'default'
}
])
} else {
navigation.goBack()
}
}}
/>
),
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.apply')}
onPress={() => {
if (changed) {
dispatch(updateInstanceNotificationsFilter(filters))
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
queryClient.invalidateQueries({ queryKey })
}
navigation.goBack()
}}
/>
)
})
}, [filters])
const profileQuery = useProfileQuery()
return (
<ScrollView style={{ flex: 1 }}>
<MenuContainer>
{NOTIFICATIONS_FILTERS_DEFAULT.filter(type => {
switch (type) {
case 'status':
return hasTypeStatus
case 'update':
return hasTypeUpdate
default:
return true
}
}).map((type, index) => (
<MenuRow
key={index}
title={t(`notifications.filters.options.${type}`)}
switchValue={filters[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })}
/>
))}
{NOTIFICATIONS_FILTERS_ADMIN.filter(({ permission }) =>
checkPermission(permission, profileQuery.data?.role?.permissions)
).map(({ type }) => (
<MenuRow
key={type}
title={t(`notifications.filters.options.${type}`)}
switchValue={filters[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })}
/>
))}
</MenuContainer>
</ScrollView>
)
}
export default TabNotificationsFilters

View File

@ -0,0 +1,61 @@
import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { ScreenTabsScreenProps, TabNotificationsStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
import { useTranslation } from 'react-i18next'
import TabShared from '../Shared'
import TabNotificationsFilters from './Filters'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
const Root = () => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineNotifications notification={item} queryKey={queryKey} />
}}
/>
)
}
const TabNotifications = ({ navigation }: ScreenTabsScreenProps<'Tab-Notifications'>) => {
const { t } = useTranslation('screenTabs')
usePopToTop()
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name='Tab-Notifications-Root'
component={Root}
options={{
title: t('tabs.notifications.name'),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('notifications.filters.accessibilityLabel')}
accessibilityHint={t('notifications.filters.accessibilityHint')}
content='Filter'
onPress={() =>
navigation.navigate('Tab-Notifications', { screen: 'Tab-Notifications-Filters' })
}
/>
)
}}
/>
<Stack.Screen
name='Tab-Notifications-Filters'
component={TabNotificationsFilters}
options={{ presentation: 'modal', gestureEnabled: false }}
/>
{TabShared({ Stack })}
</Stack.Navigator>
)
}
export default TabNotifications

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,96 @@
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 { useAppDispatch } from '@root/store'
import { ContextsLatest } from '@utils/migrations/contexts/migration'
import { TabPublicStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getPreviousSegment, updatePreviousSegment } from '@utils/slices/contextsSlice'
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'
import { useSelector } from 'react-redux'
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 dispatch = useAppDispatch()
const previousSegment = useSelector(getPreviousSegment, () => true)
const segments: ContextsLatest['previousSegment'][] = ['Local', 'LocalPublic', 'Trending']
const [segment, setSegment] = useState<number>(
segments.findIndex(segment => segment === previousSegment)
)
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)
dispatch(updatePreviousSegment(segments[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

@ -13,7 +13,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Text, View } from 'react-native' import { Text, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated' import { useSharedValue } from 'react-native-reanimated'
import { useIsFetching } from 'react-query' import { useIsFetching } from '@tanstack/react-query'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import AccountAttachments from './Account/Attachments' import AccountAttachments from './Account/Attachments'
import AccountHeader from './Account/Header' import AccountHeader from './Account/Header'

View File

@ -3,11 +3,11 @@ import { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default' import TimelineDefault from '@components/Timeline/Default'
import { useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags' import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { checkInstanceFeature } from '@utils/slices/instancesSlice' import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -27,7 +27,6 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
const { theme } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const canFollowTags = useSelector(checkInstanceFeature('follow_tags')) const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
@ -35,14 +34,16 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
tag: hashtag, tag: hashtag,
options: { enabled: canFollowTags } options: { enabled: canFollowTags }
}) })
const queryClient = useQueryClient()
const mutation = useTagsMutation({ const mutation = useTagsMutation({
onSuccess: () => { onSuccess: () => {
haptics('Success') haptics('Success')
refetch() refetch()
const queryKeyFollowedTags: QueryKeyFollowedTags = ['FollowedTags']
queryClient.invalidateQueries({ queryKey: queryKeyFollowedTags })
}, },
onError: (err: any, { to }) => { onError: (err: any, { to }) => {
displayMessage({ displayMessage({
theme,
type: 'error', type: 'error',
message: t('common:message.error.message', { message: t('common:message.error.message', {
function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow') function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow')
@ -68,7 +69,7 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
content={data?.following ? t('shared.hashtag.unfollow') : t('shared.hashtag.follow')} content={data?.following ? t('shared.hashtag.unfollow') : t('shared.hashtag.follow')}
onPress={() => onPress={() =>
typeof data?.following === 'boolean' && typeof data?.following === 'boolean' &&
mutation.mutate({ tag: hashtag, type: 'follow', to: !data.following }) mutation.mutate({ tag: hashtag, to: !data.following })
} }
/> />
) )

View File

@ -10,12 +10,12 @@ import { StyleSheet, TextInput, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
isLoading: boolean isFetching: boolean
inputRef: React.RefObject<TextInput> inputRef: React.RefObject<TextInput>
setSearchTerm: React.Dispatch<React.SetStateAction<string>> setSearchTerm: React.Dispatch<React.SetStateAction<string>>
} }
const SearchEmpty: React.FC<Props> = ({ isLoading, inputRef, setSearchTerm }) => { const SearchEmpty: React.FC<Props> = ({ isFetching, inputRef, setSearchTerm }) => {
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
@ -23,7 +23,7 @@ const SearchEmpty: React.FC<Props> = ({ isLoading, inputRef, setSearchTerm }) =>
return ( return (
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}> <View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}>
{isLoading ? ( {isFetching ? (
<View style={{ flex: 1, alignItems: 'center' }}> <View style={{ flex: 1, alignItems: 'center' }}>
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> <Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View> </View>

View File

@ -84,7 +84,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
hashtags: t('shared.search.sections.hashtags'), hashtags: t('shared.search.sections.hashtags'),
statuses: t('shared.search.sections.statuses') statuses: t('shared.search.sections.statuses')
} }
const { isLoading, data, refetch } = useSearchQuery< const { isFetching, data, refetch } = useSearchQuery<
{ {
title: string title: string
translation: string translation: string
@ -138,7 +138,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
}} }}
stickySectionHeadersEnabled stickySectionHeadersEnabled
ListEmptyComponent={ ListEmptyComponent={
<SearchEmpty isLoading={isLoading} inputRef={inputRef} setSearchTerm={setSearchTerm} /> <SearchEmpty isFetching={isFetching} inputRef={inputRef} setSearchTerm={setSearchTerm} />
} }
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
renderSectionHeader={({ section: { translation } }) => ( renderSectionHeader={({ section: { translation } }) => (

View File

@ -6,7 +6,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native' import { FlatList } from 'react-native'
import { InfiniteQueryObserver, useQueryClient } from 'react-query' import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
navigation, navigation,

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

@ -6,15 +6,10 @@ import TabSharedHistory from '@screens/Tabs/Shared/History'
import TabSharedSearch from '@screens/Tabs/Shared/Search' import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot' import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users' import TabSharedUsers from '@screens/Tabs/Shared/Users'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import TabSharedAccountInLists from './AccountInLists' import TabSharedAccountInLists from './AccountInLists'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => { const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs')
return ( return (
<Stack.Group> <Stack.Group>
<Stack.Screen <Stack.Screen

View File

@ -1,8 +1,3 @@
/// <reference types="@welldone-software/why-did-you-render" />
import React from 'react'
import log from './log'
const dev = () => { const dev = () => {
if (__DEV__) { if (__DEV__) {
// log('log', 'devs', 'initializing wdyr') // log('log', 'devs', 'initializing wdyr')

View File

@ -3,7 +3,7 @@ import NetInfo from '@react-native-community/netinfo'
import { store } from '@root/store' import { store } from '@root/store'
import removeInstance from '@utils/slices/instances/remove' import removeInstance from '@utils/slices/instances/remove'
import { getInstance, updateInstanceAccount } from '@utils/slices/instancesSlice' import { getInstance, updateInstanceAccount } from '@utils/slices/instancesSlice'
import { onlineManager } from 'react-query' import { onlineManager } from '@tanstack/react-query'
import log from './log' import log from './log'
const netInfo = async (): Promise<{ const netInfo = async (): Promise<{
@ -17,7 +17,7 @@ const netInfo = async (): Promise<{
onlineManager.setEventListener(setOnline => { onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => { return NetInfo.addEventListener(state => {
setOnline(typeof state.isConnected === 'boolean' ? state.isConnected : undefined) setOnline(!!state.isConnected)
}) })
}) })

View File

@ -1,11 +1,11 @@
import createSecureStore from '@neverdull-agency/expo-unlimited-secure-store' import createSecureStore from '@neverdull-agency/expo-unlimited-secure-store'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { AnyAction, configureStore, Reducer } from '@reduxjs/toolkit' import { AnyAction, configureStore, Reducer } from '@reduxjs/toolkit'
import contextsMigration from '@utils/migrations/contexts/migration' import contextsMigration, { ContextsLatest } from '@utils/migrations/contexts/migration'
import instancesMigration from '@utils/migrations/instances/migration' import instancesMigration from '@utils/migrations/instances/migration'
import settingsMigration from '@utils/migrations/settings/migration' import settingsMigration from '@utils/migrations/settings/migration'
import appSlice, { AppState } from '@utils/slices/appSlice' import appSlice, { AppState } from '@utils/slices/appSlice'
import contextsSlice, { ContextsState } from '@utils/slices/contextsSlice' import contextsSlice from '@utils/slices/contextsSlice'
import instancesSlice, { InstancesState } from '@utils/slices/instancesSlice' import instancesSlice, { InstancesState } from '@utils/slices/instancesSlice'
import settingsSlice, { SettingsState } from '@utils/slices/settingsSlice' import settingsSlice, { SettingsState } from '@utils/slices/settingsSlice'
import { Platform } from 'react-native' import { Platform } from 'react-native'
@ -37,7 +37,7 @@ const contextsPersistConfig = {
key: 'contexts', key: 'contexts',
prefix, prefix,
storage: AsyncStorage, storage: AsyncStorage,
version: 2, version: 3,
// @ts-ignore // @ts-ignore
migrate: createMigrate(contextsMigration) migrate: createMigrate(contextsMigration)
} }
@ -64,7 +64,7 @@ const store = configureStore({
reducer: { reducer: {
app: persistReducer(appPersistConfig, appSlice) as Reducer<AppState, AnyAction>, app: persistReducer(appPersistConfig, appSlice) as Reducer<AppState, AnyAction>,
contexts: persistReducer(contextsPersistConfig, contextsSlice) as Reducer< contexts: persistReducer(contextsPersistConfig, contextsSlice) as Reducer<
ContextsState, ContextsLatest,
AnyAction AnyAction
>, >,
instances: persistReducer(instancesPersistConfig, instancesSlice) as Reducer< instances: persistReducer(instancesPersistConfig, instancesSlice) as Reducer<

View File

@ -1,6 +1,7 @@
import { ContextsV0 } from './v0' import { ContextsV0 } from './v0'
import { ContextsV1 } from './v1' import { ContextsV1 } from './v1'
import { ContextsV2 } from './v2' import { ContextsV2 } from './v2'
import { ContextsV3 } from './v3'
const contextsMigration = { const contextsMigration = {
1: (state: ContextsV0): ContextsV1 => { 1: (state: ContextsV0): ContextsV1 => {
@ -15,7 +16,12 @@ const contextsMigration = {
2: (state: ContextsV1): ContextsV2 => { 2: (state: ContextsV1): ContextsV2 => {
const { mePage, ...rest } = state const { mePage, ...rest } = state
return rest return rest
},
3: (state: ContextsV2): ContextsV3 => {
return { ...state, previousSegment: 'Local' }
} }
} }
export { ContextsV3 as ContextsLatest }
export default contextsMigration export default contextsMigration

View File

@ -0,0 +1,19 @@
import { ScreenTabsStackParamList } from '@utils/navigation/navigators'
export type ContextsV3 = {
storeReview: {
context: Readonly<number>
current: number
shown: boolean
}
publicRemoteNotice: {
context: Readonly<number>
current: number
hidden: boolean
}
previousTab: Extract<
keyof ScreenTabsStackParamList,
'Tab-Local' | 'Tab-Public' | 'Tab-Notifications' | 'Tab-Me'
>
previousSegment: Extract<App.Pages, 'Local' | 'LocalPublic' | 'Trending'>
}

View File

@ -135,6 +135,12 @@ const instancesMigration = {
instances: state.instances.map(instance => { instances: state.instances.map(instance => {
return { return {
...instance, ...instance,
mePage: { ...instance.mePage, followedTags: { shown: false } },
notifications_filter: {
...instance.notifications_filter,
'admin.sign_up': true,
'admin.report': true
},
push: { push: {
...instance.push, ...instance.push,
global: instance.push.global.value, global: instance.push.global.value,

View File

@ -29,6 +29,8 @@ export type InstanceV11 = {
poll: boolean poll: boolean
status: boolean status: boolean
update: boolean update: boolean
'admin.sign_up': boolean
'admin.report': boolean
} }
push: { push: {
global: boolean global: boolean
@ -47,6 +49,7 @@ export type InstanceV11 = {
} }
} }
mePage: { mePage: {
followedTags: { shown: boolean }
lists: { shown: boolean } lists: { shown: boolean }
announcements: { shown: boolean; unread: number } announcements: { shown: boolean; unread: number }
} }

View File

@ -136,13 +136,17 @@ export type TabPublicStackParamList = {
export type TabNotificationsStackParamList = { export type TabNotificationsStackParamList = {
'Tab-Notifications-Root': undefined 'Tab-Notifications-Root': undefined
'Tab-Notifications-Filters': undefined
} & TabSharedStackParamList } & TabSharedStackParamList
export type TabNotificationsStackScreenProps<T extends keyof TabNotificationsStackParamList> =
NativeStackScreenProps<TabNotificationsStackParamList, T>
export type TabMeStackParamList = { export type TabMeStackParamList = {
'Tab-Me-Root': undefined 'Tab-Me-Root': undefined
'Tab-Me-Bookmarks': undefined 'Tab-Me-Bookmarks': undefined
'Tab-Me-Conversations': undefined 'Tab-Me-Conversations': undefined
'Tab-Me-Favourites': undefined 'Tab-Me-Favourites': undefined
'Tab-Me-FollowedTags': undefined
'Tab-Me-List': Mastodon.List 'Tab-Me-List': Mastodon.List
'Tab-Me-List-Accounts': Omit<Mastodon.List, 'replies_policy'> 'Tab-Me-List-Accounts': Omit<Mastodon.List, 'replies_policy'>
'Tab-Me-List-Edit': 'Tab-Me-List-Edit':

View File

@ -1,16 +1,17 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }] export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }]
const accountQueryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => { const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.Account>({ const res = await apiInstance<Mastodon.Account>({
method: 'get', method: 'get',
url: `accounts/${id}` url: `accounts/${id}`
}).then(res => res.body) })
return res.body
} }
const useAccountQuery = ({ const useAccountQuery = ({
@ -27,15 +28,16 @@ const useAccountQuery = ({
export type QueryKeyAccountInLists = ['AccountInLists', { id: Mastodon.Account['id'] }] export type QueryKeyAccountInLists = ['AccountInLists', { id: Mastodon.Account['id'] }]
const accountInListsQueryFunction = ({ const accountInListsQueryFunction = async ({
queryKey queryKey
}: QueryFunctionContext<QueryKeyAccountInLists>) => { }: QueryFunctionContext<QueryKeyAccountInLists>) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.List[]>({ const res = await apiInstance<Mastodon.List[]>({
method: 'get', method: 'get',
url: `accounts/${id}/lists` url: `accounts/${id}/lists`
}).then(res => res.body) })
return res.body
} }
const useAccountInListsQuery = ({ const useAccountInListsQuery = ({

View File

@ -6,16 +6,16 @@ import {
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from 'react-query' } from '@tanstack/react-query'
type QueryKeyAnnouncement = ['Announcements', { showAll?: boolean }] type QueryKeyAnnouncement = ['Announcements', { showAll?: boolean }]
const queryFunction = ({ const queryFunction = async ({
queryKey queryKey
}: QueryFunctionContext<QueryKeyAnnouncement>) => { }: QueryFunctionContext<QueryKeyAnnouncement>) => {
const { showAll } = queryKey[1] const { showAll } = queryKey[1]
return apiInstance<Mastodon.Announcement[]>({ const res = await apiInstance<Mastodon.Announcement[]>({
method: 'get', method: 'get',
url: `announcements`, url: `announcements`,
...(showAll && { ...(showAll && {
@ -23,7 +23,8 @@ const queryFunction = ({
with_dismissed: 'true' with_dismissed: 'true'
} }
}) })
}).then(res => res.body) })
return res.body
} }
const useAnnouncementQuery = ({ const useAnnouncementQuery = ({

View File

@ -8,7 +8,7 @@ import {
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from 'react-query' } from '@tanstack/react-query'
export type QueryKeyApps = ['Apps'] export type QueryKeyApps = ['Apps']

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from '@tanstack/react-query'
type QueryKeyEmojis = ['Emojis'] type QueryKeyEmojis = ['Emojis']

View File

@ -1,6 +1,6 @@
import apiGeneral from '@api/general' import apiGeneral from '@api/general'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyInstance = ['Instance', { domain?: string }] export type QueryKeyInstance = ['Instance', { domain?: string }]

View File

@ -8,7 +8,7 @@ import {
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from 'react-query' } from '@tanstack/react-query'
export type QueryKeyLists = ['Lists'] export type QueryKeyLists = ['Lists']
@ -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

@ -6,7 +6,7 @@ import { AxiosError } from 'axios'
import i18next from 'i18next' import i18next from 'i18next'
import { RefObject } from 'react' import { RefObject } from 'react'
import FlashMessage from 'react-native-flash-message' import FlashMessage from 'react-native-flash-message'
import { useMutation, useQuery, UseQueryOptions } from 'react-query' import { useMutation, useQuery, UseQueryOptions } from '@tanstack/react-query'
type AccountWithSource = Mastodon.Account & Required<Pick<Mastodon.Account, 'source'>> type AccountWithSource = Mastodon.Account & Required<Pick<Mastodon.Account, 'source'>>

View File

@ -6,25 +6,26 @@ import {
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from 'react-query' } from '@tanstack/react-query'
export type QueryKeyRelationship = [ export type QueryKeyRelationship = [
'Relationship', 'Relationship',
{ id: Mastodon.Account['id'] } { id: Mastodon.Account['id'] }
] ]
const queryFunction = ({ const queryFunction = async ({
queryKey queryKey
}: QueryFunctionContext<QueryKeyRelationship>) => { }: QueryFunctionContext<QueryKeyRelationship>) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.Relationship[]>({ const res = await apiInstance<Mastodon.Relationship[]>({
method: 'get', method: 'get',
url: `accounts/relationships`, url: `accounts/relationships`,
params: { params: {
'id[]': id 'id[]': id
} }
}).then(res => res.body) })
return res.body
} }
const useRelationshipQuery = ({ const useRelationshipQuery = ({

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeySearch = [ export type QueryKeySearch = [
'Search', 'Search',

View File

@ -1,16 +1,17 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyStatus = ['Status', { id: Mastodon.Status['id'] }] export type QueryKeyStatus = ['Status', { id: Mastodon.Status['id'] }]
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyStatus>) => { const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatus>) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.Status>({ const res = await apiInstance<Mastodon.Status>({
method: 'get', method: 'get',
url: `statuses/${id}` url: `statuses/${id}`
}).then(res => res.body) })
return res.body
} }
const useStatusQuery = ({ const useStatusQuery = ({

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyStatusesHistory = [ export type QueryKeyStatusesHistory = [
'StatusesHistory', 'StatusesHistory',

View File

@ -1,36 +1,45 @@
import apiInstance from '@api/instance' import apiInstance, { InstanceResponse } from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { import {
QueryFunctionContext, QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation, useMutation,
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from 'react-query' } from '@tanstack/react-query'
import { infinitePageParams } from './utils'
type QueryKeyFollowedTags = ['FollowedTags'] export type QueryKeyFollowedTags = ['FollowedTags']
const useFollowedTagsQuery = ({ const useFollowedTagsQuery = (
options params: {
}: { options?: Omit<
options?: UseQueryOptions<Mastodon.Tag, AxiosError> UseInfiniteQueryOptions<InstanceResponse<Mastodon.Tag[]>, AxiosError>,
}) => { 'getPreviousPageParam' | 'getNextPageParam'
>
} | void
) => {
const queryKey: QueryKeyFollowedTags = ['FollowedTags'] const queryKey: QueryKeyFollowedTags = ['FollowedTags']
return useQuery( return useInfiniteQuery(
queryKey, queryKey,
async ({ pageParam }: QueryFunctionContext<QueryKeyFollowedTags>) => { async ({ pageParam }: QueryFunctionContext<QueryKeyFollowedTags>) => {
const params: { [key: string]: string } = { ...pageParam } const params: { [key: string]: string } = { ...pageParam }
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `followed_tags`, params }) return await apiInstance<Mastodon.Tag[]>({ method: 'get', url: `followed_tags`, params })
return res.body
}, },
options {
...params?.options,
...infinitePageParams
}
) )
} }
type QueryKeyTags = ['Tags', { tag: string }] export type QueryKeyTags = ['Tags', { tag: string }]
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => { const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
const { tag } = queryKey[1] const { tag } = queryKey[1]
return apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` }).then(res => res.body) const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` })
return res.body
} }
const useTagsQuery = ({ const useTagsQuery = ({
options, options,
@ -42,15 +51,12 @@ const useTagsQuery = ({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
type MutationVarsAnnouncement = { tag: string; type: 'follow'; to: boolean } type MutationVarsAnnouncement = { tag: string; to: boolean }
const mutationFunction = async ({ tag, type, to }: MutationVarsAnnouncement) => { const mutationFunction = async ({ tag, to }: MutationVarsAnnouncement) => {
switch (type) { return apiInstance<{}>({
case 'follow': method: 'post',
return apiInstance<{}>({ url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
method: 'post', })
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
})
}
} }
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => { const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
return useMutation(mutationFunction, options) return useMutation(mutationFunction, options)

View File

@ -11,7 +11,7 @@ import {
useInfiniteQuery, useInfiniteQuery,
UseInfiniteQueryOptions, UseInfiniteQueryOptions,
useMutation useMutation
} from 'react-query' } from '@tanstack/react-query'
import deleteItem from './timeline/deleteItem' import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem' import editItem from './timeline/editItem'
import updateStatusProperty from './timeline/updateStatusProperty' import updateStatusProperty from './timeline/updateStatusProperty'
@ -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

@ -1,5 +1,5 @@
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query' import { InfiniteData } from '@tanstack/react-query'
import { MutationVarsTimelineDeleteItem } from '../timeline' import { MutationVarsTimelineDeleteItem } from '../timeline'
const deleteItem = ({ const deleteItem = ({

View File

@ -1,5 +1,5 @@
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query' import { InfiniteData } from '@tanstack/react-query'
import { MutationVarsTimelineEditItem } from '../timeline' import { MutationVarsTimelineEditItem } from '../timeline'
const editItem = ({ const editItem = ({

View File

@ -1,5 +1,5 @@
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query' import { InfiniteData } from '@tanstack/react-query'
import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline' import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
import updateConversation from './update/conversation' import updateConversation from './update/conversation'
import updateNotification from './update/notification' import updateNotification from './update/notification'

View File

@ -1,7 +1,7 @@
import apiTooot from '@api/tooot' import apiTooot from '@api/tooot'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
type Translations = type Translations =
| { | {

View File

@ -2,7 +2,7 @@ import apiInstance from '@api/instance'
import { store } from '@root/store' import { store } from '@root/store'
import { checkInstanceFeature } from '@utils/slices/instancesSlice' import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyTrends = ['Trends', { type: 'tags' | 'statuses' | 'links' }] export type QueryKeyTrends = ['Trends', { type: 'tags' | 'statuses' | 'links' }]

View File

@ -5,7 +5,7 @@ import {
QueryFunctionContext, QueryFunctionContext,
useInfiniteQuery, useInfiniteQuery,
UseInfiniteQueryOptions UseInfiniteQueryOptions
} from 'react-query' } from '@tanstack/react-query'
export type QueryKeyUsers = [ export type QueryKeyUsers = [
'Users', 'Users',

Some files were not shown because too many files have changed in this diff Show More