mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Fix #672
Removed image focus as different clients implement this differently
This commit is contained in:
@ -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": {
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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 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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
},
|
},
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
src/screens/Compose/utils/types.d.ts
vendored
7
src/screens/Compose/utils/types.d.ts
vendored
@ -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[]
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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 }
|
||||||
|
@ -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) }))
|
||||||
|
@ -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({
|
||||||
|
@ -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'))
|
||||||
|
Reference in New Issue
Block a user