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