diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index be1819b1..0e452f3a 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -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 = diff --git a/src/App.tsx b/src/App.tsx index 01275460..2f26fdb8 100644 --- a/src/App.tsx +++ b/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 ( - - - - - - - + + + + + + + + + ) } else { return null diff --git a/src/components/Message.tsx b/src/components/Message.tsx index fe706042..ddcdc5fa 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -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((_, ref) => { const { colors, theme } = useTheme() + const insets = useSafeAreaInsets() return ( ((_, 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, diff --git a/src/helpers/features.json b/src/helpers/features.json index 53d2d982..147b20a4 100644 --- a/src/helpers/features.json +++ b/src/helpers/features.json @@ -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" } ] \ No newline at end of file diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index eb72c7ab..ccef3557 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -310,6 +310,13 @@ "attachments": { "name": "<0 /><1>\"s media" }, + "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" } } } \ No newline at end of file diff --git a/src/screens/AccountSelection.tsx b/src/screens/AccountSelection.tsx index ff5e99aa..98611a07 100644 --- a/src/screens/AccountSelection.tsx +++ b/src/screens/AccountSelection.tsx @@ -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 }) => ( )} - ItemSeparatorComponent={() => ( - - )} + ItemSeparatorComponent={() => } /> ) @@ -99,64 +96,60 @@ const ScreenAccountSelection = ({ const instances = useSelector(getInstances, () => true) return ( - - + - : null} + - {share ? : null} - - {t('content.select_account')} - - - {instances.length - ? instances - .slice() - .sort((a, b) => - `${a.uri}${a.account.acct}`.localeCompare( - `${b.uri}${b.account.acct}` - ) + {t('content.select_account')} + + + {instances.length + ? instances + .slice() + .sort((a, b) => + `${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`) + ) + .map((instance, index) => { + return ( + { + navigationRef.navigate('Screen-Compose', { + type: 'share', + ...share + }) + }} + /> ) - .map((instance, index) => { - return ( - { - navigationRef.navigate('Screen-Compose', { - type: 'share', - ...share - }) - }} - /> - ) - }) - : null} - + }) + : null} - - + + ) } diff --git a/src/screens/Actions.tsx b/src/screens/Actions.tsx index c6db5ffa..54626356 100644 --- a/src/screens/Actions.tsx +++ b/src/screens/Actions.tsx @@ -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 ( - - - { - if (nativeEvent.state === State.ACTIVE) { - dismiss() - } - }} + + { + if (nativeEvent.state === State.ACTIVE) { + dismiss() + } + }} + > + - - - - - {actions()} - - - - - - + + + + {actions()} + + + + + ) } diff --git a/src/screens/ImagesViewer.tsx b/src/screens/ImagesViewer.tsx index 4771d265..f9d3b095 100644 --- a/src/screens/ImagesViewer.tsx +++ b/src/screens/ImagesViewer.tsx @@ -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 ( - + + ) } diff --git a/src/screens/Tabs/Local.tsx b/src/screens/Tabs/Local.tsx index acc03b1a..c5d5ac17 100644 --- a/src/screens/Tabs/Local.tsx +++ b/src/screens/Tabs/Local.tsx @@ -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() @@ -96,7 +96,7 @@ const TabLocal = React.memo( /> )} /> - {TabSharedRoot({ Stack })} + {TabShared({ Stack })} ) }, diff --git a/src/screens/Tabs/Me.tsx b/src/screens/Tabs/Me.tsx index 27ffcc1f..d7d2ac86 100644 --- a/src/screens/Tabs/Me.tsx +++ b/src/screens/Tabs/Me.tsx @@ -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() @@ -187,7 +187,7 @@ const TabMe = React.memo( })} /> - {TabSharedRoot({ Stack })} + {TabShared({ Stack })} ) }, diff --git a/src/screens/Tabs/Notifications.tsx b/src/screens/Tabs/Notifications.tsx index da04b603..e3621e49 100644 --- a/src/screens/Tabs/Notifications.tsx +++ b/src/screens/Tabs/Notifications.tsx @@ -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() @@ -65,7 +65,7 @@ const TabNotifications = React.memo( children={children} options={screenOptionsRoot} /> - {TabSharedRoot({ Stack })} + {TabShared({ Stack })} ) }, diff --git a/src/screens/Tabs/Public.tsx b/src/screens/Tabs/Public.tsx index 8c4a7bf7..bd493b17 100644 --- a/src/screens/Tabs/Public.tsx +++ b/src/screens/Tabs/Public.tsx @@ -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() @@ -107,7 +107,7 @@ const TabPublic = React.memo( return ( - {TabSharedRoot({ Stack })} + {TabShared({ Stack })} ) }, diff --git a/src/screens/Tabs/Shared/Hashtag.tsx b/src/screens/Tabs/Shared/Hashtag.tsx index 93cd5061..e905e8a7 100644 --- a/src/screens/Tabs/Shared/Hashtag.tsx +++ b/src/screens/Tabs/Shared/Hashtag.tsx @@ -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> = ({ + 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: () => ( + + typeof data?.following === 'boolean' && + mutation.mutate({ tag: hashtag, type: 'follow', to: !data.following }) + } + /> + ) + }) + }, [canFollowTags, data?.following, isFetching]) + return ( ( - - ) + renderItem: ({ item }) => }} /> ) diff --git a/src/screens/Tabs/Shared/Root.tsx b/src/screens/Tabs/Shared/index.tsx similarity index 83% rename from src/screens/Tabs/Shared/Root.tsx rename to src/screens/Tabs/Shared/index.tsx index ec2ff51a..a98e71c9 100644 --- a/src/screens/Tabs/Shared/Root.tsx +++ b/src/screens/Tabs/Shared/index.tsx @@ -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 -}) => { +const TabShared = ({ Stack }: { Stack: ReturnType }) => { const { colors, mode } = useTheme() const { t } = useTranslation('screenTabs') @@ -50,9 +46,7 @@ const TabSharedRoot = ({ backgroundColor: `rgba(255, 255, 255, 0)` }, title: '', - headerLeft: () => ( - navigation.goBack()} background /> - ), + headerLeft: () => navigation.goBack()} background />, headerRight: () => { const actions: ContextMenuAction[] = [] @@ -77,13 +71,10 @@ const TabSharedRoot = ({ dropdownMenuMode > {}} 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: () => ( - navigation.goBack()} /> - ) + headerLeft: () => 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 ( - 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') }} /> ( - + ) }) })} @@ -244,4 +219,4 @@ const TabSharedRoot = ({ ) } -export default TabSharedRoot +export default TabShared diff --git a/src/utils/queryHooks/tags.ts b/src/utils/queryHooks/tags.ts new file mode 100644 index 00000000..d7fe37bc --- /dev/null +++ b/src/utils/queryHooks/tags.ts @@ -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 +}) => { + const queryKey: QueryKeyFollowedTags = ['FollowedTags'] + return useQuery( + queryKey, + async ({ pageParam }: QueryFunctionContext) => { + const params: { [key: string]: string } = { ...pageParam } + const res = await apiInstance({ method: 'get', url: `followed_tags`, params }) + return res.body + }, + options + ) +} + +type QueryKeyTags = ['Tags', { tag: string }] +const queryFunction = ({ queryKey }: QueryFunctionContext) => { + const { tag } = queryKey[1] + + return apiInstance({ method: 'get', url: `tags/${tag}` }).then(res => res.body) +} +const useTagsQuery = ({ + options, + ...queryKeyParams +}: QueryKeyTags[1] & { + options?: UseQueryOptions +}) => { + 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 } diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts index 48f56887..8747943f 100644 --- a/src/utils/slices/instancesSlice.ts +++ b/src/utils/slices/instancesSlice.ts @@ -29,10 +29,7 @@ const instancesSlice = createSlice({ name: 'instances', initialState: instancesInitialState, reducers: { - updateInstanceActive: ( - { instances }, - action: PayloadAction - ) => { + updateInstanceActive: ({ instances }, action: PayloadAction) => { instances = instances.map(instance => { instance.active = instance.url === action.payload.url && @@ -43,9 +40,7 @@ const instancesSlice = createSlice({ }, updateInstanceAccount: ( { instances }, - action: PayloadAction< - Pick - > + action: PayloadAction> ) => { 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 - ) => { + updateInstanceDraft: ({ instances }, action: PayloadAction) => { 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 - ) => { + removeInstanceDraft: ({ instances }, action: PayloadAction) => { 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 ) => { 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,