1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00
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": { "altText": {
"heading": "Describe media for the visually impaired", "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." "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": { "draftsList": {

View File

@ -1,12 +1,14 @@
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' 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 React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 { SafeAreaView } from 'react-native-safe-area-context'
import ComposeEditAttachmentRoot from './EditAttachment/Root'
import ComposeContext from './utils/createContext' import ComposeContext from './utils/createContext'
const ComposeEditAttachment: React.FC< const ComposeEditAttachment: React.FC<
@ -17,12 +19,17 @@ const ComposeEditAttachment: React.FC<
params: { index } params: { index }
} }
}) => { }) => {
const { colors } = useTheme()
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
const { composeState } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const [isSubmitting, setIsSubmitting] = useState(false) 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(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
@ -37,6 +44,12 @@ const ComposeEditAttachment: React.FC<
content='Save' content='Save'
loading={isSubmitting} loading={isSubmitting}
onPress={() => { onPress={() => {
if (composeState.type === 'edit') {
composeDispatch({ type: 'attachment/edit', payload: { ...theAttachment } })
navigation.goBack()
return
}
setIsSubmitting(true) setIsSubmitting(true)
const formData = new FormData() const formData = new FormData()
if (theAttachment.description) { if (theAttachment.description) {
@ -80,8 +93,53 @@ const ComposeEditAttachment: React.FC<
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }} style={{ flex: 1 }}
> >
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}> <SafeAreaView
<ComposeEditAttachmentRoot index={index} /> 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> </SafeAreaView>
</KeyboardAvoidingView> </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 CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { featureCheck } from '@utils/helpers/featureCheck'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -104,9 +105,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
> >
<FastImage <FastImage
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
source={{ source={{ uri: item.local?.thumbnail || item.remote?.preview_url }}
uri: item.local?.thumbnail || item.remote?.preview_url
}}
/> />
{item.remote?.meta?.original?.duration ? ( {item.remote?.meta?.original?.duration ? (
<CustomText <CustomText
@ -165,7 +164,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
haptics('Success') haptics('Success')
}} }}
/> />
{!composeState.attachments.disallowEditing ? ( {composeState.type === 'edit' && featureCheck('edit_media_details') ? (
<Button <Button
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', { accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1 attachment: index + 1
@ -175,11 +174,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
spacing='M' spacing='M'
round round
overlay overlay
onPress={() => { onPress={() => navigation.navigate('Screen-Compose-EditAttachment', { index })}
navigation.navigate('Screen-Compose-EditAttachment', {
index
})
}}
/> />
) : null} ) : null}
</View> </View>

View File

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

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react' 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 ComposePosting from '../Posting'
import ComposeActions from './Actions' import ComposeActions from './Actions'
import ComposeDrafts from './Drafts' import ComposeDrafts from './Drafts'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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