This commit is contained in:
xmflsct 2022-12-10 23:11:41 +01:00
parent 213328ef1a
commit 36bbe5bdbd
8 changed files with 313 additions and 392 deletions

View File

@ -407,12 +407,12 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
<Stack.Screen
name='Screen-Compose-DraftsList'
component={ComposeDraftsList}
options={{ headerShown: false, presentation: 'modal' }}
options={{ presentation: 'modal' }}
/>
<Stack.Screen
name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment}
options={{ headerShown: false, presentation: 'modal' }}
options={{ presentation: 'modal' }}
/>
</Stack.Navigator>
</ComposeContext.Provider>

View File

@ -1,49 +1,227 @@
import apiInstance from '@api/instance'
import { HeaderLeft } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useAppDispatch } from '@root/store'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React, { useCallback } from 'react'
import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
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 ComposeDraftsListRoot from './DraftsList/Root'
import { Dimensions, Modal, Platform, Pressable, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
import ComposeContext from './utils/createContext'
import { formatText } from './utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
const Stack = createNativeStackNavigator()
const ComposeDraftsList: React.FC<
ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>
> = ({
const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>> = ({
navigation,
route: {
params: { timestamp }
},
navigation
}
}) => {
const { colors } = useTheme()
const { t } = useTranslation('screenCompose')
const children = useCallback(
() => <ComposeDraftsListRoot timestamp={timestamp} />,
[]
)
const headerLeft = useCallback(
() => (
<HeaderLeft
type='icon'
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
),
[]
useEffect(() => {
navigation.setOptions({
title: t('content.draftsList.header.title'),
headerLeft: () => (
<HeaderLeft type='icon' content='ChevronDown' onPress={() => navigation.goBack()} />
)
})
}, [])
const { composeDispatch } = useContext(ComposeContext)
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp
)
const [checkingAttachments, setCheckingAttachments] = useState(false)
const dispatch = useAppDispatch()
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
return (
<Stack.Navigator>
<Stack.Screen
name='Screen-Compose-EditAttachment-Root'
children={children}
options={{
headerLeft,
title: t('content.draftsList.header.title'),
headerShadowVisible: false
<>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S
}}
>
<Icon
name='AlertTriangle'
color={colors.secondary}
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{t('content.draftsList.warning')}
</CustomText>
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}
renderItem={({ item }: { item: ComposeStateDraft }) => {
return (
<Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
style={{
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault
}}
onPress={async () => {
setCheckingAttachments(true)
let tempDraft = item
let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) {
await apiInstance<Mastodon.Attachment>({
method: 'get',
url: `media/${attachment.remote?.id}`
})
.then(res => {
if (res.body.id === attachment.remote?.id) {
tempUploads.push(attachment)
}
})
.catch(() => {})
}
tempDraft = {
...tempDraft,
attachments: { ...item.attachments, uploads: tempUploads }
}
}
tempDraft.spoiler?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.spoiler })
tempDraft.text?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.text })
composeDispatch({
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack()
}}
>
<View style={{ flex: 1 }}>
<HeaderSharedCreated created_at={item.timestamp} />
<CustomText
fontStyle='M'
numberOfLines={2}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
>
{item.text || item.spoiler || t('content.draftsList.content.textEmpty')}
</CustomText>
{item.attachments?.uploads.length ? (
<View
style={{
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.S
}}
>
{item.attachments.uploads.map((attachment, index) => (
<FastImage
key={index}
style={{
width:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
height:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
/>
))}
</View>
) : null}
</View>
</Pressable>
)
}}
renderHiddenItem={({ item }) => (
<Pressable
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
backgroundColor: colors.red
}}
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
children={
<View
style={{
flexBasis:
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
justifyContent: 'center',
alignItems: 'center'
}}
children={
<Icon
name='Trash'
size={StyleConstants.Font.Size.L}
color={colors.primaryOverlay}
/>
}
/>
}
/>
)}
disableRightSwipe={true}
rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()}
/>
</PanGestureHandler>
<Modal
transparent
animationType='fade'
visible={checkingAttachments}
children={
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.backgroundOverlayInvert
}}
children={
<CustomText
fontStyle='M'
children={t('content.draftsList.checkAttachment')}
style={{ color: colors.primaryOverlay }}
/>
}
/>
}
/>
</Stack.Navigator>
</>
)
}

View File

