mirror of https://github.com/tooot-app/app
Fix #672
Removed image focus as different clients implement this differently
This commit is contained in:
parent
613cf1365c
commit
47d5b02468
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) }))
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue