Removed image focus as different clients implement this differently
This commit is contained in:
xmflsct 2023-01-23 23:05:25 +01:00
parent 613cf1365c
commit 47d5b02468
16 changed files with 197 additions and 410 deletions

View File

@ -153,8 +153,7 @@
"altText": {
"heading": "Describe media for the visually impaired",
"placeholder": "You can add a description, sometimes called alt-text, to your media so they are accessible to even more people, including those who are blind or visually impaired.\n\nGood descriptions are concise, but present what is in your media accurately enough to understand their context."
},
"imageFocus": "Drag the focus circle to update focus point"
}
}
},
"draftsList": {

View File

@ -1,12 +1,14 @@
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, KeyboardAvoidingView, Platform } from 'react-native'
import { Alert, KeyboardAvoidingView, Platform, ScrollView, TextInput } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import ComposeEditAttachmentRoot from './EditAttachment/Root'
import ComposeContext from './utils/createContext'
const ComposeEditAttachment: React.FC<
@ -17,12 +19,17 @@ const ComposeEditAttachment: React.FC<
params: { index }
}
}) => {
const { colors } = useTheme()
const { t } = useTranslation('screenCompose')
const { composeState } = useContext(ComposeContext)
const { composeState, composeDispatch } = useContext(ComposeContext)
const [isSubmitting, setIsSubmitting] = useState(false)
const theAttachment = composeState.attachments.uploads[index].remote!
const theAttachment = composeState.attachments.uploads[index].remote
if (!theAttachment) {
navigation.goBack()
return null
}
useEffect(() => {
navigation.setOptions({
@ -37,6 +44,12 @@ const ComposeEditAttachment: React.FC<
content='Save'
loading={isSubmitting}
onPress={() => {
if (composeState.type === 'edit') {
composeDispatch({ type: 'attachment/edit', payload: { ...theAttachment } })
navigation.goBack()
return
}
setIsSubmitting(true)
const formData = new FormData()
if (theAttachment.description) {
@ -80,8 +93,53 @@ const ComposeEditAttachment: React.FC<
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<ComposeEditAttachmentRoot index={index} />
<SafeAreaView
style={{ flex: 1, padding: StyleConstants.Spacing.Global.PagePadding }}
edges={['left', 'right', 'bottom']}
>
<ScrollView>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
{t('content.editAttachment.content.altText.heading')}
</CustomText>
<TextInput
style={{
height:
StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.Global.PagePadding,
borderWidth: 1,
borderColor: colors.border,
color: colors.primaryDefault
}}
maxLength={1500}
multiline
onChangeText={e =>
composeDispatch({
type: 'attachment/edit',
payload: {
...theAttachment,
description: e
}
})
}
placeholder={t('content.editAttachment.content.altText.placeholder')}
placeholderTextColor={colors.secondary}
value={theAttachment.description}
/>
<CustomText
fontStyle='S'
style={{
textAlign: 'right',
marginRight: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.M,
color: colors.secondary
}}
>
{theAttachment.description?.length || 0} / 1500
</CustomText>
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
)

View File

@ -1,173 +0,0 @@
import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions, Image, View } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue
} from 'react-native-reanimated'
import ComposeContext from '../utils/createContext'
export interface Props {
index: number
}
const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
const { t } = useTranslation('screenCompose')
const { colors } = useTheme()
const { screenReaderEnabled } = useAccessibility()
const { composeState, composeDispatch } = useContext(ComposeContext)
const theAttachmentRemote = composeState.attachments.uploads[index].remote!
const theAttachmentLocal = composeState.attachments.uploads[index].local
const windowWidth = Dimensions.get('window').width
const imageWidthBase =
theAttachmentRemote?.meta?.original?.aspect < 1
? windowWidth * theAttachmentRemote?.meta?.original?.aspect
: windowWidth
const imageDimensions = {
width: imageWidthBase,
height:
imageWidthBase /
((theAttachmentRemote as Mastodon.AttachmentImage)?.meta?.original?.aspect || 1)
}
const updateFocus = ({ x, y }: { x: number; y: number }) => {
composeDispatch({
type: 'attachment/edit',
payload: {
...theAttachmentRemote,
meta: {
...theAttachmentRemote.meta,
focus: {
x: x > 1 ? 1 : x,
y: y > 1 ? 1 : y
}
}
}
})
}
const pan = useSharedValue({
x:
(((theAttachmentRemote as Mastodon.AttachmentImage)?.meta?.focus?.x || 0) *
imageDimensions.width) /
2,
y:
(((theAttachmentRemote as Mastodon.AttachmentImage)?.meta?.focus?.y || 0) *
imageDimensions.height) /
2
})
const start = useSharedValue({ x: 0, y: 0 })
const gesture = Gesture.Pan()
.onBegin(() => {
start.value = pan.value
})
.onUpdate(e => {
pan.value = {
x: e.translationX + start.value.x,
y: e.translationY + start.value.y
}
})
.onEnd(() => {
runOnJS(updateFocus)({
x: pan.value.x / (imageDimensions.width / 2),
y: pan.value.y / (imageDimensions.height / 2)
})
})
.onFinalize(() => {
start.value = pan.value
})
const styleTransform = useAnimatedStyle(() => {
return {
transform: [
{
translateX: interpolate(
pan.value.x,
[-imageDimensions.width / 2, imageDimensions.width / 2],
[-imageDimensions.width / 2, imageDimensions.width / 2],
Extrapolate.CLAMP
)
},
{
translateY: interpolate(
pan.value.y,
[-imageDimensions.height / 2, imageDimensions.height / 2],
[-imageDimensions.height / 2, imageDimensions.height / 2],
Extrapolate.CLAMP
)
}
]
}
})
return (
<>
<CustomText
fontStyle='M'
style={{
color: colors.primaryDefault,
padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: 0
}}
fontWeight='Bold'
>
{t('content.editAttachment.content.imageFocus')}
</CustomText>
<View style={{ overflow: 'hidden', flex: 1, alignItems: 'center' }}>
<Image
style={{
width: imageDimensions.width,
height: imageDimensions.height
}}
source={{
uri: theAttachmentLocal?.uri ? theAttachmentLocal.uri : theAttachmentRemote?.preview_url
}}
/>
<GestureDetector gesture={gesture}>
<Animated.View
style={[
styleTransform,
{
width: windowWidth * 2,
height: imageDimensions.height * 2,
position: 'absolute',
left: -windowWidth / 2,
top: -imageDimensions.height / 2,
backgroundColor: colors.backgroundOverlayInvert,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}
]}
children={
<View
style={{
width: 48,
height: 48,
borderRadius: 24,
borderWidth: 2,
borderColor: colors.primaryOverlay,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}}
/>
}
/>
</GestureDetector>
</View>
</>
)
}
export default ComposeEditAttachmentImage

View File

@ -1,100 +0,0 @@
import CustomText from '@components/Text'
import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView, StyleSheet, TextInput, View } from 'react-native'
import ComposeContext from '../utils/createContext'
import ComposeEditAttachmentImage from './Image'
export interface Props {
index: number
}
const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
const { t } = useTranslation('screenCompose')
const { colors, mode } = useTheme()
const { composeState, composeDispatch } = useContext(ComposeContext)
const theAttachment = composeState.attachments.uploads[index].remote!
const mediaDisplay = () => {
if (theAttachment) {
switch (theAttachment.type) {
case 'image':
return <ComposeEditAttachmentImage index={index} />
case 'video':
case 'gifv':
const video = composeState.attachments.uploads[index]
return (
<AttachmentVideo
total={1}
index={0}
sensitiveShown={false}
video={
video.local
? ({
url: video.local.uri,
preview_url: video.local.thumbnail,
blurhash: video.remote?.blurhash
} as Mastodon.AttachmentVideo)
: (video.remote as Mastodon.AttachmentVideo)
}
/>
)
}
}
return null
}
return (
<ScrollView>
<View style={{ padding: StyleConstants.Spacing.Global.PagePadding, paddingBottom: 0 }}>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
{t('content.editAttachment.content.altText.heading')}
</CustomText>
<TextInput
style={{
height: StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.Global.PagePadding,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border,
color: colors.primaryDefault
}}
maxLength={1500}
multiline
onChangeText={e =>
composeDispatch({
type: 'attachment/edit',
payload: {
...theAttachment,
description: e
}
})
}
placeholder={t('content.editAttachment.content.altText.placeholder')}
placeholderTextColor={colors.secondary}
value={theAttachment.description}
keyboardAppearance={mode}
/>
<CustomText
fontStyle='S'
style={{
textAlign: 'right',
marginRight: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.M,
color: colors.secondary
}}
>
{theAttachment.description?.length || 0} / 1500
</CustomText>
</View>
{mediaDisplay()}
</ScrollView>
)
}
export default ComposeEditAttachmentRoot

View File

@ -6,6 +6,7 @@ import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native'
import { featureCheck } from '@utils/helpers/featureCheck'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
@ -104,9 +105,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
>
<FastImage
style={{ width: '100%', height: '100%' }}
source={{
uri: item.local?.thumbnail || item.remote?.preview_url
}}
source={{ uri: item.local?.thumbnail || item.remote?.preview_url }}
/>
{item.remote?.meta?.original?.duration ? (
<CustomText
@ -165,7 +164,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
haptics('Success')
}}
/>
{!composeState.attachments.disallowEditing ? (
{composeState.type === 'edit' && featureCheck('edit_media_details') ? (
<Button
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1
@ -175,11 +174,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
spacing='M'
round
overlay
onPress={() => {
navigation.navigate('Screen-Compose-EditAttachment', {
index
})
}}
onPress={() => navigation.navigate('Screen-Compose-EditAttachment', { index })}
/>
) : null}
</View>

View File

@ -50,17 +50,8 @@ const ComposePoll: React.FC = () => {
marginBottom: StyleConstants.Spacing.S
}}
>
{[...Array(total)].map((e, i) => {
const restOptions = Object.keys(options).filter(
o => parseInt(o) !== i && parseInt(o) < total
)
let hasConflict = false
restOptions.forEach(o => {
// @ts-ignore
if (options[o] === options[i]) {
hasConflict = true
}
})
{[...Array(total)].map((_, i) => {
const hasConflict = options.filter((_, ii) => ii !== i && ii < total).includes(options[i])
return (
<View key={i} style={styles.option}>
<Icon
@ -92,14 +83,15 @@ const ComposePoll: React.FC = () => {
}
placeholderTextColor={colors.disabled}
maxLength={MAX_CHARS_PER_OPTION}
// @ts-ignore
value={options[i]}
onChangeText={e =>
onChangeText={e => {
const newOptions = [...options]
newOptions[i] = e
composeDispatch({
type: 'poll',
payload: { options: { ...options, [i]: e } }
payload: { options: [...newOptions] }
})
}
}}
/>
</View>
)

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react'
import { AccessibilityInfo, findNodeHandle, ScrollView, View } from 'react-native'
import { AccessibilityInfo, findNodeHandle, ScrollView } from 'react-native'
import ComposePosting from '../Posting'
import ComposeActions from './Actions'
import ComposeDrafts from './Drafts'

View File

@ -2,6 +2,7 @@ import { createRef } from 'react'
import { ComposeState } from './types'
const composeInitialState: Omit<ComposeState, 'timestamp'> = {
type: undefined,
dirty: false,
posting: false,
spoiler: {
@ -21,12 +22,7 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
poll: {
active: false,
total: 2,
options: {
'0': undefined,
'1': undefined,
'2': undefined,
'3': undefined
},
options: [],
multiple: false,
expire: '86400'
},

View File

@ -36,11 +36,12 @@ const composeParseState = (
): ComposeState => {
switch (params.type) {
case 'share':
return { ...composeInitialState, dirty: true, timestamp: Date.now() }
return { ...composeInitialState, type: params.type, dirty: true, timestamp: Date.now() }
case 'edit':
case 'deleteEdit':
return {
...composeInitialState,
type: params.type,
dirty: true,
timestamp: Date.now(),
...(params.incomingStatus.spoiler_text && {
@ -50,19 +51,13 @@ const composeParseState = (
poll: {
active: true,
total: params.incomingStatus.poll.options.length,
options: {
'0': params.incomingStatus.poll.options[0]?.title || undefined,
'1': params.incomingStatus.poll.options[1]?.title || undefined,
'2': params.incomingStatus.poll.options[2]?.title || undefined,
'3': params.incomingStatus.poll.options[3]?.title || undefined
},
options: params.incomingStatus.poll.options.map(option => option.title),
multiple: params.incomingStatus.poll.multiple,
expire: '86400' // !!!
}
}),
...(params.incomingStatus.media_attachments && {
attachments: {
...(params.type === 'edit' && { disallowEditing: true }),
sensitive: params.incomingStatus.sensitive,
uploads: params.incomingStatus.media_attachments.map(media => ({
remote: media
@ -77,6 +72,7 @@ const composeParseState = (
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
return {
...composeInitialState,
type: params.type,
dirty: true,
timestamp: Date.now(),
...(actualStatus.spoiler_text && {
@ -88,6 +84,7 @@ const composeParseState = (
case 'conversation':
return {
...composeInitialState,
type: params.type,
dirty: true,
timestamp: Date.now(),
...assignVisibility(params.visibility || 'direct')

View File

@ -9,13 +9,27 @@ const composePost = async (
params: RootStackParamList['Screen-Compose'],
composeState: ComposeState
): Promise<Mastodon.Status> => {
const formData = new FormData()
const body: {
language?: string
in_reply_to_id?: string
spoiler_text?: string
status: string
visibility: ComposeState['visibility']
sensitive?: boolean
media_ids?: string[]
media_attributes?: { id: string; description?: string }[]
poll?: {
expires_in: string
multiple: boolean
options: (string | undefined)[]
}
} = { status: composeState.text.raw, visibility: composeState.visibility }
const detectedLanguage = await detectLanguage(
getPureContent([composeState.spoiler.raw, composeState.text.raw].join('\n\n'))
)
if (detectedLanguage) {
formData.append('language', detectedLanguage.language)
body.language = detectedLanguage.language
}
if (composeState.replyToStatus) {
@ -29,29 +43,44 @@ const composePost = async (
return Promise.reject({ removeReply: true })
}
}
formData.append('in_reply_to_id', composeState.replyToStatus.id)
body.in_reply_to_id = composeState.replyToStatus.id
}
if (composeState.spoiler.active) {
formData.append('spoiler_text', composeState.spoiler.raw)
body.spoiler_text = composeState.spoiler.raw
}
formData.append('status', composeState.text.raw)
if (composeState.poll.active) {
Object.values(composeState.poll.options).forEach(
e => e && e.length && formData.append('poll[options][]', e)
)
formData.append('poll[expires_in]', composeState.poll.expire)
formData.append('poll[multiple]', composeState.poll.multiple?.toString())
body.poll = {
expires_in: composeState.poll.expire,
multiple: composeState.poll.multiple,
options: composeState.poll.options.filter(option => !!option)
}
}
if (composeState.attachments.uploads.filter(upload => upload.remote && upload.remote.id).length) {
formData.append('sensitive', composeState.attachments.sensitive?.toString())
composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e.remote!.id!))
}
body.sensitive = composeState.attachments.sensitive
body.media_ids = []
if (params?.type === 'edit') {
body.media_attributes = []
}
formData.append('visibility', composeState.visibility)
composeState.attachments.uploads.forEach((attachment, index) => {
body.media_ids?.push(attachment.remote!.id)
if (params?.type === 'edit') {
if (
attachment.remote?.description !==
params.incomingStatus.media_attachments[index].description
) {
body.media_attributes?.push({
id: attachment.remote!.id,
description: attachment.remote!.description
})
}
}
})
}
return apiInstance<Mastodon.Status>({
method: params?.type === 'edit' ? 'put' : 'post',
@ -73,7 +102,7 @@ const composePost = async (
(params?.type === 'edit' || params?.type === 'deleteEdit' ? Math.random().toString() : '')
)
},
body: formData
body
}).then(res => res.body)
}

View File

@ -1,3 +1,4 @@
import type { RootStackParamList } from '@utils/navigation/navigators'
import { RefObject } from 'react'
import { Asset } from 'react-native-image-picker'
@ -19,6 +20,7 @@ export type ComposeStateDraft = {
}
export type ComposeState = {
type: NonNullable<RootStackParamList['Screen-Compose']>['type'] | undefined
dirty: boolean
timestamp: number
posting: boolean
@ -44,14 +46,11 @@ export type ComposeState = {
poll: {
active: boolean
total: number
options: {
[key: string]: string | undefined
}
options: (string | undefined)[]
multiple: boolean
expire: '300' | '1800' | '3600' | '21600' | '86400' | '259200' | '604800'
}
attachments: {
disallowEditing?: boolean // https://github.com/mastodon/mastodon/pull/20878
sensitive: boolean
uploads: ExtendedAttachment[]
}

View File

@ -1,5 +1,5 @@
import axios from 'axios'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import { ctx, handleError, PagedResponse, parseHeaderLinks, processBody, userAgent } from './helpers'
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
@ -39,15 +39,11 @@ const apiGeneral = async <T = unknown>({
url,
params,
headers: {
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
Accept: '*/*',
Accept: 'application/json',
...userAgent,
...headers
},
...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
data: processBody(body)
})
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))
.catch(handleError())

View File

@ -94,7 +94,6 @@ export const parseHeaderLinks = (headerLink?: string): PagedResponse['links'] =>
}
}
type LinkFormat = { id: string; isOffset: boolean }
export type PagedResponse<T = unknown> = {
body: T
links?: {
@ -103,4 +102,20 @@ export type PagedResponse<T = unknown> = {
}
}
export const processBody = (body?: FormData | Object): FormData | Object | undefined => {
if (!body) return
if (body instanceof FormData) {
if ((body as FormData & { _parts: [][] })._parts?.length) {
return body
} else {
return
}
}
if (Object.keys(body).length) {
return body
}
}
export { ctx, handleError, userAgent }

View File

@ -1,7 +1,14 @@
import { getAccountDetails } from '@utils/storage/actions'
import { StorageGlobal } from '@utils/storage/global'
import axios, { AxiosRequestConfig } from 'axios'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import {
ctx,
handleError,
PagedResponse,
parseHeaderLinks,
processBody,
userAgent
} from './helpers'
export type Params = {
account?: StorageGlobal['account.active']
@ -12,7 +19,7 @@ export type Params = {
[key: string]: string | number | boolean | string[] | number[] | boolean[]
}
headers?: { [key: string]: string }
body?: FormData
body?: FormData | Object
extras?: Omit<AxiosRequestConfig, 'method' | 'baseURL' | 'url' | 'params' | 'headers' | 'data'>
}
@ -51,13 +58,12 @@ const apiInstance = async <T = unknown>({
url,
params,
headers: {
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
Accept: '*/*',
Accept: 'application/json',
...userAgent,
...headers,
Authorization: `Bearer ${accountDetails['auth.token']}`
},
...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
data: processBody(body),
...extras
})
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))

View File

@ -1,6 +1,6 @@
import { mapEnvironment } from '@utils/helpers/checkEnvironment'
import axios from 'axios'
import { ctx, handleError, userAgent } from './helpers'
import { ctx, handleError, processBody, userAgent } from './helpers'
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
@ -42,15 +42,11 @@ const apiTooot = async <T = unknown>({
url: `${url}`,
params,
headers: {
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
Accept: '*/*',
Accept: 'application/json',
...userAgent,
...headers
},
...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
data: processBody(body)
})
.then(response => {
return Promise.resolve({

View File

@ -1,56 +1,38 @@
import { getAccountStorage } from '@utils/storage/actions'
const features = [
{
feature: 'account_follow_notify',
version: 3.3
},
{
feature: 'notification_type_status',
version: 3.3
},
{
feature: 'account_return_suspended',
version: 3.3
},
{
feature: 'edit_post',
version: 3.5
},
{
feature: 'deprecate_auth_follow',
version: 3.5
},
{
feature: 'notification_type_update',
version: 3.5
},
{
feature: 'notification_type_admin_signup',
version: 3.5
},
{
feature: 'notification_types_positive_filter',
version: 3.5
},
{
feature: 'trends_new_path',
version: 3.5
},
{
feature: 'follow_tags',
version: 4.0
},
{
feature: 'notification_type_admin_report',
version: 4.0
},
{
feature: 'filter_server_side',
version: 4.0
}
type Features =
| 'account_follow_notify'
| 'notification_type_status'
| 'account_return_suspended'
| 'edit_post'
| 'deprecate_auth_follow'
| 'notification_type_update'
| 'notification_type_admin_signup'
| 'notification_types_positive_filter'
| 'trends_new_path'
| 'follow_tags'
| 'notification_type_admin_report'
| 'filter_server_side'
| 'instance_new_path'
| 'edit_media_details'
const features: { feature: Features; version: number }[] = [
{ feature: 'account_follow_notify', version: 3.3 },
{ feature: 'notification_type_status', version: 3.3 },
{ feature: 'account_return_suspended', version: 3.3 },
{ feature: 'edit_post', version: 3.5 },
{ feature: 'deprecate_auth_follow', version: 3.5 },
{ feature: 'notification_type_update', version: 3.5 },
{ feature: 'notification_type_admin_signup', version: 3.5 },
{ feature: 'notification_types_positive_filter', version: 3.5 },
{ feature: 'trends_new_path', version: 3.5 },
{ feature: 'follow_tags', version: 4.0 },
{ feature: 'notification_type_admin_report', version: 4.0 },
{ feature: 'filter_server_side', version: 4.0 },
{ feature: 'instance_new_path', version: 4.0 },
{ feature: 'edit_media_details', version: 4.1 }
]
export const featureCheck = (feature: string, v?: string): boolean =>
export const featureCheck = (feature: Features, v?: string): boolean =>
(features.find(f => f.feature === feature)?.version || 999) <=
parseFloat(v || getAccountStorage.string('version'))