@ -1,223 +0,0 @@
import apiInstance from '@api/instance'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions, Image, Modal, Platform, Pressable, View } from 'react-native'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import { formatText } from '../utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from '../utils/types'
export interface Props {
timestamp: number
}
const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const { composeDispatch } = useContext(ComposeContext)
const { t } = useTranslation('screenCompose')
const navigation = useNavigation()
const dispatch = useAppDispatch()
const { colors, theme } = useTheme()
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp
)
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
const [checkingAttachments, setCheckingAttachments] = useState(false)
const renderItem = useCallback(
({ item }: { item: ComposeStateDraft }) => {
return (
<Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
style={{
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault
}}
onPress={async () => {
setCheckingAttachments(true)
let tempDraft = item
let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) {
await apiInstance<Mastodon.Attachment>({
method: 'get',
url: `media/${attachment.remote?.id}`
})
.then(res => {
if (res.body.id === attachment.remote?.id) {
tempUploads.push(attachment)
}
})
.catch(() => {})
}
tempDraft = {
...tempDraft,
attachments: { ...item.attachments, uploads: tempUploads }
}
}
tempDraft.spoiler?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.spoiler })
tempDraft.text?.length &&
formatText({ textInput: 'text', composeDispatch, content: tempDraft.text })
composeDispatch({
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack()
}}
>
<View style={{ flex: 1 }}>
<HeaderSharedCreated created_at={item.timestamp} />
<CustomText
fontStyle='M'
numberOfLines={2}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
>
{item.text || item.spoiler || t('content.draftsList.content.textEmpty')}
</CustomText>
{item.attachments?.uploads.length ? (
<View
style={{
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.S
}}
>
{item.attachments.uploads.map((attachment, index) => (
<Image
key={index}
style={{
width:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
height:
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
/>
))}
</View>
) : null}
</View>
</Pressable>
)
},
[theme]
)
return (
<>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S
}}
>
<Icon
name='AlertTriangle'
color={colors.secondary}
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{t('content.draftsList.warning')}
</CustomText>
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}
renderItem={renderItem}
renderHiddenItem={({ item }) => (
<Pressable
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
backgroundColor: colors.red
}}
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
children={
<View
style={{
flexBasis:
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 255, 0, 0.2)'
}}
children={
<Icon
name='Trash'
size={StyleConstants.Font.Size.L}
color={colors.primaryOverlay}
/>
}
/>
}
/>
)}
disableRightSwipe={true}
rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()}
/>
</PanGestureHandler>
<Modal
transparent
animationType='fade'
visible={checkingAttachments}
children={
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.backgroundOverlayInvert
}}
children={
<CustomText
fontStyle='M'
children={t('content.draftsList.checkAttachment')}
style={{ color: colors.primaryOverlay }}
/>
}
/>
}
/>
</>
)
}
export default ComposeDraftsListRoot

View File

@ -1,49 +1,90 @@
import { HeaderLeft } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React from 'react'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native'
import { Alert, KeyboardAvoidingView, Platform } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import ComposeEditAttachmentRoot from './EditAttachment/Root'
import ComposeEditAttachmentSubmit from './EditAttachment/Submit'
import ComposeContext from './utils/createContext'
const Stack = createNativeStackNavigator()
const ComposeEditAttachment: React.FC<ScreenComposeStackScreenProps<
'Screen-Compose-EditAttachment'
>> = ({
const ComposeEditAttachment: React.FC<
ScreenComposeStackScreenProps<'Screen-Compose-EditAttachment'>
> = ({
navigation,
route: {
params: { index }
},
navigation
}) => {
const { t } = useTranslation('screenCompose')
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<Stack.Navigator>
<Stack.Screen
name='Screen-Compose-EditAttachment-Root'
children={() => <ComposeEditAttachmentRoot index={index} />}
options={{
headerLeft: () => <HeaderLeft
type='icon'
content='ChevronDown'
onPress={() => navigation.goBack()}
/>,
headerRight: () => <ComposeEditAttachmentSubmit index={index} />,
title: t('content.editAttachment.header.title')
}}
/>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
)
}
}) => {
const { t } = useTranslation('screenCompose')
const { composeState } = useContext(ComposeContext)
const [isSubmitting, setIsSubmitting] = useState(false)
const theAttachment = composeState.attachments.uploads[index].remote!
useEffect(() => {
navigation.setOptions({
title: t('content.editAttachment.header.title'),
headerLeft: () => (
<HeaderLeft type='icon' content='ChevronDown' onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('content.editAttachment.header.right.accessibilityLabel')}
type='icon'
content='Save'
loading={isSubmitting}
onPress={() => {
setIsSubmitting(true)
const formData = new FormData()
if (theAttachment.description) {
formData.append('description', theAttachment.description)
}
if (theAttachment.meta?.focus?.x !== 0 || theAttachment.meta.focus.y !== 0) {
formData.append(
'focus',
`${theAttachment.meta?.focus?.x || 0},${-theAttachment.meta?.focus?.y || 0}`
)
}
theAttachment?.id &&
apiInstance<Mastodon.Attachment>({
method: 'put',
url: `media/${theAttachment.id}`,
body: formData
})
.then(() => {
haptics('Success')
navigation.goBack()
})
.catch(() => {
setIsSubmitting(false)
haptics('Error')
Alert.alert(t('content.editAttachment.header.right.failed.title'), undefined, [
{
text: t('content.editAttachment.header.right.failed.button'),
style: 'cancel'
}
])
})
}}
/>
)
})
}, [theAttachment])
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<ComposeEditAttachmentRoot index={index} />
</SafeAreaView>
</KeyboardAvoidingView>
)
}
export default ComposeEditAttachment

