FIX: Manage Wallets dragging (#6999)

This commit is contained in:
Marcos Rodriguez Vélez 2024-09-02 18:16:31 -04:00 committed by GitHub
parent c7f15c08d8
commit d0bca03d02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 185 additions and 100 deletions

View File

@ -7,18 +7,6 @@ import { TransactionListItem } from './TransactionListItem';
import { useTheme } from './themes';
import { BitcoinUnit } from '../models/bitcoinUnits';
interface ManageWalletsListItemProps {
item: Item;
isDraggingDisabled: boolean;
drag: () => void;
isActive: boolean;
state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void;
}
enum ItemType {
WalletSection = 'wallet',
TransactionSection = 'transaction',
@ -36,6 +24,18 @@ interface TransactionItem {
type Item = WalletItem | TransactionItem;
interface ManageWalletsListItemProps {
item: Item;
isDraggingDisabled: boolean;
drag?: () => void;
isPlaceHolder?: boolean;
state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void;
}
interface SwipeContentProps {
onPress: () => void;
hideBalance?: boolean;
@ -61,8 +61,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
item,
isDraggingDisabled,
drag,
isActive,
state,
isPlaceHolder = false,
navigateToWallet,
renderHighlightedText,
handleDeleteWallet,
@ -76,27 +76,59 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
}
}, [item, navigateToWallet]);
const leftContent = useCallback(
(reset: () => void) => (
<LeftSwipeContent
onPress={() => {
handleToggleHideBalance(item.data as TWallet);
reset();
}}
hideBalance={(item.data as TWallet).hideBalance}
colors={colors}
/>
),
[colors, handleToggleHideBalance, item.data],
);
const rightContent = useCallback(
(reset: () => void) => (
<RightSwipeContent
onPress={() => {
handleDeleteWallet(item.data as TWallet);
reset();
}}
/>
),
[handleDeleteWallet, item.data],
);
if (item.type === ItemType.WalletSection) {
return (
<ListItem.Swipeable
leftWidth={80}
rightWidth={90}
minSlideWidth={40}
leftContent={
<LeftSwipeContent onPress={() => handleToggleHideBalance(item.data)} hideBalance={item.data.hideBalance} colors={colors} />
}
rightContent={<RightSwipeContent colors={colors} onPress={() => handleDeleteWallet(item.data)} />}
animation={{ duration: 400 }}
containerStyle={{ backgroundColor: colors.background }}
leftContent={leftContent}
rightContent={rightContent}
>
<View style={styles.walletCarouselItemContainer}>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
isActive={isActive}
onPress={onPress}
searchQuery={state.searchQuery}
renderHighlightedText={renderHighlightedText}
/>
</View>
<ListItem.Content
style={{
backgroundColor: colors.background,
}}
>
<View style={styles.walletCarouselItemContainer}>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
onPress={onPress}
animationsEnabled={false}
searchQuery={state.searchQuery}
isPlaceHolder={isPlaceHolder}
renderHighlightedText={renderHighlightedText}
/>
</View>
</ListItem.Content>
</ListItem.Swipeable>
);
} else if (item.type === ItemType.TransactionSection && item.data) {
@ -135,4 +167,4 @@ const styles = StyleSheet.create({
},
});
export default ManageWalletsListItem;
export { ManageWalletsListItem, LeftSwipeContent, RightSwipeContent };

View File

@ -98,7 +98,6 @@ interface WalletCarouselItemProps {
isSelectedWallet?: boolean;
customStyle?: ViewStyle;
horizontal?: boolean;
isActive?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
}
@ -162,8 +161,32 @@ const iStyles = StyleSheet.create({
},
});
interface WalletCarouselItemProps {
item: TWallet;
onPress: (item: TWallet) => void;
handleLongPress?: () => void;
isSelectedWallet?: boolean;
customStyle?: ViewStyle;
horizontal?: boolean;
isPlaceHolder?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, searchQuery, renderHighlightedText }) => {
({
item,
onPress,
handleLongPress,
isSelectedWallet,
customStyle,
horizontal,
searchQuery,
renderHighlightedText,
animationsEnabled = true,
isPlaceHolder = false,
}) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
const { walletTransactionUpdateStatus } = useStorage();
@ -172,22 +195,26 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
const isLargeScreen = useIsLargeScreen();
const onPressedIn = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
if (animationsEnabled) {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
const onPressedOut = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
if (animationsEnabled) {
Animated.spring(scaleValue, {
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
const handlePress = useCallback(() => {
onPressedOut();
@ -239,33 +266,37 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
<Image defaultSource={image} source={image} style={iStyles.image} />
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()}
</Text>
<View style={iStyles.balanceContainer}>
{item.hideBalance ? (
<>
<BlueSpacing10 />
<BlurredBalanceView />
</>
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
style={[iStyles.balance, { color: colors.inverseForegroundColor }]}
>
{`${balance} `}
{!isPlaceHolder && (
<>
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()}
</Text>
)}
</View>
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.latestTx, { color: colors.inverseForegroundColor }]}>
{loc.wallets.list_latest_transaction}
</Text>
<Text numberOfLines={1} style={[iStyles.latestTxTime, { color: colors.inverseForegroundColor }]}>
{latestTransactionText}
</Text>
<View style={iStyles.balanceContainer}>
{item.hideBalance ? (
<>
<BlueSpacing10 />
<BlurredBalanceView />
</>
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
style={[iStyles.balance, { color: colors.inverseForegroundColor }]}
>
{`${balance} `}
</Text>
)}
</View>
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.latestTx, { color: colors.inverseForegroundColor }]}>
{loc.wallets.list_latest_transaction}
</Text>
<Text numberOfLines={1} style={[iStyles.latestTxTime, { color: colors.inverseForegroundColor }]}>
{latestTransactionText}
</Text>
</>
)}
</LinearGradient>
</View>
</Pressable>

View File

@ -380,10 +380,10 @@ const DetailViewStackScreensStack = () => {
component={ManageWallets}
options={navigationStyle({
headerBackVisible: false,
headerLargeTitle: true,
gestureEnabled: false,
presentation: 'modal',
presentation: 'containedModal',
title: loc.wallets.manage_title,
statusBarStyle: 'auto',
})(theme)}
/>
</DetailViewStack.Navigator>

4
package-lock.json generated
View File

@ -69,7 +69,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "11.1.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#v4.0.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.18.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -13015,7 +13015,7 @@
},
"node_modules/react-native-draggable-flatlist": {
"version": "4.0.1",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-draggable-flatlist.git#ebfddc4877e8f65d5391a748db61b9cd030430ba",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-draggable-flatlist.git#3a61627474a4e35198ae961310c77fb305507509",
"license": "MIT",
"dependencies": {
"@babel/preset-typescript": "^7.17.12"

View File

@ -86,6 +86,7 @@
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#958fac3d40811f38b53042ada9168175e321b99f",
"@react-native/gradle-plugin": "^0.74.85",
"@react-native/metro-config": "0.74.87",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
@ -132,7 +133,7 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "11.1.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#v4.0.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.18.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -153,7 +154,6 @@
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.15.1",
"@react-native/metro-config": "0.74.87",
"react-native-safe-area-context": "4.10.9",
"react-native-screens": "3.34.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",

View File

@ -1,18 +1,13 @@
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef } from 'react';
import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native';
import {
Platform,
StyleSheet,
useColorScheme,
TouchableOpacity,
Image,
Text,
Alert,
I18nManager,
Animated,
LayoutAnimation,
} from 'react-native';
// @ts-expect-error: react-native-draggable-flatlist is not typed
import { NestableScrollContainer, NestableDraggableFlatList, RenderItem } from 'react-native-draggable-flatlist';
NestableScrollContainer,
ScaleDecorator,
OpacityDecorator,
NestableDraggableFlatList,
RenderItem,
// @ts-expect-error: react-native-draggable-flatlist is not typed
} from 'react-native-draggable-flatlist';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
@ -28,7 +23,7 @@ import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import presentAlert from '../../components/Alert';
import prompt from '../../helpers/prompt';
import HeaderRightButton from '../../components/HeaderRightButton';
import ManageWalletsListItem from '../../components/ManageWalletsListItem';
import { ManageWalletsListItem } from '../../components/ManageWalletsListItem';
enum ItemType {
WalletSection = 'wallet',
@ -194,7 +189,6 @@ const ManageWallets: React.FC = () => {
const { colors, closeImage } = useTheme();
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata } = useStorage();
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
const colorScheme = useColorScheme();
const { navigate, setOptions, goBack } = useExtendedNavigation();
const [state, dispatch] = useReducer(reducer, initialState);
const { isBiometricUseCapableAndEnabled } = useBiometrics();
@ -283,12 +277,11 @@ const ManageWallets: React.FC = () => {
};
setOptions({
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
headerLeft: () => HeaderLeftButton,
headerRight: () => SaveButton,
headerSearchBarOptions: searchBarOptions,
});
}, [colorScheme, setOptions, HeaderLeftButton, SaveButton]);
}, [setOptions, HeaderLeftButton, SaveButton]);
useFocusEffect(
useCallback(() => {
@ -429,20 +422,39 @@ const ManageWallets: React.FC = () => {
[goBack, navigate],
);
const renderWalletItem = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ScaleDecorator drag={drag} activeScale={1.1}>
<OpacityDecorator activeOpacity={0.5}>
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
drag={drag}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
</OpacityDecorator>
</ScaleDecorator>
),
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
);
const renderPlaceholder = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
drag={drag}
isActive={isActive}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
isPlaceHolder
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
),
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
[handleDeleteWallet, handleToggleHideBalance, navigateToWallet, renderHighlightedText, state],
);
const onChangeOrder = useCallback(() => {
@ -482,28 +494,38 @@ const ManageWallets: React.FC = () => {
}, [state.searchQuery, state.wallets.length, state.txMetadata, stylesHook.noResultsText]);
return (
<GestureHandlerRootView style={[styles.root, stylesHook.root]}>
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
<NestableScrollContainer contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets>
{renderHeader}
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is WalletItem => item.type === ItemType.WalletSection)}
extraData={state.tempOrder}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onPlaceholderIndexChange={onChangeOrder}
onRelease={onRelease}
delayLongPress={150}
useNativeDriver={true}
dragItemOverflow
autoscrollThreshold={1}
renderPlaceholder={renderPlaceholder}
autoscrollSpeed={0.5}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
onDragEnd={onDragEnd}
containerStyle={styles.root}
ListHeaderComponent={renderHeader}
/>
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is TransactionItem => item.type === ItemType.TransactionSection)}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
dragItemOverflow
containerStyle={styles.root}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
useNativeDriver={true}
ListHeaderComponent={renderHeader}
/>
</NestableScrollContainer>
</GestureHandlerRootView>