BlueWallet/screen/wallets/ManageWallets.tsx

378 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react';
2024-07-26 19:35:53 -04:00
import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native';
2024-07-24 20:22:06 -04:00
// @ts-ignore: no declaration file
2024-06-07 18:44:41 -04:00
import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { useTheme } from '../../components/themes';
2024-07-25 18:41:01 -04:00
import { WalletCarouselItem } from '../../components/WalletsCarousel';
import { TransactionListItem } from '../../components/TransactionListItem';
2024-06-07 18:44:41 -04:00
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import useDebounce from '../../hooks/useDebounce';
2024-07-24 20:53:40 -04:00
import { TTXMetadata } from '../../class';
2024-07-25 18:41:01 -04:00
import { ExtendedTransaction, LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
2024-07-25 01:54:51 -04:00
import { BitcoinUnit } from '../../models/bitcoinUnits';
2024-07-25 18:41:01 -04:00
import useBounceAnimation from '../../hooks/useBounceAnimation';
enum ItemType {
WalletSection = 'wallet',
TransactionSection = 'transaction',
}
interface WalletItem {
type: ItemType.WalletSection;
data: TWallet;
}
interface TransactionItem {
type: ItemType.TransactionSection;
data: ExtendedTransaction & LightningTransaction;
}
type Item = WalletItem | TransactionItem;
2024-06-07 18:44:41 -04:00
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED';
const SET_WALLET_DATA = 'SET_WALLET_DATA';
const SET_TX_METADATA = 'SET_TX_METADATA';
const SET_ORDER = 'SET_ORDER';
2024-06-07 18:44:41 -04:00
interface SetSearchQueryAction {
type: typeof SET_SEARCH_QUERY;
payload: string;
}
interface SetIsSearchFocusedAction {
type: typeof SET_IS_SEARCH_FOCUSED;
payload: boolean;
}
interface SetWalletDataAction {
type: typeof SET_WALLET_DATA;
2024-07-25 18:41:01 -04:00
payload: TWallet[];
2024-06-07 18:44:41 -04:00
}
interface SetTxMetadataAction {
type: typeof SET_TX_METADATA;
2024-07-25 18:41:01 -04:00
payload: TTXMetadata;
}
interface SetOrderAction {
type: typeof SET_ORDER;
2024-07-25 18:41:01 -04:00
payload: Item[];
}
type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction | SetTxMetadataAction | SetOrderAction;
2024-06-07 18:44:41 -04:00
interface State {
searchQuery: string;
isSearchFocused: boolean;
2024-07-24 20:53:40 -04:00
walletData: TWallet[];
txMetadata: TTXMetadata;
2024-07-25 18:41:01 -04:00
order: Item[];
2024-06-07 18:44:41 -04:00
}
const initialState: State = {
searchQuery: '',
isSearchFocused: false,
walletData: [],
txMetadata: {},
order: [],
2024-06-07 18:44:41 -04:00
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case SET_SEARCH_QUERY:
return { ...state, searchQuery: action.payload };
case SET_IS_SEARCH_FOCUSED:
return { ...state, isSearchFocused: action.payload };
case SET_WALLET_DATA:
return { ...state, walletData: action.payload };
case SET_TX_METADATA:
return { ...state, txMetadata: action.payload };
case SET_ORDER:
return { ...state, order: action.payload };
2024-06-07 18:44:41 -04:00
default:
return state;
}
};
2024-07-24 20:22:06 -04:00
const ManageWallets: React.FC = () => {
2024-06-07 18:44:41 -04:00
const sortableList = useRef(null);
const { colors, closeImage } = useTheme();
const { wallets, setWalletsWithNewOrder, txMetadata } = useStorage();
2024-06-07 18:44:41 -04:00
const colorScheme = useColorScheme();
2024-06-14 16:50:35 -04:00
const { navigate, setOptions, goBack } = useExtendedNavigation();
2024-06-07 18:44:41 -04:00
const [state, dispatch] = useReducer(reducer, initialState);
const stylesHook = {
root: {
backgroundColor: colors.elevated,
},
tip: {
backgroundColor: colors.ballOutgoingExpired,
},
2024-07-24 20:22:06 -04:00
noResultsText: {
color: colors.foregroundColor,
},
2024-06-07 18:44:41 -04:00
};
useEffect(() => {
2024-07-25 18:41:01 -04:00
const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
2024-06-07 18:44:41 -04:00
dispatch({ type: SET_WALLET_DATA, payload: wallets });
dispatch({ type: SET_TX_METADATA, payload: txMetadata });
2024-07-24 20:53:40 -04:00
dispatch({ type: SET_ORDER, payload: initialOrder });
}, [wallets, txMetadata]);
const handleClose = useCallback(() => {
2024-07-25 18:41:01 -04:00
const walletOrder = state.order.filter(item => item.type === ItemType.WalletSection).map(item => item.data);
setWalletsWithNewOrder(walletOrder);
goBack();
}, [goBack, setWalletsWithNewOrder, state.order]);
const HeaderRightButton = useMemo(
() => (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.close}
style={styles.button}
onPress={handleClose}
testID="NavigationCloseButton"
>
<Image source={closeImage} />
</TouchableOpacity>
),
[handleClose, closeImage],
);
2024-06-07 18:44:41 -04:00
useEffect(() => {
setOptions({
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
headerRight: () => HeaderRightButton,
2024-06-07 18:44:41 -04:00
});
}, [colorScheme, setOptions, HeaderRightButton]);
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
2024-06-07 18:44:41 -04:00
useEffect(() => {
if (debouncedSearchQuery) {
const filteredWallets = wallets.filter(wallet => wallet.getLabel()?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()));
const filteredTxMetadata = Object.entries(txMetadata).filter(([_, tx]) =>
tx.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()),
);
2024-07-25 18:41:01 -04:00
// Filter transactions
const filteredTransactions = wallets.flatMap(wallet =>
wallet
.getTransactions()
.filter((tx: Transaction) =>
filteredTxMetadata.some(
([txid, txMeta]) => tx.hash === txid && txMeta.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()),
),
),
);
const filteredOrder: Item[] = [
...filteredWallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet })),
...filteredTransactions.map(tx => ({ type: ItemType.TransactionSection, data: tx })),
2024-07-24 20:53:40 -04:00
];
2024-07-25 18:41:01 -04:00
dispatch({ type: SET_WALLET_DATA, payload: filteredWallets });
dispatch({ type: SET_TX_METADATA, payload: Object.fromEntries(filteredTxMetadata) });
2024-07-24 20:53:40 -04:00
dispatch({ type: SET_ORDER, payload: filteredOrder });
} else {
2024-07-25 18:41:01 -04:00
const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
dispatch({ type: SET_WALLET_DATA, payload: wallets });
dispatch({ type: SET_TX_METADATA, payload: {} });
2024-07-24 20:53:40 -04:00
dispatch({ type: SET_ORDER, payload: initialOrder });
}
}, [wallets, txMetadata, debouncedSearchQuery]);
2024-06-07 18:44:41 -04:00
useLayoutEffect(() => {
setOptions({
headerSearchBarOptions: {
hideWhenScrolling: false,
onChangeText: (event: { nativeEvent: { text: any } }) => dispatch({ type: SET_SEARCH_QUERY, payload: event.nativeEvent.text }),
onClear: () => dispatch({ type: SET_SEARCH_QUERY, payload: '' }),
onFocus: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: true }),
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
2024-07-26 19:35:53 -04:00
placeholder: loc.wallets.manage_wallets_search_placeholder,
2024-06-07 18:44:41 -04:00
},
});
}, [setOptions]);
const navigateToWallet = useCallback(
2024-07-25 18:41:01 -04:00
(wallet: TWallet) => {
2024-06-07 18:44:41 -04:00
const walletID = wallet.getID();
2024-06-14 16:55:51 -04:00
goBack();
2024-06-07 18:44:41 -04:00
navigate('WalletTransactions', {
walletID,
walletType: wallet.type,
});
},
2024-06-14 17:07:01 -04:00
[goBack, navigate],
2024-06-07 18:44:41 -04:00
);
const isDraggingDisabled = state.searchQuery.length > 0 || state.isSearchFocused;
const bounceAnim = useBounceAnimation(state.searchQuery);
const renderHighlightedText = useCallback(
(text: string, query: string) => {
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return (
<Text>
{parts.map((part, index) =>
2024-07-26 19:35:53 -04:00
query && part.toLowerCase().includes(query.toLowerCase()) ? (
<Animated.View key={`${index}-${query}`} style={[iStyles.highlightedContainer, { transform: [{ scale: bounceAnim }] }]}>
<Text style={iStyles.highlighted}>{part}</Text>
</Animated.View>
) : (
2024-07-26 19:35:53 -04:00
<Text key={`${index}-${query}`} style={query ? iStyles.dimmedText : iStyles.defaultText}>
{part}
</Text>
),
)}
</Text>
);
},
[bounceAnim],
);
2024-06-07 18:44:41 -04:00
const renderItem = useCallback(
2024-07-26 19:35:53 -04:00
// eslint-disable-next-line react/no-unused-prop-types
2024-07-25 18:41:01 -04:00
({ item, drag, isActive }: { item: Item; drag: () => void; isActive: boolean }) => {
if (item.type === ItemType.TransactionSection && item.data) {
2024-07-26 19:35:53 -04:00
const w = wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash));
const walletID = w ? w.getID() : '';
return (
2024-07-26 19:35:53 -04:00
<TransactionListItem
item={item.data}
itemPriceUnit={item.data.walletPreferredBalanceUnit || BitcoinUnit.BTC}
walletID={walletID}
searchQuery={state.searchQuery}
renderHighlightedText={renderHighlightedText}
/>
);
2024-07-25 18:41:01 -04:00
} else if (item.type === ItemType.WalletSection) {
return (
<ScaleDecorator>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
isActive={isActive}
onPress={() => navigateToWallet(item.data)}
2024-07-26 19:35:53 -04:00
customStyle={styles.padding16}
2024-07-25 18:41:01 -04:00
searchQuery={state.searchQuery}
2024-07-26 19:35:53 -04:00
renderHighlightedText={renderHighlightedText}
2024-07-25 18:41:01 -04:00
/>
</ScaleDecorator>
);
}
2024-07-25 18:41:01 -04:00
return null;
2024-06-07 18:44:41 -04:00
},
2024-07-26 19:35:53 -04:00
[wallets, isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText],
2024-06-07 18:44:41 -04:00
);
const onChangeOrder = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
}, []);
const onDragBegin = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.Selection);
}, []);
const onRelease = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}, []);
const onDragEnd = useCallback(({ data }: any) => {
dispatch({ type: SET_ORDER, payload: data });
}, []);
2024-06-07 18:44:41 -04:00
const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []);
const renderHeader = useMemo(() => {
if (!state.searchQuery) return null;
const hasWallets = state.walletData.length > 0;
2024-07-26 19:35:53 -04:00
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
);
const hasTransactions = filteredTxMetadata.length > 0;
return (
2024-07-26 19:48:26 -04:00
!hasWallets &&
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
);
2024-07-24 20:22:06 -04:00
}, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]);
2024-06-07 18:44:41 -04:00
return (
<GestureHandlerRootView style={[styles.root, stylesHook.root]}>
<DraggableFlatList
ref={sortableList}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
2024-07-24 20:53:40 -04:00
data={state.order}
2024-06-07 18:44:41 -04:00
keyExtractor={_keyExtractor}
renderItem={renderItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onRelease={onRelease}
onDragEnd={onDragEnd}
containerStyle={styles.root}
ListHeaderComponent={renderHeader}
2024-06-07 18:44:41 -04:00
/>
</GestureHandlerRootView>
);
};
2024-07-24 20:22:06 -04:00
export default ManageWallets;
2024-06-07 18:44:41 -04:00
const styles = StyleSheet.create({
root: {
flex: 1,
},
padding16: {
padding: 16,
},
button: {
padding: 16,
},
2024-07-25 01:54:51 -04:00
noResultsText: {
fontSize: 19,
fontWeight: 'bold',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
textAlign: 'center',
justifyContent: 'center',
marginTop: 34,
},
});
const iStyles = StyleSheet.create({
highlightedContainer: {
backgroundColor: 'white',
borderColor: 'black',
borderWidth: 1,
borderRadius: 5,
padding: 2,
2024-07-26 19:35:53 -04:00
alignSelf: 'flex-start',
textDecorationLine: 'underline',
textDecorationStyle: 'double',
textShadowColor: '#000',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 1,
},
highlighted: {
color: 'black',
2024-07-26 19:35:53 -04:00
fontSize: 19,
2024-07-25 18:41:01 -04:00
fontWeight: '600',
},
defaultText: {
2024-07-26 19:35:53 -04:00
fontSize: 19,
},
dimmedText: {
2024-07-26 19:35:53 -04:00
opacity: 0.8,
},
2024-06-07 18:44:41 -04:00
});