This commit is contained in:
xmflsct 2022-11-20 16:14:08 +01:00
parent fbfae52627
commit 18e7262f6f
16 changed files with 297 additions and 267 deletions

View File

@ -475,7 +475,8 @@ declare namespace Mastodon {
// Base // Base
name: string name: string
url: string url: string
// history: types history: { day: string; accounts: string; uses: string }[]
following: boolean // Since v4.0
} }
type WebSocketStream = type WebSocketStream =

View File

@ -20,6 +20,7 @@ import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { LogBox, Platform } from 'react-native' 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 { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { QueryClientProvider } from 'react-query' import { QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
@ -95,13 +96,15 @@ const App: React.FC = () => {
} }
return ( return (
<ActionSheetProvider> <SafeAreaProvider>
<AccessibilityManager> <ActionSheetProvider>
<ThemeManager> <AccessibilityManager>
<Screens localCorrupt={localCorrupt} /> <ThemeManager>
</ThemeManager> <Screens localCorrupt={localCorrupt} />
</AccessibilityManager> </ThemeManager>
</ActionSheetProvider> </AccessibilityManager>
</ActionSheetProvider>
</SafeAreaProvider>
) )
} else { } else {
return null return null

View File

@ -4,10 +4,8 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { getColors, Theme } from '@utils/styles/themes' import { getColors, Theme } from '@utils/styles/themes'
import React, { RefObject } from 'react' import React, { RefObject } from 'react'
import { AccessibilityInfo } from 'react-native' import { AccessibilityInfo } from 'react-native'
import FlashMessage, { import FlashMessage, { hideMessage, showMessage } from 'react-native-flash-message'
hideMessage, import { useSafeAreaInsets } from 'react-native-safe-area-context'
showMessage
} from 'react-native-flash-message'
import haptics from './haptics' import haptics from './haptics'
const displayMessage = ({ const displayMessage = ({
@ -112,6 +110,7 @@ const removeMessage = () => {
const Message = React.forwardRef<FlashMessage>((_, ref) => { const Message = React.forwardRef<FlashMessage>((_, ref) => {
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const insets = useSafeAreaInsets()
return ( return (
<FlashMessage <FlashMessage
@ -125,7 +124,8 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: theme === 'light' ? 0.16 : 0.24, shadowOpacity: theme === 'light' ? 0.16 : 0.24,
shadowRadius: 4, shadowRadius: 4,
paddingRight: StyleConstants.Spacing.M * 2 paddingRight: StyleConstants.Spacing.M * 2,
marginTop: insets.top
}} }}
titleStyle={{ titleStyle={{
color: colors.primaryDefault, color: colors.primaryDefault,

View File

@ -23,5 +23,10 @@
"feature": "notification_types_positive_filter", "feature": "notification_types_positive_filter",
"version": 3.5, "version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0" "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"
} }
] ]

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"s media</1>" "name": "<0 /><1>\"s media</1>"
}, },
"hashtag": {
"follow": "Follow",
"unfollow": "Unfollow"
},
"history": {
"name": "Edit History"
},
"search": { "search": {
"header": { "header": {
"prefix": "Searching", "prefix": "Searching",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boosted", "reblogged_by": "{{count}} boosted",
"favourited_by": "{{count}} favourited" "favourited_by": "{{count}} favourited"
} }
},
"history": {
"name": "Edit History"
} }
} }
} }

View File

@ -9,7 +9,6 @@ import * as VideoThumbnails from 'expo-video-thumbnails'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, Image, ScrollView, View } from 'react-native' import { FlatList, Image, ScrollView, View } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const Share = ({ const Share = ({
@ -76,9 +75,7 @@ const Share = ({
renderItem={({ item }) => ( renderItem={({ item }) => (
<Image source={{ uri: item }} style={{ width: 88, height: 88 }} /> <Image source={{ uri: item }} style={{ width: 88, height: 88 }} />
)} )}
ItemSeparatorComponent={() => ( ItemSeparatorComponent={() => <View style={{ width: StyleConstants.Spacing.S }} />}
<View style={{ width: StyleConstants.Spacing.S }} />
)}
/> />
</View> </View>
) )
@ -99,64 +96,60 @@ const ScreenAccountSelection = ({
const instances = useSelector(getInstances, () => true) const instances = useSelector(getInstances, () => true)
return ( return (
<SafeAreaProvider> <ScrollView
<ScrollView style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
style={{ marginBottom: StyleConstants.Spacing.L * 2 }} keyboardShouldPersistTaps='always'
keyboardShouldPersistTaps='always' >
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
> >
<View {share ? <Share {...share} /> : null}
<CustomText
fontStyle='M'
fontWeight='Bold'
style={{ style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding textAlign: 'center',
marginTop: StyleConstants.Spacing.L,
marginBottom: StyleConstants.Spacing.S,
color: colors.primaryDefault
}} }}
> >
{share ? <Share {...share} /> : null} {t('content.select_account')}
<CustomText </CustomText>
fontStyle='M' <View
fontWeight='Bold' style={{
style={{ flex: 1,
textAlign: 'center', flexDirection: 'row',
marginTop: StyleConstants.Spacing.L, flexWrap: 'wrap',
marginBottom: StyleConstants.Spacing.S, marginTop: StyleConstants.Spacing.M
color: colors.primaryDefault }}
}} >
> {instances.length
{t('content.select_account')} ? instances
</CustomText> .slice()
<View .sort((a, b) =>
style={{ `${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
flex: 1, )
flexDirection: 'row', .map((instance, index) => {
flexWrap: 'wrap', return (
marginTop: StyleConstants.Spacing.M <AccountButton
}} key={index}
> instance={instance}
{instances.length additionalActions={() => {
? instances navigationRef.navigate('Screen-Compose', {
.slice() type: 'share',
.sort((a, b) => ...share
`${a.uri}${a.account.acct}`.localeCompare( })
`${b.uri}${b.account.acct}` }}
) />
) )
.map((instance, index) => { })
return ( : null}
<AccountButton
key={index}
instance={instance}
additionalActions={() => {
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}}
/>
)
})
: null}
</View>
</View> </View>
</ScrollView> </View>
</SafeAreaProvider> </ScrollView>
) )
} }

View File

@ -3,11 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native' import { Dimensions, StyleSheet, View } from 'react-native'
import { import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
PanGestureHandler,
State,
TapGestureHandler
} from 'react-native-gesture-handler'
import Animated, { import Animated, {
Extrapolate, Extrapolate,
interpolate, interpolate,
@ -17,10 +13,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming withTiming
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { import { useSafeAreaInsets } from 'react-native-safe-area-context'
SafeAreaProvider,
useSafeAreaInsets
} from 'react-native-safe-area-context'
import ActionsAltText from './Actions/AltText' import ActionsAltText from './Actions/AltText'
import ActionsNotificationsFilter from './Actions/NotificationsFilter' import ActionsNotificationsFilter from './Actions/NotificationsFilter'
@ -39,12 +32,7 @@ const ScreenActions = ({
}, []) }, [])
const styleTop = useAnimatedStyle(() => { const styleTop = useAnimatedStyle(() => {
return { return {
bottom: interpolate( bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
panY.value,
[0, screenHeight],
[0, -screenHeight],
Extrapolate.CLAMP
)
} }
}) })
const dismiss = useCallback(() => { const dismiss = useCallback(() => {
@ -73,45 +61,35 @@ const ScreenActions = ({
} }
return ( return (
<SafeAreaProvider> <Animated.View style={{ flex: 1 }}>
<Animated.View style={{ flex: 1 }}> <TapGestureHandler
<TapGestureHandler onHandlerStateChange={({ nativeEvent }) => {
onHandlerStateChange={({ nativeEvent }) => { if (nativeEvent.state === State.ACTIVE) {
if (nativeEvent.state === State.ACTIVE) { dismiss()
dismiss() }
} }}
}} >
<Animated.View
style={[styles.overlay, { backgroundColor: colors.backgroundOverlayInvert }]}
> >
<Animated.View <PanGestureHandler onGestureEvent={onGestureEvent}>
style={[ <Animated.View
styles.overlay, style={[
{ backgroundColor: colors.backgroundOverlayInvert } styles.container,
]} styleTop,
> {
<PanGestureHandler onGestureEvent={onGestureEvent}> backgroundColor: colors.backgroundDefault,
<Animated.View paddingBottom: insets.bottom || StyleConstants.Spacing.L
style={[ }
styles.container, ]}
styleTop, >
{ <View style={[styles.handle, { backgroundColor: colors.primaryOverlay }]} />
backgroundColor: colors.backgroundDefault, {actions()}
paddingBottom: insets.bottom || StyleConstants.Spacing.L </Animated.View>
} </PanGestureHandler>
]} </Animated.View>
> </TapGestureHandler>
<View </Animated.View>
style={[
styles.handle,
{ backgroundColor: colors.primaryOverlay }
]}
/>
{actions()}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</TapGestureHandler>
</Animated.View>
</SafeAreaProvider>
) )
} }

View File

@ -20,7 +20,7 @@ import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gestu
import { LiveTextImageView } from 'react-native-live-text-image-view' import { LiveTextImageView } from 'react-native-live-text-image-view'
import { runOnJS, useSharedValue } from 'react-native-reanimated' import { runOnJS, useSharedValue } from 'react-native-reanimated'
import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom' 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' import saveImage from './ImageViewer/save'
const ZoomFlatList = createZoomListComponent(FlatList) const ZoomFlatList = createZoomListComponent(FlatList)
@ -153,7 +153,7 @@ const ScreenImagesViewer = ({
) )
return ( return (
<SafeAreaProvider style={{ backgroundColor: 'black' }}> <View style={{ backgroundColor: 'black' }}>
<StatusBar hidden /> <StatusBar hidden />
<View <View
style={{ style={{
@ -232,7 +232,7 @@ const ScreenImagesViewer = ({
})} })}
/> />
</LongPressGestureHandler> </LongPressGestureHandler>
</SafeAreaProvider> </View>
) )
} }

View File

@ -12,7 +12,7 @@ import React, { 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 ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabLocalStackParamList>() const Stack = createNativeStackNavigator<TabLocalStackParamList>()
@ -96,7 +96,7 @@ const TabLocal = React.memo(
/> />
)} )}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -16,7 +16,7 @@ import TabMeSettings from './Me/Settings'
import TabMeSettingsFontsize from './Me/SettingsFontsize' import TabMeSettingsFontsize from './Me/SettingsFontsize'
import TabMeSettingsLanguage from './Me/SettingsLanguage' import TabMeSettingsLanguage from './Me/SettingsLanguage'
import TabMeSwitch from './Me/Switch' import TabMeSwitch from './Me/Switch'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabMeStackParamList>() const Stack = createNativeStackNavigator<TabMeStackParamList>()
@ -187,7 +187,7 @@ const TabMe = React.memo(
})} })}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>() const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
@ -65,7 +65,7 @@ const TabNotifications = React.memo(
children={children} children={children}
options={screenOptionsRoot} options={screenOptionsRoot}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -12,7 +12,7 @@ import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Dimensions } from 'react-native' import { Dimensions } from 'react-native'
import { TabView } from 'react-native-tab-view' import { TabView } from 'react-native-tab-view'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabPublicStackParamList>() const Stack = createNativeStackNavigator<TabPublicStackParamList>()
@ -107,7 +107,7 @@ const TabPublic = React.memo(
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} /> <Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -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 Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default' import TimelineDefault from '@components/Timeline/Default'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' 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< const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
TabSharedStackScreenProps<'Tab-Shared-Hashtag'> navigation,
> = ({
route: { route: {
params: { hashtag } params: { hashtag }
} }
}) => { }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', 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 ( return (
<Timeline <Timeline
queryKey={queryKey} queryKey={queryKey}
customProps={{ customProps={{
renderItem: ({ item }) => ( renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
<TimelineDefault item={item} queryKey={queryKey} />
)
}} }}
/> />
) )

View File

@ -20,11 +20,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { Platform, TextInput, View } from 'react-native' import { Platform, TextInput, View } from 'react-native'
import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view' import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view'
const TabSharedRoot = ({ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
Stack
}: {
Stack: ReturnType<typeof createNativeStackNavigator>
}) => {
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
@ -50,9 +46,7 @@ const TabSharedRoot = ({
backgroundColor: `rgba(255, 255, 255, 0)` backgroundColor: `rgba(255, 255, 255, 0)`
}, },
title: '', title: '',
headerLeft: () => ( headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />,
<HeaderLeft onPress={() => navigation.goBack()} background />
),
headerRight: () => { headerRight: () => {
const actions: ContextMenuAction[] = [] const actions: ContextMenuAction[] = []
@ -77,13 +71,10 @@ const TabSharedRoot = ({
dropdownMenuMode dropdownMenuMode
> >
<HeaderRight <HeaderRight
accessibilityLabel={t( accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
'shared.account.actions.accessibilityLabel', user: account.acct
{ user: account.acct } })}
)} accessibilityHint={t('shared.account.actions.accessibilityHint')}
accessibilityHint={t(
'shared.account.actions.accessibilityHint'
)}
content='MoreHorizontal' content='MoreHorizontal'
onPress={() => {}} onPress={() => {}}
background background
@ -132,9 +123,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Hashtag' key='Tab-Shared-Hashtag'
name='Tab-Shared-Hashtag' name='Tab-Shared-Hashtag'
component={TabSharedHashtag} component={TabSharedHashtag}
options={({ options={({ route }: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
route
}: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
title: `#${decodeURIComponent(route.params.hashtag)}` title: `#${decodeURIComponent(route.params.hashtag)}`
})} })}
/> />
@ -150,24 +139,16 @@ const TabSharedRoot = ({
key='Tab-Shared-Search' key='Tab-Shared-Search'
name='Tab-Shared-Search' name='Tab-Shared-Search'
component={TabSharedSearch} component={TabSharedSearch}
options={({ options={({ navigation }: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
navigation
}: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
...(Platform.OS === 'ios' ...(Platform.OS === 'ios'
? { ? {
headerLeft: () => ( headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
<HeaderLeft onPress={() => navigation.goBack()} />
)
} }
: { headerLeft: () => null }), : { headerLeft: () => null }),
headerTitle: () => { headerTitle: () => {
const onChangeText = debounce( const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
(text: string) => navigation.setParams({ text }), trailing: true
1000, })
{
trailing: true
}
)
return ( return (
<View <View
style={{ style={{
@ -199,9 +180,7 @@ const TabSharedRoot = ({
autoCorrect={false} autoCorrect={false}
clearButtonMode='never' clearButtonMode='never'
keyboardType='web-search' keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) => onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
navigation.setParams({ text })
}
placeholder={t('shared.search.header.placeholder')} placeholder={t('shared.search.header.placeholder')}
placeholderTextColor={colors.secondary} placeholderTextColor={colors.secondary}
returnKeyType='go' returnKeyType='go'
@ -216,9 +195,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Toot' key='Tab-Shared-Toot'
name='Tab-Shared-Toot' name='Tab-Shared-Toot'
component={TabSharedToot} component={TabSharedToot}
options={{ options={{ title: t('shared.toot.name') }}
title: t('shared.toot.name')
}}
/> />
<Stack.Screen <Stack.Screen
@ -233,9 +210,7 @@ const TabSharedRoot = ({
title: t(`shared.users.${reference}.${type}`, { count }), title: t(`shared.users.${reference}.${type}`, { count }),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => (
<HeaderCenter <HeaderCenter content={t(`shared.users.${reference}.${type}`, { count })} />
content={t(`shared.users.${reference}.${type}`, { count })}
/>
) )
}) })
})} })}
@ -244,4 +219,4 @@ const TabSharedRoot = ({
) )
} }
export default TabSharedRoot export default TabShared

View File

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

View File

@ -29,10 +29,7 @@ const instancesSlice = createSlice({
name: 'instances', name: 'instances',
initialState: instancesInitialState, initialState: instancesInitialState,
reducers: { reducers: {
updateInstanceActive: ( updateInstanceActive: ({ instances }, action: PayloadAction<InstanceLatest>) => {
{ instances },
action: PayloadAction<InstanceLatest>
) => {
instances = instances.map(instance => { instances = instances.map(instance => {
instance.active = instance.active =
instance.url === action.payload.url && instance.url === action.payload.url &&
@ -43,9 +40,7 @@ const instancesSlice = createSlice({
}, },
updateInstanceAccount: ( updateInstanceAccount: (
{ instances }, { instances },
action: PayloadAction< action: PayloadAction<Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>>
Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>
>
) => { ) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].account = { instances[activeIndex].account = {
@ -60,10 +55,7 @@ const instancesSlice = createSlice({
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].notifications_filter = action.payload instances[activeIndex].notifications_filter = action.payload
}, },
updateInstanceDraft: ( updateInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft>) => {
{ instances },
action: PayloadAction<ComposeStateDraft>
) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
const draftIndex = instances[activeIndex].drafts.findIndex( const draftIndex = instances[activeIndex].drafts.findIndex(
({ timestamp }) => timestamp === action.payload.timestamp ({ timestamp }) => timestamp === action.payload.timestamp
@ -74,10 +66,7 @@ const instancesSlice = createSlice({
instances[activeIndex].drafts[draftIndex] = action.payload instances[activeIndex].drafts[draftIndex] = action.payload
} }
}, },
removeInstanceDraft: ( removeInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft['timestamp']>) => {
{ instances },
action: PayloadAction<ComposeStateDraft['timestamp']>
) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter( instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload draft => draft.timestamp !== action.payload
@ -126,9 +115,7 @@ const instancesSlice = createSlice({
action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']> action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']>
) => { ) => {
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
const calculateScore = ( const calculateScore = (emoji: InstanceLatest['frequentEmojis'][0]): number => {
emoji: InstanceLatest['frequentEmojis'][0]
): number => {
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000 var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
var score = emoji.count + 1 var score = emoji.count + 1
var order = Math.log(Math.max(score, 1)) / Math.LN10 var order = Math.log(Math.max(score, 1)) / Math.LN10
@ -137,9 +124,7 @@ const instancesSlice = createSlice({
} }
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex( const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex(
e => e => e.emoji.shortcode === action.payload.shortcode && e.emoji.url === action.payload.url
e.emoji.shortcode === action.payload.shortcode &&
e.emoji.url === action.payload.url
) )
let newEmojisSort: InstanceLatest['frequentEmojis'] let newEmojisSort: InstanceLatest['frequentEmojis']
if (foundEmojiIndex > -1) { if (foundEmojiIndex > -1) {
@ -147,11 +132,11 @@ const instancesSlice = createSlice({
.map((e, i) => .map((e, i) =>
i === foundEmojiIndex i === foundEmojiIndex
? { ? {
...e, ...e,
score: calculateScore(e), score: calculateScore(e),
count: e.count + 1, count: e.count + 1,
lastUsed: new Date().getTime() lastUsed: new Date().getTime()
} }
: e : e
) )
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
@ -218,8 +203,7 @@ const instancesSlice = createSlice({
return true return true
} }
}) })
state.instances.length && state.instances.length && (state.instances[state.instances.length - 1].active = true)
(state.instances[state.instances.length - 1].active = true)
analytics('logout') analytics('logout')
}) })
@ -250,8 +234,7 @@ const instancesSlice = createSlice({
.addCase(updateConfiguration.fulfilled, (state, action) => { .addCase(updateConfiguration.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].version = action.payload?.version || '0' state.instances[activeIndex].version = action.payload?.version || '0'
state.instances[activeIndex].configuration = state.instances[activeIndex].configuration = action.payload.configuration
action.payload.configuration
}) })
.addCase(updateConfiguration.rejected, (_, action) => { .addCase(updateConfiguration.rejected, (_, action) => {
console.error(action.error) console.error(action.error)
@ -291,22 +274,16 @@ const instancesSlice = createSlice({
// Update Instance Push Individual Alert // Update Instance Push Individual Alert
.addCase(updateInstancePushAlert.fulfilled, (state, action) => { .addCase(updateInstancePushAlert.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
action.meta.arg.changed
].loading = false
state.instances[activeIndex].push.alerts = action.payload state.instances[activeIndex].push.alerts = action.payload
}) })
.addCase(updateInstancePushAlert.rejected, (state, action) => { .addCase(updateInstancePushAlert.rejected, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
action.meta.arg.changed
].loading = false
}) })
.addCase(updateInstancePushAlert.pending, (state, action) => { .addCase(updateInstancePushAlert.pending, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = true
action.meta.arg.changed
].loading = true
}) })
// Check if frequently used emojis still exist // Check if frequently used emojis still exist
@ -317,8 +294,7 @@ const instancesSlice = createSlice({
activeIndex activeIndex
].frequentEmojis?.filter(emoji => { ].frequentEmojis?.filter(emoji => {
return action.payload?.find( return action.payload?.find(
e => e => e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
) )
}) })
}) })
@ -331,8 +307,7 @@ const instancesSlice = createSlice({
export const getInstanceActive = ({ instances: { instances } }: RootState) => export const getInstanceActive = ({ instances: { instances } }: RootState) =>
findInstanceActive(instances) findInstanceActive(instances)
export const getInstances = ({ instances: { instances } }: RootState) => export const getInstances = ({ instances: { instances } }: RootState) => instances
instances
export const getInstance = ({ instances: { instances } }: RootState) => export const getInstance = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)] instances[findInstanceActive(instances)]
@ -350,42 +325,30 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.version instances[findInstanceActive(instances)]?.version
export const checkInstanceFeature = export const checkInstanceFeature =
(feature: string) => (feature: string) =>
({ instances: { instances } }: RootState): Boolean => { ({ instances: { instances } }: RootState): boolean => {
return ( return (
features features
.filter(f => f.feature === feature) .filter(f => f.feature === feature)
.filter( .filter(f => parseFloat(instances[findInstanceActive(instances)]?.version) >= f.version)
f => ?.length > 0
parseFloat(instances[findInstanceActive(instances)]?.version) >= )
f.version }
)?.length > 0
)
}
/* Get Instance Configuration */ /* Get Instance Configuration */
export const getInstanceConfigurationStatusMaxChars = ({ export const getInstanceConfigurationStatusMaxChars = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.statuses.max_characters || 500
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses
.max_characters || 500
export const getInstanceConfigurationStatusMaxAttachments = ({ export const getInstanceConfigurationStatusMaxAttachments = ({
instances: { instances } instances: { instances }
}: RootState) => }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses instances[findInstanceActive(instances)]?.configuration?.statuses.max_media_attachments || 4
.max_media_attachments || 4
export const getInstanceConfigurationStatusCharsURL = ({ export const getInstanceConfigurationStatusCharsURL = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.statuses.characters_reserved_per_url ||
}: RootState) => 23
instances[findInstanceActive(instances)]?.configuration?.statuses
.characters_reserved_per_url || 23
export const getInstanceConfigurationMediaAttachments = ({ export const getInstanceConfigurationMediaAttachments = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.media_attachments || {
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration
?.media_attachments || {
supported_mime_types: [ supported_mime_types: [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@ -418,9 +381,7 @@ export const getInstanceConfigurationMediaAttachments = ({
video_matrix_limit: 2304000 video_matrix_limit: 2304000
} }
export const getInstanceConfigurationPoll = ({ export const getInstanceConfigurationPoll = ({ instances: { instances } }: RootState) =>
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.polls || { instances[findInstanceActive(instances)]?.configuration?.polls || {
max_options: 4, max_options: 4,
max_characters_per_option: 50, max_characters_per_option: 50,
@ -432,16 +393,14 @@ export const getInstanceConfigurationPoll = ({
export const getInstanceAccount = ({ instances: { instances } }: RootState) => export const getInstanceAccount = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.account instances[findInstanceActive(instances)]?.account
export const getInstanceNotificationsFilter = ({ export const getInstanceNotificationsFilter = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.notifications_filter
}: RootState) => instances[findInstanceActive(instances)]?.notifications_filter
export const getInstancePush = ({ instances: { instances } }: RootState) => export const getInstancePush = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.push instances[findInstanceActive(instances)]?.push
export const getInstanceTimelinesLookback = ({ export const getInstanceTimelinesLookback = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.timelinesLookback
}: RootState) => instances[findInstanceActive(instances)]?.timelinesLookback
export const getInstanceMePage = ({ instances: { instances } }: RootState) => export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.mePage instances[findInstanceActive(instances)]?.mePage
@ -449,9 +408,8 @@ export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.drafts instances[findInstanceActive(instances)]?.drafts
export const getInstanceFrequentEmojis = ({ export const getInstanceFrequentEmojis = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.frequentEmojis
}: RootState) => instances[findInstanceActive(instances)]?.frequentEmojis
export const { export const {
updateInstanceActive, updateInstanceActive,