View File

@ -1,79 +0,0 @@
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { HeaderRight } from '@components/Header'
import { useNavigation } from '@react-navigation/native'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import ComposeContext from '../utils/createContext'
export interface Props {
index: number
}
const ComposeEditAttachmentSubmit: React.FC<Props> = ({ index }) => {
const { composeState } = useContext(ComposeContext)
const navigation = useNavigation()
const [isSubmitting, setIsSubmitting] = useState(false)
const { t } = useTranslation('screenCompose')
const theAttachment = composeState.attachments.uploads[index].remote!
return (
<HeaderRight
accessibilityLabel={t(
'content.editAttachment.header.right.accessibilityLabel'
)}
type='icon'
content='Save'
loading={isSubmitting}
onPress={() => {
setIsSubmitting(true)
const formData = new FormData()
if (theAttachment.description) {
formData.append('description', theAttachment.description)
}
if (
theAttachment.meta?.focus?.x !== 0 ||
theAttachment.meta.focus.y !== 0
) {
formData.append(
'focus',
`${theAttachment.meta?.focus?.x || 0},${
-theAttachment.meta?.focus?.y || 0
}`
)
}
theAttachment?.id &&
apiInstance<Mastodon.Attachment>({
method: 'put',
url: `media/${theAttachment.id}`,
body: formData
})
.then(() => {
haptics('Success')
navigation.goBack()
})
.catch(() => {
setIsSubmitting(false)
haptics('Error')
Alert.alert(
t('content.editAttachment.header.right.failed.title'),
undefined,
[
{
text: t(
'content.editAttachment.header.right.failed.button'
),
style: 'cancel'
}
]
)
})
}}
/>
)
}
export default ComposeEditAttachmentSubmit

View File

@ -171,21 +171,23 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
haptics('Success')
}}
/>
<Button
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1
})}
type='icon'
content='Edit'
spacing='M'
round
overlay
onPress={() => {
navigation.navigate('Screen-Compose-EditAttachment', {
index
})
}}
/>
{!composeState.attachments.disallowEditing ? (
<Button
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1
})}
type='icon'
content='Edit'
spacing='M'
round
overlay
onPress={() => {
navigation.navigate('Screen-Compose-EditAttachment', {
index
})
}}
/>
) : null}
</View>
)}
</View>

View File

@ -65,6 +65,7 @@ const composeParseState = (
}),
...(params.incomingStatus.media_attachments && {
attachments: {
...(params.type === 'edit' && { disallowEditing: true }),
sensitive: params.incomingStatus.sensitive,
uploads: params.incomingStatus.media_attachments.map(media => ({
remote: media

View File

@ -51,6 +51,7 @@ export type ComposeState = {
expire: '300' | '1800' | '3600' | '21600' | '86400' | '259200' | '604800'
}
attachments: {
disallowEditing?: boolean // https://github.com/mastodon/mastodon/pull/20878
sensitive: boolean
uploads: ExtendedAttachment[]
}
@ -59,8 +60,8 @@ export type ComposeState = {
replyToStatus?: Mastodon.Status
textInputFocus: {
current: 'text' | 'spoiler'
refs: { text: RefObject<TextInput>, spoiler: RefObject<TextInput> }
isFocused: { text: MutableRefObject<boolean>, spoiler: MutableRefObject<boolean> }
refs: { text: RefObject<TextInput>; spoiler: RefObject<TextInput> }
isFocused: { text: MutableRefObject<boolean>; spoiler: MutableRefObject<boolean> }
}
}