mirror of https://github.com/tooot-app/app
Consolidate swipe to delete views
This commit is contained in:
parent
aa5a607666
commit
738194d108
|
@ -2,7 +2,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { Fragment } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { View } from 'react-native'
|
||||
import { View, ViewStyle } from 'react-native'
|
||||
import { TouchableNativeFeedback } from 'react-native-gesture-handler'
|
||||
import Icon from './Icon'
|
||||
import CustomText from './Text'
|
||||
|
@ -11,9 +11,10 @@ export type Props = {
|
|||
onPress: () => void
|
||||
filter: Mastodon.Filter<'v2'>
|
||||
button?: React.ReactNode
|
||||
style?: ViewStyle
|
||||
}
|
||||
|
||||
export const Filter: React.FC<Props> = ({ onPress, filter, button }) => {
|
||||
export const Filter: React.FC<Props> = ({ onPress, filter, button, style }) => {
|
||||
const { t } = useTranslation(['common', 'screenTabs'])
|
||||
const { colors } = useTheme()
|
||||
|
||||
|
@ -24,7 +25,8 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button }) => {
|
|||
paddingVertical: StyleConstants.Spacing.S,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundDefault
|
||||
backgroundColor: colors.backgroundDefault,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
|
@ -83,12 +85,7 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button }) => {
|
|||
{filter.context.map((c, index) => (
|
||||
<Fragment key={index}>
|
||||
<CustomText
|
||||
style={{
|
||||
color: colors.secondary,
|
||||
textDecorationColor: colors.disabled,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationStyle: 'solid'
|
||||
}}
|
||||
style={{ color: colors.secondary }}
|
||||
children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)}
|
||||
/>
|
||||
<CustomText children={t('common:separator')} />
|
||||
|
|
|
@ -12,7 +12,7 @@ const Hr: React.FC<{ style?: ViewStyle }> = ({ style }) => {
|
|||
borderTopColor: colors.border,
|
||||
borderTopWidth: 1,
|
||||
height: 1,
|
||||
marginVertical: StyleConstants.Spacing.S
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
style
|
||||
]}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { ColorValue, TouchableNativeFeedback, View } from 'react-native'
|
||||
import { SwipeListView } from 'react-native-swipe-list-view'
|
||||
import haptics from './haptics'
|
||||
import Icon, { IconName } from './Icon'
|
||||
import ComponentSeparator from './Separator'
|
||||
|
||||
export type Props = {
|
||||
actions: {
|
||||
onPress: (item: any) => void
|
||||
color: ColorValue
|
||||
icon: IconName
|
||||
haptic?: Parameters<typeof haptics>['0']
|
||||
}[]
|
||||
}
|
||||
|
||||
export const SwipeToActions = <T extends unknown>({
|
||||
actions,
|
||||
...rest
|
||||
}: Props & SwipeListView<T>['props']) => {
|
||||
const perActionWidth = StyleConstants.Spacing.L * 2 + StyleConstants.Font.Size.L
|
||||
|
||||
return (
|
||||
<SwipeListView
|
||||
renderHiddenItem={({ item }) => (
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'flex-end' }}>
|
||||
{actions.map((action, index) => (
|
||||
<TouchableNativeFeedback
|
||||
key={index}
|
||||
onPress={() => {
|
||||
haptics(action.haptic || 'Light')
|
||||
action.onPress({ item })
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: StyleConstants.Spacing.L,
|
||||
flexBasis: perActionWidth,
|
||||
backgroundColor: action.color,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Icon name={action.icon} color='white' size={StyleConstants.Font.Size.L} />
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
rightOpenValue={-perActionWidth * actions.length}
|
||||
disableRightSwipe
|
||||
closeOnRowPress
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { HeaderLeft } from '@components/Header'
|
||||
import Icon from '@components/Icon'
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import { SwipeToActions } from '@components/SwipeToActions'
|
||||
import CustomText from '@components/Text'
|
||||
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
|
||||
import apiInstance from '@utils/api/instance'
|
||||
|
@ -10,10 +10,8 @@ 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 { Dimensions, Modal, Platform, Pressable, View } from 'react-native'
|
||||
import { Dimensions, Modal, 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 ComposeContext from './utils/createContext'
|
||||
import { formatText } from './utils/processText'
|
||||
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
|
||||
|
@ -39,9 +37,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: t('content.draftsList.header.title'),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />
|
||||
)
|
||||
headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
@ -49,8 +45,6 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||
const [drafts] = useAccountStorage.object('drafts')
|
||||
const [checkingAttachments, setCheckingAttachments] = useState(false)
|
||||
|
||||
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
|
@ -61,7 +55,8 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||
padding: StyleConstants.Spacing.S,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
@ -74,135 +69,104 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||
{t('content.draftsList.warning')}
|
||||
</CustomText>
|
||||
</View>
|
||||
<PanGestureHandler enabled={Platform.OS === 'ios'}>
|
||||
<SwipeListView
|
||||
data={drafts.filter(draft => draft.timestamp !== timestamp)}
|
||||
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
|
||||
})
|
||||
removeDraft(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('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 -
|
||||
StyleConstants.Spacing.S * 3) /
|
||||
4,
|
||||
height:
|
||||
(Dimensions.get('window').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 }) => (
|
||||
<SwipeToActions
|
||||
actions={[
|
||||
{ onPress: ({ item }) => removeDraft(item.timestamp), color: colors.red, icon: 'trash' }
|
||||
]}
|
||||
data={drafts.filter(draft => draft.timestamp !== timestamp)}
|
||||
renderItem={({ item }: { item: ComposeStateDraft }) => {
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: colors.red
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
onPress={() => removeDraft(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}
|
||||
/>
|
||||
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(() => {})
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
disableRightSwipe={true}
|
||||
rightOpenValue={-actionWidth}
|
||||
previewOpenValue={-actionWidth / 2}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
keyExtractor={item => item.timestamp?.toString()}
|
||||
/>
|
||||
</PanGestureHandler>
|
||||
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
|
||||
})
|
||||
removeDraft(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('window').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 -
|
||||
StyleConstants.Spacing.S * 3) /
|
||||
4,
|
||||
height:
|
||||
(Dimensions.get('window').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>
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item.timestamp?.toString()}
|
||||
/>
|
||||
<Modal
|
||||
transparent
|
||||
animationType='fade'
|
||||
|
|
|
@ -416,12 +416,12 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||
<Stack.Screen
|
||||
name='Screen-Compose-DraftsList'
|
||||
component={ComposeDraftsList}
|
||||
options={{ presentation: 'modal' }}
|
||||
options={{ presentation: 'modal', headerShadowVisible: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment'
|
||||
component={ComposeEditAttachment}
|
||||
options={{ presentation: 'modal' }}
|
||||
options={{ presentation: 'modal', headerShadowVisible: false }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</ComposeContext.Provider>
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
import { Filter } from '@components/Filter'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import Icon from '@components/Icon'
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import { SwipeToActions } from '@components/SwipeToActions'
|
||||
import apiInstance from '@utils/api/instance'
|
||||
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useFiltersQuery } from '@utils/queryHooks/filters'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { SwipeListView } from 'react-native-swipe-list-view'
|
||||
|
||||
const TabMePreferencesFilters: React.FC<
|
||||
TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filters'>
|
||||
> = ({ navigation }) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation(['common', 'screenTabs'])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
@ -38,40 +33,31 @@ const TabMePreferencesFilters: React.FC<
|
|||
const { data, refetch } = useFiltersQuery<'v2'>({ version: 'v2' })
|
||||
|
||||
return (
|
||||
<SwipeListView
|
||||
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
|
||||
renderHiddenItem={({ item }) => (
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
backgroundColor: colors.red
|
||||
}}
|
||||
onPress={() => {
|
||||
<SwipeToActions
|
||||
actions={[
|
||||
{
|
||||
onPress: ({ item }) => {
|
||||
apiInstance({ method: 'delete', version: 'v2', url: `filters/${item.id}` }).then(() =>
|
||||
refetch()
|
||||
)
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingHorizontal: StyleConstants.Spacing.L }}>
|
||||
<Icon name='trash' color='white' size={StyleConstants.Font.Size.L} />
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
rightOpenValue={-(StyleConstants.Spacing.L * 2 + StyleConstants.Font.Size.L)}
|
||||
disableRightSwipe
|
||||
closeOnRowPress
|
||||
},
|
||||
color: colors.red,
|
||||
icon: 'trash'
|
||||
}
|
||||
]}
|
||||
data={data?.sort(filter =>
|
||||
filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1
|
||||
)}
|
||||
renderItem={({ item: filter }) => (
|
||||
<Filter
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingVertical: StyleConstants.Spacing.M
|
||||
}}
|
||||
filter={filter}
|
||||
onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'edit', filter })}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { HeaderLeft } from '@components/Header'
|
||||
import { Message } from '@components/Message'
|
||||
import { useNavigationState } from '@react-navigation/native'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import { TabMePreferencesStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import TabMePreferencesFilter from './Filter'
|
||||
|
@ -17,6 +18,15 @@ const TabMePreferences: React.FC<TabMeStackScreenProps<'Tab-Me-Preferences'>> =
|
|||
const { t } = useTranslation('screenTabs')
|
||||
const messageRef = useRef<FlashMessage>(null)
|
||||
|
||||
const isNested =
|
||||
(useNavigationState(
|
||||
state => state.routes.find(route => route.name === 'Tab-Me-Preferences')?.state?.routes.length
|
||||
) || 0) > 1
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({ gestureEnabled: !isNested })
|
||||
}, [isNested])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
|
||||
|
|
Loading…
Reference in New Issue