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:
@ -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
|
@ -1,5 +1,6 @@
|
|||||||
toooting愉快!此版本包括以下改进和修复:
|
toooting愉快!此版本包括以下改进和修复:
|
||||||
- 自动识别发嘟语言
|
- 自动识别发嘟语言
|
||||||
|
- 记住上次公共时间轴选项
|
||||||
- 新增管理员推送通知
|
- 新增管理员推送通知
|
||||||
- 修复过滤整词功能
|
- 修复过滤整词功能
|
||||||
- 修复平板不能删除草稿
|
- 修复平板不能删除草稿
|
@ -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
1
src/@types/app.d.ts
vendored
@ -3,6 +3,7 @@ declare namespace App {
|
|||||||
| 'Following'
|
| 'Following'
|
||||||
| 'Local'
|
| 'Local'
|
||||||
| 'LocalPublic'
|
| 'LocalPublic'
|
||||||
|
| 'Trending'
|
||||||
| 'Notifications'
|
| 'Notifications'
|
||||||
| 'Hashtag'
|
| 'Hashtag'
|
||||||
| 'List'
|
| 'List'
|
||||||
|
1
src/@types/untyped.d.ts
vendored
1
src/@types/untyped.d.ts
vendored
@ -1,6 +1,5 @@
|
|||||||
declare module 'gl-react-blurhash'
|
declare module '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'
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
@ -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']
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -125,6 +125,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
spoilerHidden,
|
spoilerHidden,
|
||||||
copiableContent,
|
copiableContent,
|
||||||
highlighted,
|
highlighted,
|
||||||
|
inThread: queryKey?.[1].page === 'Toot',
|
||||||
disableDetails,
|
disableDetails,
|
||||||
disableOnPress
|
disableOnPress
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -16,6 +16,7 @@ type ContextType = {
|
|||||||
}>
|
}>
|
||||||
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
inThread?: boolean
|
||||||
disableDetails?: boolean
|
disableDetails?: boolean
|
||||||
disableOnPress?: boolean
|
disableOnPress?: boolean
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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}
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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
|
||||||
|
@ -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 } } })
|
||||||
|
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
||||||
|
@ -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} />
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
@ -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>
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
src/screens/Compose/utils/types.d.ts
vendored
5
src/screens/Compose/utils/types.d.ts
vendored
@ -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> }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
import { HeaderCenter, HeaderRight } from '@components/Header'
|
|
||||||
import Timeline from '@components/Timeline'
|
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|
||||||
import { ScreenTabsScreenProps, TabLocalStackParamList } from '@utils/navigation/navigators'
|
|
||||||
import usePopToTop from '@utils/navigation/usePopToTop'
|
|
||||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
|
||||||
import TabShared from './Shared'
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<TabLocalStackParamList>()
|
|
||||||
|
|
||||||
const TabLocal = React.memo(
|
|
||||||
({ navigation }: ScreenTabsScreenProps<'Tab-Local'>) => {
|
|
||||||
const { t } = useTranslation('screenTabs')
|
|
||||||
|
|
||||||
const { data: lists } = useListsQuery({})
|
|
||||||
|
|
||||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }])
|
|
||||||
|
|
||||||
usePopToTop()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Tab-Local-Root'
|
|
||||||
options={{
|
|
||||||
headerTitle: () => (
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<HeaderCenter
|
|
||||||
dropdown={(lists?.length ?? 0) > 0}
|
|
||||||
content={
|
|
||||||
queryKey[1].page === 'List' && queryKey[1].list?.length
|
|
||||||
? lists?.find(list => list.id === queryKey[1].list)?.title
|
|
||||||
: t('tabs.local.name')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
{lists?.length
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: 'default',
|
|
||||||
item: {
|
|
||||||
onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]),
|
|
||||||
disabled: queryKey[1].page === 'Following',
|
|
||||||
destructive: false,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
title: t('tabs.local.name'),
|
|
||||||
icon: ''
|
|
||||||
},
|
|
||||||
...lists?.map(list => ({
|
|
||||||
key: list.id,
|
|
||||||
item: {
|
|
||||||
onSelect: () =>
|
|
||||||
setQueryKey(['Timeline', { page: 'List', list: list.id }]),
|
|
||||||
disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id,
|
|
||||||
destructive: false,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
title: list.title,
|
|
||||||
icon: ''
|
|
||||||
}))
|
|
||||||
].map(menu => (
|
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
|
||||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))
|
|
||||||
: undefined}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
),
|
|
||||||
headerRight: () => (
|
|
||||||
<HeaderRight
|
|
||||||
accessibilityLabel={t('common.search.accessibilityLabel')}
|
|
||||||
accessibilityHint={t('common.search.accessibilityHint')}
|
|
||||||
content='Search'
|
|
||||||
onPress={() => navigation.navigate('Tab-Local', { screen: 'Tab-Shared-Search' })}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
children={() => (
|
|
||||||
<Timeline
|
|
||||||
queryKey={queryKey}
|
|
||||||
lookback='Following'
|
|
||||||
customProps={{
|
|
||||||
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{TabShared({ Stack })}
|
|
||||||
</Stack.Navigator>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TabLocal
|
|
96
src/screens/Tabs/Local/Root.tsx
Normal file
96
src/screens/Tabs/Local/Root.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { HeaderCenter, HeaderRight } from '@components/Header'
|
||||||
|
import Timeline from '@components/Timeline'
|
||||||
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||||
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import usePopToTop from '@utils/navigation/usePopToTop'
|
||||||
|
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
|
|
||||||
|
const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({
|
||||||
|
navigation
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('screenTabs')
|
||||||
|
|
||||||
|
const { data: lists } = useListsQuery()
|
||||||
|
|
||||||
|
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerTitle: () => (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<HeaderCenter
|
||||||
|
dropdown={(lists?.length ?? 0) > 0}
|
||||||
|
content={
|
||||||
|
queryKey[1].page === 'List' && queryKey[1].list?.length
|
||||||
|
? lists?.find(list => list.id === queryKey[1].list)?.title
|
||||||
|
: t('tabs.local.name')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{lists?.length
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: 'default',
|
||||||
|
item: {
|
||||||
|
onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]),
|
||||||
|
disabled: queryKey[1].page === 'Following',
|
||||||
|
destructive: false,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
title: t('tabs.local.name'),
|
||||||
|
icon: ''
|
||||||
|
},
|
||||||
|
...lists?.map(list => ({
|
||||||
|
key: list.id,
|
||||||
|
item: {
|
||||||
|
onSelect: () => setQueryKey(['Timeline', { page: 'List', list: list.id }]),
|
||||||
|
disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id,
|
||||||
|
destructive: false,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
title: list.title,
|
||||||
|
icon: ''
|
||||||
|
}))
|
||||||
|
].map(menu => (
|
||||||
|
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
||||||
|
<DropdownMenu.ItemTitle children={menu.title} />
|
||||||
|
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))
|
||||||
|
: undefined}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
),
|
||||||
|
headerRight: () => (
|
||||||
|
<HeaderRight
|
||||||
|
accessibilityLabel={t('common.search.accessibilityLabel')}
|
||||||
|
accessibilityHint={t('common.search.accessibilityHint')}
|
||||||
|
content='Search'
|
||||||
|
onPress={() => navigation.navigate('Tab-Shared-Search')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
usePopToTop()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Timeline
|
||||||
|
queryKey={queryKey}
|
||||||
|
lookback='Following'
|
||||||
|
customProps={{
|
||||||
|
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Root
|
18
src/screens/Tabs/Local/index.tsx
Normal file
18
src/screens/Tabs/Local/index.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import React from 'react'
|
||||||
|
import TabShared from '../Shared'
|
||||||
|
import Root from './Root'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<TabLocalStackParamList>()
|
||||||
|
|
||||||
|
const TabLocal: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||||
|
<Stack.Screen name='Tab-Local-Root' component={Root} />
|
||||||
|
{TabShared({ Stack })}
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabLocal
|
@ -1,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
|
|
71
src/screens/Tabs/Me/FollowedTags.tsx
Normal file
71
src/screens/Tabs/Me/FollowedTags.tsx
Normal 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
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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' })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -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'
|
||||||
|
@ -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 = () => {
|
||||||
|
159
src/screens/Tabs/Me/index.tsx
Normal file
159
src/screens/Tabs/Me/index.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { HeaderLeft } from '@components/Header'
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
|
import { TabMeStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import TabMeBookmarks from './Bookmarks'
|
||||||
|
import TabMeConversations from './Cconversations'
|
||||||
|
import TabMeFavourites from './Favourites'
|
||||||
|
import TabMeFollowedTags from './FollowedTags'
|
||||||
|
import TabMeList from './List'
|
||||||
|
import TabMeListAccounts from './List/Accounts'
|
||||||
|
import TabMeListEdit from './List/Edit'
|
||||||
|
import TabMeListList from './List/List'
|
||||||
|
import TabMeProfile from './Profile'
|
||||||
|
import TabMePush from './Push'
|
||||||
|
import TabMeRoot from './Root'
|
||||||
|
import TabMeSettings from './Settings'
|
||||||
|
import TabMeSettingsFontsize from './SettingsFontsize'
|
||||||
|
import TabMeSettingsLanguage from './SettingsLanguage'
|
||||||
|
import TabMeSwitch from './Switch'
|
||||||
|
import TabShared from '../Shared'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<TabMeStackParamList>()
|
||||||
|
|
||||||
|
const TabMe: React.FC = () => {
|
||||||
|
const { t } = useTranslation('screenTabs')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Root'
|
||||||
|
component={TabMeRoot}
|
||||||
|
options={{
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
||||||
|
headerShown: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Bookmarks'
|
||||||
|
component={TabMeBookmarks}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.bookmarks.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Conversations'
|
||||||
|
component={TabMeConversations}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.conversations.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Favourites'
|
||||||
|
component={TabMeFavourites}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.favourites.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-FollowedTags'
|
||||||
|
component={TabMeFollowedTags}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.followedTags.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-List'
|
||||||
|
component={TabMeList}
|
||||||
|
options={({ route, navigation }: any) => ({
|
||||||
|
title: t('me.stacks.list.name', { list: route.params.title }),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-List-Accounts'
|
||||||
|
component={TabMeListAccounts}
|
||||||
|
options={({ navigation, route: { params } }) => ({
|
||||||
|
title: t('me.stacks.listAccounts.name', { list: params.title }),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-List-Edit'
|
||||||
|
component={TabMeListEdit}
|
||||||
|
options={{
|
||||||
|
gestureEnabled: false,
|
||||||
|
presentation: 'modal'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-List-List'
|
||||||
|
component={TabMeListList}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.lists.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Profile'
|
||||||
|
component={TabMeProfile}
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: 'modal'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Push'
|
||||||
|
component={TabMePush}
|
||||||
|
options={({ navigation }) => ({
|
||||||
|
title: t('me.stacks.push.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Settings'
|
||||||
|
component={TabMeSettings}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.settings.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Settings-Fontsize'
|
||||||
|
component={TabMeSettingsFontsize}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.fontSize.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Settings-Language'
|
||||||
|
component={TabMeSettingsLanguage}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
title: t('me.stacks.language.name'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Tab-Me-Switch'
|
||||||
|
component={TabMeSwitch}
|
||||||
|
options={({ navigation }) => ({
|
||||||
|
presentation: 'modal',
|
||||||
|
headerShown: true,
|
||||||
|
title: t('me.stacks.switch.name'),
|
||||||
|
headerLeft: () => <HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{TabShared({ Stack })}
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabMe
|
@ -1,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
|
|
138
src/screens/Tabs/Notifications/Filters.tsx
Normal file
138
src/screens/Tabs/Notifications/Filters.tsx
Normal 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
|
61
src/screens/Tabs/Notifications/index.tsx
Normal file
61
src/screens/Tabs/Notifications/index.tsx
Normal 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
|
@ -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
|
|
96
src/screens/Tabs/Public/Root.tsx
Normal file
96
src/screens/Tabs/Public/Root.tsx
Normal 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
|
18
src/screens/Tabs/Public/index.tsx
Normal file
18
src/screens/Tabs/Public/index.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
|
import { TabPublicStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import React from 'react'
|
||||||
|
import TabShared from '../Shared'
|
||||||
|
import Root from './Root'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<TabPublicStackParamList>()
|
||||||
|
|
||||||
|
const TabPublic: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||||
|
<Stack.Screen name='Tab-Public-Root' component={Root} />
|
||||||
|
{TabShared({ Stack })}
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabPublic
|
@ -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'
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
@ -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 } }) => (
|
||||||
|
@ -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,
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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<
|
||||||
|
@ -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
|
||||||
|
19
src/utils/migrations/contexts/v3.ts
Normal file
19
src/utils/migrations/contexts/v3.ts
Normal 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'>
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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':
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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 }]
|
||||||
|
|
||||||
|
@ -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 =
|
||||||
|
@ -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'>>
|
||||||
|
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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',
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
@ -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 }
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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'
|
||||||
|
@ -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 =
|
||||||
| {
|
| {
|
||||||
|
@ -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' }]
|
||||||
|
|
||||||
|
@ -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
Reference in New Issue
Block a user