mirror of https://github.com/tooot-app/app
Fixed #471
This commit is contained in:
parent
fbfae52627
commit
18e7262f6f
|
@ -475,7 +475,8 @@ declare namespace Mastodon {
|
|||
// Base
|
||||
name: string
|
||||
url: string
|
||||
// history: types
|
||||
history: { day: string; accounts: string; uses: string }[]
|
||||
following: boolean // Since v4.0
|
||||
}
|
||||
|
||||
type WebSocketStream =
|
||||
|
|
17
src/App.tsx
17
src/App.tsx
|
@ -20,6 +20,7 @@ import * as SplashScreen from 'expo-splash-screen'
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { LogBox, Platform } from 'react-native'
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { enableFreeze } from 'react-native-screens'
|
||||
import { QueryClientProvider } from 'react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
|
@ -95,13 +96,15 @@ const App: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<ActionSheetProvider>
|
||||
<AccessibilityManager>
|
||||
<ThemeManager>
|
||||
<Screens localCorrupt={localCorrupt} />
|
||||
</ThemeManager>
|
||||
</AccessibilityManager>
|
||||
</ActionSheetProvider>
|
||||
<SafeAreaProvider>
|
||||
<ActionSheetProvider>
|
||||
<AccessibilityManager>
|
||||
<ThemeManager>
|
||||
<Screens localCorrupt={localCorrupt} />
|
||||
</ThemeManager>
|
||||
</AccessibilityManager>
|
||||
</ActionSheetProvider>
|
||||
</SafeAreaProvider>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
|
|
|
@ -4,10 +4,8 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import { getColors, Theme } from '@utils/styles/themes'
|
||||
import React, { RefObject } from 'react'
|
||||
import { AccessibilityInfo } from 'react-native'
|
||||
import FlashMessage, {
|
||||
hideMessage,
|
||||
showMessage
|
||||
} from 'react-native-flash-message'
|
||||
import FlashMessage, { hideMessage, showMessage } from 'react-native-flash-message'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import haptics from './haptics'
|
||||
|
||||
const displayMessage = ({
|
||||
|
@ -112,6 +110,7 @@ const removeMessage = () => {
|
|||
|
||||
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<FlashMessage
|
||||
|
@ -125,7 +124,8 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
|||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
|
||||
shadowRadius: 4,
|
||||
paddingRight: StyleConstants.Spacing.M * 2
|
||||
paddingRight: StyleConstants.Spacing.M * 2,
|
||||
marginTop: insets.top
|
||||
}}
|
||||
titleStyle={{
|
||||
color: colors.primaryDefault,
|
||||
|
|
|
@ -23,5 +23,10 @@
|
|||
"feature": "notification_types_positive_filter",
|
||||
"version": 3.5,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
||||
},
|
||||
{
|
||||
"feature": "follow_tags",
|
||||
"version": 4.0,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v4.0.0"
|
||||
}
|
||||
]
|
|
@ -310,6 +310,13 @@
|
|||
"attachments": {
|
||||
"name": "<0 /><1>\"s media</1>"
|
||||
},
|
||||
"hashtag": {
|
||||
"follow": "Follow",
|
||||
"unfollow": "Unfollow"
|
||||
},
|
||||
"history": {
|
||||
"name": "Edit History"
|
||||
},
|
||||
"search": {
|
||||
"header": {
|
||||
"prefix": "Searching",
|
||||
|
@ -346,9 +353,6 @@
|
|||
"reblogged_by": "{{count}} boosted",
|
||||
"favourited_by": "{{count}} favourited"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"name": "Edit History"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import * as VideoThumbnails from 'expo-video-thumbnails'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlatList, Image, ScrollView, View } from 'react-native'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const Share = ({
|
||||
|
@ -76,9 +75,7 @@ const Share = ({
|
|||
renderItem={({ item }) => (
|
||||
<Image source={{ uri: item }} style={{ width: 88, height: 88 }} />
|
||||
)}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View style={{ width: StyleConstants.Spacing.S }} />
|
||||
)}
|
||||
ItemSeparatorComponent={() => <View style={{ width: StyleConstants.Spacing.S }} />}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
@ -99,64 +96,60 @@ const ScreenAccountSelection = ({
|
|||
const instances = useSelector(getInstances, () => true)
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<ScrollView
|
||||
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
|
||||
keyboardShouldPersistTaps='always'
|
||||
<ScrollView
|
||||
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
<View
|
||||
{share ? <Share {...share} /> : null}
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
fontWeight='Bold'
|
||||
style={{
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
textAlign: 'center',
|
||||
marginTop: StyleConstants.Spacing.L,
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{share ? <Share {...share} /> : null}
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
fontWeight='Bold'
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: StyleConstants.Spacing.L,
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('content.select_account')}
|
||||
</CustomText>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
{t('content.select_account')}
|
||||
</CustomText>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
additionalActions={() => {
|
||||
navigationRef.navigate('Screen-Compose', {
|
||||
type: 'share',
|
||||
...share
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
.map((instance, index) => {
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
additionalActions={() => {
|
||||
navigationRef.navigate('Screen-Compose', {
|
||||
type: 'share',
|
||||
...share
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||
import {
|
||||
PanGestureHandler,
|
||||
State,
|
||||
TapGestureHandler
|
||||
} from 'react-native-gesture-handler'
|
||||
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
|
@ -17,10 +13,7 @@ import Animated, {
|
|||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated'
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
useSafeAreaInsets
|
||||
} from 'react-native-safe-area-context'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import ActionsAltText from './Actions/AltText'
|
||||
import ActionsNotificationsFilter from './Actions/NotificationsFilter'
|
||||
|
||||
|
@ -39,12 +32,7 @@ const ScreenActions = ({
|
|||
}, [])
|
||||
const styleTop = useAnimatedStyle(() => {
|
||||
return {
|
||||
bottom: interpolate(
|
||||
panY.value,
|
||||
[0, screenHeight],
|
||||
[0, -screenHeight],
|
||||
Extrapolate.CLAMP
|
||||
)
|
||||
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
||||
}
|
||||
})
|
||||
const dismiss = useCallback(() => {
|
||||
|
@ -73,45 +61,35 @@ const ScreenActions = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<Animated.View style={{ flex: 1 }}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
dismiss()
|
||||
}
|
||||
}}
|
||||
<Animated.View style={{ flex: 1 }}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
dismiss()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.overlay, { backgroundColor: colors.backgroundOverlayInvert }]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{ backgroundColor: colors.backgroundOverlayInvert }
|
||||
]}
|
||||
>
|
||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
styleTop,
|
||||
{
|
||||
backgroundColor: colors.backgroundDefault,
|
||||
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.handle,
|
||||
{ backgroundColor: colors.primaryOverlay }
|
||||
]}
|
||||
/>
|
||||
{actions()}
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</TapGestureHandler>
|
||||
</Animated.View>
|
||||
</SafeAreaProvider>
|
||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
styleTop,
|
||||
{
|
||||
backgroundColor: colors.backgroundDefault,
|
||||
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View style={[styles.handle, { backgroundColor: colors.primaryOverlay }]} />
|
||||
{actions()}
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</TapGestureHandler>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gestu
|
|||
import { LiveTextImageView } from 'react-native-live-text-image-view'
|
||||
import { runOnJS, useSharedValue } from 'react-native-reanimated'
|
||||
import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom'
|
||||
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import saveImage from './ImageViewer/save'
|
||||
|
||||
const ZoomFlatList = createZoomListComponent(FlatList)
|
||||
|
@ -153,7 +153,7 @@ const ScreenImagesViewer = ({
|
|||
)
|
||||
|
||||
return (
|
||||
<SafeAreaProvider style={{ backgroundColor: 'black' }}>
|
||||
<View style={{ backgroundColor: 'black' }}>
|
||||
<StatusBar hidden />
|
||||
<View
|
||||
style={{
|
||||
|
@ -232,7 +232,7 @@ const ScreenImagesViewer = ({
|
|||
})}
|
||||
/>
|
||||
</LongPressGestureHandler>
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import ContextMenu from 'react-native-context-menu-view'
|
||||
import TabSharedRoot from './Shared/Root'
|
||||
import TabShared from './Shared'
|
||||
|
||||
const Stack = createNativeStackNavigator<TabLocalStackParamList>()
|
||||
|
||||
|
@ -96,7 +96,7 @@ const TabLocal = React.memo(
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
{TabSharedRoot({ Stack })}
|
||||
{TabShared({ Stack })}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import TabMeSettings from './Me/Settings'
|
|||
import TabMeSettingsFontsize from './Me/SettingsFontsize'
|
||||
import TabMeSettingsLanguage from './Me/SettingsLanguage'
|
||||
import TabMeSwitch from './Me/Switch'
|
||||
import TabSharedRoot from './Shared/Root'
|
||||
import TabShared from './Shared'
|
||||
|
||||
const Stack = createNativeStackNavigator<TabMeStackParamList>()
|
||||
|
||||
|
@ -187,7 +187,7 @@ const TabMe = React.memo(
|
|||
})}
|
||||
/>
|
||||
|
||||
{TabSharedRoot({ Stack })}
|
||||
{TabShared({ Stack })}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import TabSharedRoot from './Shared/Root'
|
||||
import TabShared from './Shared'
|
||||
|
||||
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
|
||||
|
||||
|
@ -65,7 +65,7 @@ const TabNotifications = React.memo(
|
|||
children={children}
|
||||
options={screenOptionsRoot}
|
||||
/>
|
||||
{TabSharedRoot({ Stack })}
|
||||
{TabShared({ Stack })}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ 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 TabSharedRoot from './Shared/Root'
|
||||
import TabShared from './Shared'
|
||||
|
||||
const Stack = createNativeStackNavigator<TabPublicStackParamList>()
|
||||
|
||||
|
@ -107,7 +107,7 @@ const TabPublic = React.memo(
|
|||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||
<Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} />
|
||||
{TabSharedRoot({ Stack })}
|
||||
{TabShared({ Stack })}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -1,24 +1,78 @@
|
|||
import haptics from '@components/haptics'
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React from 'react'
|
||||
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const TabSharedHashtag: React.FC<
|
||||
TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
|
||||
> = ({
|
||||
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
|
||||
navigation,
|
||||
route: {
|
||||
params: { hashtag }
|
||||
}
|
||||
}) => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
|
||||
const { data, isFetching, refetch } = useTagsQuery({
|
||||
tag: hashtag,
|
||||
options: { enabled: canFollowTags }
|
||||
})
|
||||
const mutation = useTagsMutation({
|
||||
onSuccess: () => {
|
||||
haptics('Success')
|
||||
refetch()
|
||||
},
|
||||
onError: (err: any, { to }) => {
|
||||
displayMessage({
|
||||
theme,
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
useEffect(() => {
|
||||
if (!canFollowTags) return
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<HeaderRight
|
||||
loading={isFetching || mutation.isLoading}
|
||||
type='text'
|
||||
content={data?.following ? t('shared.hashtag.unfollow') : t('shared.hashtag.follow')}
|
||||
onPress={() =>
|
||||
typeof data?.following === 'boolean' &&
|
||||
mutation.mutate({ tag: hashtag, type: 'follow', to: !data.following })
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [canFollowTags, data?.following, isFetching])
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -20,11 +20,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
import { Platform, TextInput, View } from 'react-native'
|
||||
import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view'
|
||||
|
||||
const TabSharedRoot = ({
|
||||
Stack
|
||||
}: {
|
||||
Stack: ReturnType<typeof createNativeStackNavigator>
|
||||
}) => {
|
||||
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
|
||||
const { colors, mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
|
@ -50,9 +46,7 @@ const TabSharedRoot = ({
|
|||
backgroundColor: `rgba(255, 255, 255, 0)`
|
||||
},
|
||||
title: '',
|
||||
headerLeft: () => (
|
||||
<HeaderLeft onPress={() => navigation.goBack()} background />
|
||||
),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />,
|
||||
headerRight: () => {
|
||||
const actions: ContextMenuAction[] = []
|
||||
|
||||
|
@ -77,13 +71,10 @@ const TabSharedRoot = ({
|
|||
dropdownMenuMode
|
||||
>
|
||||
<HeaderRight
|
||||
accessibilityLabel={t(
|
||||
'shared.account.actions.accessibilityLabel',
|
||||
{ user: account.acct }
|
||||
)}
|
||||
accessibilityHint={t(
|
||||
'shared.account.actions.accessibilityHint'
|
||||
)}
|
||||
accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
|
||||
user: account.acct
|
||||
})}
|
||||
accessibilityHint={t('shared.account.actions.accessibilityHint')}
|
||||
content='MoreHorizontal'
|
||||
onPress={() => {}}
|
||||
background
|
||||
|
@ -132,9 +123,7 @@ const TabSharedRoot = ({
|
|||
key='Tab-Shared-Hashtag'
|
||||
name='Tab-Shared-Hashtag'
|
||||
component={TabSharedHashtag}
|
||||
options={({
|
||||
route
|
||||
}: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
|
||||
options={({ route }: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
|
||||
title: `#${decodeURIComponent(route.params.hashtag)}`
|
||||
})}
|
||||
/>
|
||||
|
@ -150,24 +139,16 @@ const TabSharedRoot = ({
|
|||
key='Tab-Shared-Search'
|
||||
name='Tab-Shared-Search'
|
||||
component={TabSharedSearch}
|
||||
options={({
|
||||
navigation
|
||||
}: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
|
||||
options={({ navigation }: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
|
||||
...(Platform.OS === 'ios'
|
||||
? {
|
||||
headerLeft: () => (
|
||||
<HeaderLeft onPress={() => navigation.goBack()} />
|
||||
)
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
}
|
||||
: { headerLeft: () => null }),
|
||||
headerTitle: () => {
|
||||
const onChangeText = debounce(
|
||||
(text: string) => navigation.setParams({ text }),
|
||||
1000,
|
||||
{
|
||||
trailing: true
|
||||
}
|
||||
)
|
||||
const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
|
||||
trailing: true
|
||||
})
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
@ -199,9 +180,7 @@ const TabSharedRoot = ({
|
|||
autoCorrect={false}
|
||||
clearButtonMode='never'
|
||||
keyboardType='web-search'
|
||||
onSubmitEditing={({ nativeEvent: { text } }) =>
|
||||
navigation.setParams({ text })
|
||||
}
|
||||
onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
|
||||
placeholder={t('shared.search.header.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
returnKeyType='go'
|
||||
|
@ -216,9 +195,7 @@ const TabSharedRoot = ({
|
|||
key='Tab-Shared-Toot'
|
||||
name='Tab-Shared-Toot'
|
||||
component={TabSharedToot}
|
||||
options={{
|
||||
title: t('shared.toot.name')
|
||||
}}
|
||||
options={{ title: t('shared.toot.name') }}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
|
@ -233,9 +210,7 @@ const TabSharedRoot = ({
|
|||
title: t(`shared.users.${reference}.${type}`, { count }),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter
|
||||
content={t(`shared.users.${reference}.${type}`, { count })}
|
||||
/>
|
||||
<HeaderCenter content={t(`shared.users.${reference}.${type}`, { count })} />
|
||||
)
|
||||
})
|
||||
})}
|
||||
|
@ -244,4 +219,4 @@ const TabSharedRoot = ({
|
|||
)
|
||||
}
|
||||
|
||||
export default TabSharedRoot
|
||||
export default TabShared
|
|
@ -0,0 +1,59 @@
|
|||
import apiInstance from '@api/instance'
|
||||
import { AxiosError } from 'axios'
|
||||
import {
|
||||
QueryFunctionContext,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions
|
||||
} from 'react-query'
|
||||
|
||||
type QueryKeyFollowedTags = ['FollowedTags']
|
||||
const useFollowedTagsQuery = ({
|
||||
options
|
||||
}: {
|
||||
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
|
||||
}) => {
|
||||
const queryKey: QueryKeyFollowedTags = ['FollowedTags']
|
||||
return useQuery(
|
||||
queryKey,
|
||||
async ({ pageParam }: QueryFunctionContext<QueryKeyFollowedTags>) => {
|
||||
const params: { [key: string]: string } = { ...pageParam }
|
||||
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `followed_tags`, params })
|
||||
return res.body
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
type QueryKeyTags = ['Tags', { tag: string }]
|
||||
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
|
||||
const { tag } = queryKey[1]
|
||||
|
||||
return apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` }).then(res => res.body)
|
||||
}
|
||||
const useTagsQuery = ({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKeyTags[1] & {
|
||||
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
|
||||
}) => {
|
||||
const queryKey: QueryKeyTags = ['Tags', { ...queryKeyParams }]
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
}
|
||||
|
||||
type MutationVarsAnnouncement = { tag: string; type: 'follow'; to: boolean }
|
||||
const mutationFunction = async ({ tag, type, to }: MutationVarsAnnouncement) => {
|
||||
switch (type) {
|
||||
case 'follow':
|
||||
return apiInstance<{}>({
|
||||
method: 'post',
|
||||
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
|
||||
})
|
||||
}
|
||||
}
|
||||
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
|
||||
return useMutation(mutationFunction, options)
|
||||
}
|
||||
|
||||
export { useFollowedTagsQuery, useTagsQuery, useTagsMutation }
|
|
@ -29,10 +29,7 @@ const instancesSlice = createSlice({
|
|||
name: 'instances',
|
||||
initialState: instancesInitialState,
|
||||
reducers: {
|
||||
updateInstanceActive: (
|
||||
{ instances },
|
||||
action: PayloadAction<InstanceLatest>
|
||||
) => {
|
||||
updateInstanceActive: ({ instances }, action: PayloadAction<InstanceLatest>) => {
|
||||
instances = instances.map(instance => {
|
||||
instance.active =
|
||||
instance.url === action.payload.url &&
|
||||
|
@ -43,9 +40,7 @@ const instancesSlice = createSlice({
|
|||
},
|
||||
updateInstanceAccount: (
|
||||
{ instances },
|
||||
action: PayloadAction<
|
||||
Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>
|
||||
>
|
||||
action: PayloadAction<Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>>
|
||||
) => {
|
||||
const activeIndex = findInstanceActive(instances)
|
||||
instances[activeIndex].account = {
|
||||
|
@ -60,10 +55,7 @@ const instancesSlice = createSlice({
|
|||
const activeIndex = findInstanceActive(instances)
|
||||
instances[activeIndex].notifications_filter = action.payload
|
||||
},
|
||||
updateInstanceDraft: (
|
||||
{ instances },
|
||||
action: PayloadAction<ComposeStateDraft>
|
||||
) => {
|
||||
updateInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft>) => {
|
||||
const activeIndex = findInstanceActive(instances)
|
||||
const draftIndex = instances[activeIndex].drafts.findIndex(
|
||||
({ timestamp }) => timestamp === action.payload.timestamp
|
||||
|
@ -74,10 +66,7 @@ const instancesSlice = createSlice({
|
|||
instances[activeIndex].drafts[draftIndex] = action.payload
|
||||
}
|
||||
},
|
||||
removeInstanceDraft: (
|
||||
{ instances },
|
||||
action: PayloadAction<ComposeStateDraft['timestamp']>
|
||||
) => {
|
||||
removeInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft['timestamp']>) => {
|
||||
const activeIndex = findInstanceActive(instances)
|
||||
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
|
||||
draft => draft.timestamp !== action.payload
|
||||
|
@ -126,9 +115,7 @@ const instancesSlice = createSlice({
|
|||
action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']>
|
||||
) => {
|
||||
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
|
||||
const calculateScore = (
|
||||
emoji: InstanceLatest['frequentEmojis'][0]
|
||||
): number => {
|
||||
const calculateScore = (emoji: InstanceLatest['frequentEmojis'][0]): number => {
|
||||
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
|
||||
var score = emoji.count + 1
|
||||
var order = Math.log(Math.max(score, 1)) / Math.LN10
|
||||
|
@ -137,9 +124,7 @@ const instancesSlice = createSlice({
|
|||
}
|
||||
const activeIndex = findInstanceActive(instances)
|
||||
const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex(
|
||||
e =>
|
||||
e.emoji.shortcode === action.payload.shortcode &&
|
||||
e.emoji.url === action.payload.url
|
||||
e => e.emoji.shortcode === action.payload.shortcode && e.emoji.url === action.payload.url
|
||||
)
|
||||
let newEmojisSort: InstanceLatest['frequentEmojis']
|
||||
if (foundEmojiIndex > -1) {
|
||||
|
@ -147,11 +132,11 @@ const instancesSlice = createSlice({
|
|||
.map((e, i) =>
|
||||
i === foundEmojiIndex
|
||||
? {
|
||||
...e,
|
||||
score: calculateScore(e),
|
||||
count: e.count + 1,
|
||||
lastUsed: new Date().getTime()
|
||||
}
|
||||
...e,
|
||||
score: calculateScore(e),
|
||||
count: e.count + 1,
|
||||
lastUsed: new Date().getTime()
|
||||
}
|
||||
: e
|
||||
)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
@ -218,8 +203,7 @@ const instancesSlice = createSlice({
|
|||
return true
|
||||
}
|
||||
})
|
||||
state.instances.length &&
|
||||
(state.instances[state.instances.length - 1].active = true)
|
||||
state.instances.length && (state.instances[state.instances.length - 1].active = true)
|
||||
|
||||
analytics('logout')
|
||||
})
|
||||
|
@ -250,8 +234,7 @@ const instancesSlice = createSlice({
|
|||
.addCase(updateConfiguration.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].version = action.payload?.version || '0'
|
||||
state.instances[activeIndex].configuration =
|
||||
action.payload.configuration
|
||||
state.instances[activeIndex].configuration = action.payload.configuration
|
||||
})
|
||||
.addCase(updateConfiguration.rejected, (_, action) => {
|
||||
console.error(action.error)
|
||||
|
@ -291,22 +274,16 @@ const instancesSlice = createSlice({
|
|||
// Update Instance Push Individual Alert
|
||||
.addCase(updateInstancePushAlert.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = false
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
|
||||
state.instances[activeIndex].push.alerts = action.payload
|
||||
})
|
||||
.addCase(updateInstancePushAlert.rejected, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = false
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
|
||||
})
|
||||
.addCase(updateInstancePushAlert.pending, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = true
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = true
|
||||
})
|
||||
|
||||
// Check if frequently used emojis still exist
|
||||
|
@ -317,8 +294,7 @@ const instancesSlice = createSlice({
|
|||
activeIndex
|
||||
].frequentEmojis?.filter(emoji => {
|
||||
return action.payload?.find(
|
||||
e =>
|
||||
e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
|
||||
e => e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -331,8 +307,7 @@ const instancesSlice = createSlice({
|
|||
export const getInstanceActive = ({ instances: { instances } }: RootState) =>
|
||||
findInstanceActive(instances)
|
||||
|
||||
export const getInstances = ({ instances: { instances } }: RootState) =>
|
||||
instances
|
||||
export const getInstances = ({ instances: { instances } }: RootState) => instances
|
||||
|
||||
export const getInstance = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]
|
||||
|
@ -350,42 +325,30 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) =>
|
|||
instances[findInstanceActive(instances)]?.version
|
||||
export const checkInstanceFeature =
|
||||
(feature: string) =>
|
||||
({ instances: { instances } }: RootState): Boolean => {
|
||||
return (
|
||||
features
|
||||
.filter(f => f.feature === feature)
|
||||
.filter(
|
||||
f =>
|
||||
parseFloat(instances[findInstanceActive(instances)]?.version) >=
|
||||
f.version
|
||||
)?.length > 0
|
||||
)
|
||||
}
|
||||
({ instances: { instances } }: RootState): boolean => {
|
||||
return (
|
||||
features
|
||||
.filter(f => f.feature === feature)
|
||||
.filter(f => parseFloat(instances[findInstanceActive(instances)]?.version) >= f.version)
|
||||
?.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
/* Get Instance Configuration */
|
||||
export const getInstanceConfigurationStatusMaxChars = ({
|
||||
instances: { instances }
|
||||
}: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||
.max_characters || 500
|
||||
export const getInstanceConfigurationStatusMaxChars = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses.max_characters || 500
|
||||
|
||||
export const getInstanceConfigurationStatusMaxAttachments = ({
|
||||
instances: { instances }
|
||||
}: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||
.max_media_attachments || 4
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses.max_media_attachments || 4
|
||||
|
||||
export const getInstanceConfigurationStatusCharsURL = ({
|
||||
instances: { instances }
|
||||
}: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||
.characters_reserved_per_url || 23
|
||||
export const getInstanceConfigurationStatusCharsURL = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.statuses.characters_reserved_per_url ||
|
||||
23
|
||||
|
||||
export const getInstanceConfigurationMediaAttachments = ({
|
||||
instances: { instances }
|
||||
}: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration
|
||||
?.media_attachments || {
|
||||
export const getInstanceConfigurationMediaAttachments = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.media_attachments || {
|
||||
supported_mime_types: [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
|
@ -418,9 +381,7 @@ export const getInstanceConfigurationMediaAttachments = ({
|
|||
video_matrix_limit: 2304000
|
||||
}
|
||||
|
||||
export const getInstanceConfigurationPoll = ({
|
||||
instances: { instances }
|
||||
}: RootState) =>
|
||||
export const getInstanceConfigurationPoll = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.configuration?.polls || {
|
||||
max_options: 4,
|
||||
max_characters_per_option: 50,
|
||||
|
@ -432,16 +393,14 @@ export const getInstanceConfigurationPoll = ({
|
|||
export const getInstanceAccount = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.account
|
||||
|
||||
export const getInstanceNotificationsFilter = ({
|
||||
instances: { instances }
|
||||
}: RootState) => instances[findInstanceActive(instances)]?.notifications_filter
|
||||
export const getInstanceNotificationsFilter = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.notifications_filter
|
||||
|
||||
export const getInstancePush = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.push
|
||||
|
||||
export const getInstanceTimelinesLookback = ({
|
||||
instances: { instances }
|
||||
}: RootState) => instances[findInstanceActive(instances)]?.timelinesLookback
|
||||
export const getInstanceTimelinesLookback = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.timelinesLookback
|
||||
|
||||
export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.mePage
|
||||
|
@ -449,9 +408,8 @@ export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
|
|||
export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.drafts
|
||||
|
||||
export const getInstanceFrequentEmojis = ({
|
||||
instances: { instances }
|
||||
}: RootState) => instances[findInstanceActive(instances)]?.frequentEmojis
|
||||
export const getInstanceFrequentEmojis = ({ instances: { instances } }: RootState) =>
|
||||
instances[findInstanceActive(instances)]?.frequentEmojis
|
||||
|
||||
export const {
|
||||
updateInstanceActive,
|
||||
|
|
Loading…
Reference in New Issue