Merge pull request #6819 from BlueWallet/search

ADD: Search wallets, memos
This commit is contained in:
GLaDOS 2024-07-31 10:17:40 +00:00 committed by GitHub
commit c3d3e33824
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 730 additions and 526 deletions

View file

@ -21,7 +21,7 @@ interface ListItemProps {
switch?: object; // Define more specific type if needed switch?: object; // Define more specific type if needed
leftIcon?: any; // Define more specific type if needed leftIcon?: any; // Define more specific type if needed
title: string; title: string;
subtitle?: string; subtitle?: string | React.ReactNode;
subtitleNumberOfLines?: number; subtitleNumberOfLines?: number;
rightTitle?: string; rightTitle?: string;
rightTitleStyle?: object; rightTitleStyle?: object;

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard'; import Clipboard from '@react-native-clipboard/clipboard';
import { Linking, View } from 'react-native'; import { Linking, View, ViewStyle } from 'react-native';
import Lnurl from '../class/lnurl'; import Lnurl from '../class/lnurl';
import { LightningTransaction, Transaction } from '../class/wallets/types'; import { LightningTransaction, Transaction } from '../class/wallets/types';
import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon'; import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon';
@ -23,312 +23,321 @@ import { DetailViewStackParamList } from '../navigation/DetailViewStackParamList
import { useStorage } from '../hooks/context/useStorage'; import { useStorage } from '../hooks/context/useStorage';
import ToolTipMenu from './TooltipMenu'; import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions'; import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { pop } from '../NavigationService';
interface TransactionListItemProps { interface TransactionListItemProps {
itemPriceUnit: BitcoinUnit; itemPriceUnit: BitcoinUnit;
walletID: string; walletID: string;
item: Transaction & LightningTransaction; // using type intersection to have less issues with ts item: Transaction & LightningTransaction; // using type intersection to have less issues with ts
searchQuery?: string;
style?: ViewStyle;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
} }
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>; type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, walletID }) => { export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
const { colors } = useTheme(); const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
const { navigate } = useExtendedNavigation<NavigationProps>(); const { colors } = useTheme();
const menuRef = useRef<ToolTipMenuProps>(); const { navigate } = useExtendedNavigation<NavigationProps>();
const { txMetadata, counterpartyMetadata, wallets } = useStorage(); const menuRef = useRef<ToolTipMenuProps>();
const { language } = useSettings(); const { txMetadata, counterpartyMetadata, wallets } = useStorage();
const containerStyle = useMemo( const { language } = useSettings();
() => ({ const containerStyle = useMemo(
backgroundColor: 'transparent', () => ({
borderBottomColor: colors.lightBorder, backgroundColor: 'transparent',
}), borderBottomColor: colors.lightBorder,
[colors.lightBorder], }),
); [colors.lightBorder],
);
const shortenContactName = (name: string): string => { const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]);
if (name.length < 16) return name;
return name.substr(0, 7) + '...' + name.substr(name.length - 7, 7);
};
const title = useMemo(() => { const shortenContactName = (name: string): string => {
if (item.confirmations === 0) { if (name.length < 16) return name;
return loc.transactions.pending; return name.substr(0, 7) + '...' + name.substr(name.length - 7, 7);
} else { };
return transactionTimeToReadable(item.received!);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.confirmations, item.received, language]);
let counterparty; const title = useMemo(() => {
if (item.counterparty) { if (item.confirmations === 0) {
counterparty = counterpartyMetadata?.[item.counterparty]?.label ?? item.counterparty; return loc.transactions.pending;
}
const txMemo = (counterparty ? `[${shortenContactName(counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? '');
const subtitle = useMemo(() => {
let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
if (sub !== '') sub += ' ';
sub += txMemo;
if (item.memo) sub += item.memo;
return sub || undefined;
}, [txMemo, item.confirmations, item.memo]);
const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(Number(item.value))) {
item.value = 0;
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else { } else {
if (item.ispaid) { return transactionTimeToReadable(item.received!);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.confirmations, item.received, language]);
let counterparty;
if (item.counterparty) {
counterparty = counterpartyMetadata?.[item.counterparty]?.label ?? item.counterparty;
}
const txMemo = (counterparty ? `[${shortenContactName(counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? '');
const subtitle = useMemo(() => {
let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
if (sub !== '') sub += ' ';
sub += txMemo;
if (item.memo) sub += item.memo;
return sub || undefined;
}, [txMemo, item.confirmations, item.memo]);
const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(Number(item.value))) {
item.value = 0;
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else { } else {
return loc.lnd.expired; if (item.ispaid) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
} }
} else {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} }
} else { }, [item, itemPriceUnit]);
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}
}, [item, itemPriceUnit]);
const rowTitleStyle = useMemo(() => { const rowTitleStyle = useMemo(() => {
let color = colors.successColor; let color = colors.successColor;
if (item.type === 'user_invoice' || item.type === 'payment_request') { if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date(); const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!; const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) { if (invoiceExpiration > now) {
color = colors.successColor;
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = colors.successColor; color = colors.successColor;
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = colors.successColor;
} else {
color = '#9AA0AA';
}
}
} else if (item.value! / 100000000 < 0) {
color = colors.foregroundColor;
}
return {
color,
fontSize: 14,
fontWeight: '600',
textAlign: 'right',
};
}, [item, colors.foregroundColor, colors.successColor]);
const determineTransactionTypeAndAvatar = () => {
if (item.category === 'receive' && item.confirmations! < 3) {
return {
label: loc.transactions.pending_transaction,
icon: <TransactionPendingIcon />,
};
}
if (item.type && item.type === 'bitcoind_tx') {
return {
label: loc.transactions.onchain,
icon: <TransactionOnchainIcon />,
};
}
if (item.type === 'paid_invoice') {
return {
label: loc.transactions.offchain,
icon: <TransactionOffchainIcon />,
};
}
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (!item.ispaid && invoiceExpiration < now) {
return {
label: loc.transactions.expired_transaction,
icon: <TransactionExpiredIcon />,
};
} else { } else {
color = '#9AA0AA'; return {
label: loc.transactions.incoming_transaction,
icon: <TransactionOffchainIncomingIcon />,
};
} }
} }
} else if (item.value! / 100000000 < 0) {
color = colors.foregroundColor;
}
return { if (!item.confirmations) {
color,
fontSize: 14,
fontWeight: '600',
textAlign: 'right',
};
}, [item, colors.foregroundColor, colors.successColor]);
const determineTransactionTypeAndAvatar = () => {
if (item.category === 'receive' && item.confirmations! < 3) {
return {
label: loc.transactions.pending_transaction,
icon: <TransactionPendingIcon />,
};
}
if (item.type && item.type === 'bitcoind_tx') {
return {
label: loc.transactions.onchain,
icon: <TransactionOnchainIcon />,
};
}
if (item.type === 'paid_invoice') {
return {
label: loc.transactions.offchain,
icon: <TransactionOffchainIcon />,
};
}
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (!item.ispaid && invoiceExpiration < now) {
return { return {
label: loc.transactions.expired_transaction, label: loc.transactions.pending_transaction,
icon: <TransactionExpiredIcon />, icon: <TransactionPendingIcon />,
};
} else if (item.value! < 0) {
return {
label: loc.transactions.outgoing_transaction,
icon: <TransactionOutgoingIcon />,
}; };
} else { } else {
return { return {
label: loc.transactions.incoming_transaction, label: loc.transactions.incoming_transaction,
icon: <TransactionOffchainIncomingIcon />, icon: <TransactionIncomingIcon />,
}; };
} }
}
if (!item.confirmations) {
return {
label: loc.transactions.pending_transaction,
icon: <TransactionPendingIcon />,
};
} else if (item.value! < 0) {
return {
label: loc.transactions.outgoing_transaction,
icon: <TransactionOutgoingIcon />,
};
} else {
return {
label: loc.transactions.incoming_transaction,
icon: <TransactionIncomingIcon />,
};
}
};
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
const amountWithUnit = useMemo(() => {
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
return `${amount}${unit}`;
}, [item.value, itemPriceUnit]);
useEffect(() => {
setSubtitleNumberOfLines(1);
}, [subtitle]);
const onPress = useCallback(async () => {
menuRef?.current?.dismissMenu?.();
if (item.hash) {
navigate('TransactionStatus', { hash: item.hash, walletID });
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID);
if (lightningWallet.length === 1) {
try {
// is it a successful lnurl-pay?
const LN = new Lnurl(false, AsyncStorage);
let paymentHash = item.payment_hash!;
if (typeof paymentHash === 'object') {
paymentHash = Buffer.from(paymentHash.data).toString('hex');
}
const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) {
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPaySuccess',
params: {
paymentHash,
justPaid: false,
fromWalletID: lightningWallet[0].getID(),
},
});
return;
}
} catch (e) {
console.log(e);
}
navigate('LNDViewInvoice', {
invoice: item,
walletID: lightningWallet[0].getID(),
});
}
}
}, [item, wallets, navigate, walletID]);
const handleOnExpandNote = useCallback(() => {
setSubtitleNumberOfLines(0);
}, []);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]);
const handleOnViewOnBlockExplorer = useCallback(() => {
const url = `https://mempool.space/tx/${item.hash}`;
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url);
}
});
}, [item.hash]);
const handleCopyOpenInBlockExplorerPress = useCallback(() => {
Clipboard.setString(`https://mempool.space/tx/${item.hash}`);
}, [item.hash]);
const onToolTipPress = useCallback(
(id: any) => {
if (id === CommonToolTipActions.CopyAmount.id) {
handleOnCopyAmountTap();
} else if (id === CommonToolTipActions.CopyNote.id) {
handleOnCopyNote();
} else if (id === CommonToolTipActions.OpenInBlockExplorer.id) {
handleOnViewOnBlockExplorer();
} else if (id === CommonToolTipActions.ExpandNote.id) {
handleOnExpandNote();
} else if (id === CommonToolTipActions.CopyBlockExplorerLink.id) {
handleCopyOpenInBlockExplorerPress();
} else if (id === CommonToolTipActions.CopyTXID.id) {
handleOnCopyTransactionID();
}
},
[
handleCopyOpenInBlockExplorerPress,
handleOnCopyAmountTap,
handleOnCopyNote,
handleOnCopyTransactionID,
handleOnExpandNote,
handleOnViewOnBlockExplorer,
],
);
const toolTipActions = useMemo((): Action[] | Action[][] => {
const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) {
actions.push(CommonToolTipActions.CopyAmount);
}
if (subtitle) {
actions.push(CommonToolTipActions.CopyNote);
}
if (item.hash) {
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
}
if (subtitle && subtitleNumberOfLines === 1) {
actions.push([CommonToolTipActions.ExpandNote]);
}
return actions as Action[] | Action[][];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
const accessibilityState = useMemo(() => {
return {
expanded: subtitleNumberOfLines === 0,
}; };
}, [subtitleNumberOfLines]);
return ( const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
<ToolTipMenu
isButton const amountWithUnit = useMemo(() => {
actions={toolTipActions} const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
onPressMenuItem={onToolTipPress} const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
onPress={onPress} return `${amount}${unit}`;
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`} }, [item.value, itemPriceUnit]);
accessibilityRole="button"
accessibilityState={accessibilityState} useEffect(() => {
> setSubtitleNumberOfLines(1);
<ListItem }, [subtitle]);
leftAvatar={avatar}
title={title} const onPress = useCallback(async () => {
subtitleNumberOfLines={subtitleNumberOfLines} menuRef?.current?.dismissMenu?.();
subtitle={subtitle} if (item.hash) {
Component={View} pop();
subtitleProps={subtitleProps} navigate('TransactionStatus', { hash: item.hash, walletID });
chevron={false} } else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
rightTitle={rowTitle} const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID);
rightTitleStyle={rowTitleStyle} if (lightningWallet.length === 1) {
containerStyle={containerStyle} try {
/> // is it a successful lnurl-pay?
</ToolTipMenu> const LN = new Lnurl(false, AsyncStorage);
); let paymentHash = item.payment_hash!;
}); if (typeof paymentHash === 'object') {
paymentHash = Buffer.from(paymentHash.data).toString('hex');
}
const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) {
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPaySuccess',
params: {
paymentHash,
justPaid: false,
fromWalletID: lightningWallet[0].getID(),
},
});
return;
}
} catch (e) {
console.debug(e);
}
navigate('LNDViewInvoice', {
invoice: item,
walletID: lightningWallet[0].getID(),
});
}
}
}, [item, wallets, navigate, walletID]);
const handleOnExpandNote = useCallback(() => {
setSubtitleNumberOfLines(0);
}, []);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]);
const handleOnViewOnBlockExplorer = useCallback(() => {
const url = `https://mempool.space/tx/${item.hash}`;
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url);
}
});
}, [item.hash]);
const handleCopyOpenInBlockExplorerPress = useCallback(() => {
Clipboard.setString(`https://mempool.space/tx/${item.hash}`);
}, [item.hash]);
const onToolTipPress = useCallback(
(id: any) => {
if (id === CommonToolTipActions.CopyAmount.id) {
handleOnCopyAmountTap();
} else if (id === CommonToolTipActions.CopyNote.id) {
handleOnCopyNote();
} else if (id === CommonToolTipActions.OpenInBlockExplorer.id) {
handleOnViewOnBlockExplorer();
} else if (id === CommonToolTipActions.ExpandNote.id) {
handleOnExpandNote();
} else if (id === CommonToolTipActions.CopyBlockExplorerLink.id) {
handleCopyOpenInBlockExplorerPress();
} else if (id === CommonToolTipActions.CopyTXID.id) {
handleOnCopyTransactionID();
}
},
[
handleCopyOpenInBlockExplorerPress,
handleOnCopyAmountTap,
handleOnCopyNote,
handleOnCopyTransactionID,
handleOnExpandNote,
handleOnViewOnBlockExplorer,
],
);
const toolTipActions = useMemo((): Action[] | Action[][] => {
const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) {
actions.push(CommonToolTipActions.CopyAmount);
}
if (subtitle) {
actions.push(CommonToolTipActions.CopyNote);
}
if (item.hash) {
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
}
if (subtitle && subtitleNumberOfLines === 1) {
actions.push([CommonToolTipActions.ExpandNote]);
}
return actions as Action[] | Action[][];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
const accessibilityState = useMemo(() => {
return {
expanded: subtitleNumberOfLines === 0,
};
}, [subtitleNumberOfLines]);
return (
<ToolTipMenu
isButton
actions={toolTipActions}
onPressMenuItem={onToolTipPress}
onPress={onPress}
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
accessibilityRole="button"
accessibilityState={accessibilityState}
>
<ListItem
leftAvatar={avatar}
title={title}
subtitleNumberOfLines={subtitleNumberOfLines}
subtitle={subtitle ? (renderHighlightedText ? renderHighlightedText(subtitle, searchQuery ?? '') : subtitle) : undefined}
Component={View}
subtitleProps={subtitleProps}
chevron={false}
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}
containerStyle={combinedStyle}
/>
</ToolTipMenu>
);
},
);

View file

@ -8,7 +8,6 @@ import {
Pressable, Pressable,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity,
useWindowDimensions, useWindowDimensions,
View, View,
FlatListProps, FlatListProps,
@ -73,12 +72,7 @@ const NewWalletPanel: React.FC<NewWalletPanelProps> = ({ onPress }) => {
}); });
return ( return (
<TouchableOpacity <Pressable accessibilityRole="button" testID="CreateAWallet" onPress={onPress} style={isLargeScreen ? {} : { width: itemWidth * 1.2 }}>
accessibilityRole="button"
testID="CreateAWallet"
onPress={onPress}
style={isLargeScreen ? {} : { width: itemWidth * 1.2 }}
>
<View <View
style={[ style={[
nStyles.container, nStyles.container,
@ -93,7 +87,7 @@ const NewWalletPanel: React.FC<NewWalletPanelProps> = ({ onPress }) => {
<Text style={[nStyles.buttonText, { color: colors.brandingColor }]}>{loc.wallets.list_create_a_button}</Text> <Text style={[nStyles.buttonText, { color: colors.brandingColor }]}>{loc.wallets.list_create_a_button}</Text>
</View> </View>
</View> </View>
</TouchableOpacity> </Pressable>
); );
}; };
@ -105,7 +99,8 @@ interface WalletCarouselItemProps {
customStyle?: ViewStyle; customStyle?: ViewStyle;
horizontal?: boolean; horizontal?: boolean;
isActive?: boolean; isActive?: boolean;
allowOnPressAnimation?: boolean; searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
} }
const iStyles = StyleSheet.create({ const iStyles = StyleSheet.create({
@ -168,7 +163,7 @@ const iStyles = StyleSheet.create({
}); });
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo( export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, allowOnPressAnimation = true }) => { ({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, searchQuery, renderHighlightedText }) => {
const scaleValue = useRef(new Animated.Value(1.0)).current; const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme(); const { colors } = useTheme();
const { walletTransactionUpdateStatus } = useStorage(); const { walletTransactionUpdateStatus } = useStorage();
@ -233,8 +228,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
testID={item.getLabel()} testID={item.getLabel()}
onPressIn={allowOnPressAnimation ? onPressedIn : undefined} onPressIn={onPressedIn}
onPressOut={allowOnPressAnimation ? onPressedOut : undefined} onPressOut={onPressedOut}
onLongPress={() => { onLongPress={() => {
if (handleLongPress) handleLongPress(); if (handleLongPress) handleLongPress();
}} }}
@ -245,7 +240,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
<Image source={image} style={iStyles.image} /> <Image source={image} style={iStyles.image} />
<Text style={iStyles.br} /> <Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}> <Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{item.getLabel()} {renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()}
</Text> </Text>
<View style={iStyles.balanceContainer}> <View style={iStyles.balanceContainer}>
{item.hideBalance ? ( {item.hideBalance ? (
@ -287,6 +282,8 @@ interface WalletsCarouselProps extends Partial<FlatListProps<any>> {
handleLongPress?: () => void; handleLongPress?: () => void;
data: TWallet[]; data: TWallet[];
scrollEnabled?: boolean; scrollEnabled?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
} }
type FlatListRefType = FlatList<any> & { type FlatListRefType = FlatList<any> & {
@ -315,7 +312,17 @@ const cStyles = StyleSheet.create({
const ListHeaderComponent: React.FC = () => <View style={cStyles.separatorStyle} />; const ListHeaderComponent: React.FC = () => <View style={cStyles.separatorStyle} />;
const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => { const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => {
const { horizontal, data, handleLongPress, onPress, selectedWallet, scrollEnabled, onNewWalletPress } = props; const {
horizontal,
data,
handleLongPress,
onPress,
selectedWallet,
scrollEnabled,
onNewWalletPress,
searchQuery,
renderHighlightedText,
} = props;
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: ListRenderItemInfo<TWallet>) => ({ item, index }: ListRenderItemInfo<TWallet>) =>
item ? ( item ? (
@ -325,9 +332,11 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
handleLongPress={handleLongPress} handleLongPress={handleLongPress}
onPress={onPress} onPress={onPress}
horizontal={horizontal} horizontal={horizontal}
searchQuery={searchQuery}
renderHighlightedText={renderHighlightedText}
/> />
) : null, ) : null,
[horizontal, selectedWallet, handleLongPress, onPress], [horizontal, selectedWallet, handleLongPress, onPress, searchQuery, renderHighlightedText],
); );
const flatListRef = useRef<FlatList<any>>(null); const flatListRef = useRef<FlatList<any>>(null);
@ -401,6 +410,8 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
handleLongPress={handleLongPress} handleLongPress={handleLongPress}
onPress={onPress} onPress={onPress}
key={index} key={index}
searchQuery={props.searchQuery}
renderHighlightedText={props.renderHighlightedText}
/> />
) : null, ) : null,
)} )}

View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
import { Animated } from 'react-native';
const useBounceAnimation = (query: string) => {
const bounceAnim = useRef(new Animated.Value(1.0)).current;
useEffect(() => {
if (query) {
Animated.timing(bounceAnim, {
toValue: 1.2,
duration: 150,
useNativeDriver: true,
}).start(() => {
Animated.timing(bounceAnim, {
toValue: 1.0,
duration: 150,
useNativeDriver: true,
}).start();
});
}
}, [bounceAnim, query]);
return bounceAnim;
};
export default useBounceAnimation;

View file

@ -467,7 +467,8 @@
"list_tryagain": "Try again", "list_tryagain": "Try again",
"no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.", "no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.",
"looks_like_bip38": "This looks like a password-protected private key (BIP38).", "looks_like_bip38": "This looks like a password-protected private key (BIP38).",
"reorder_title": "Re-order Wallets", "manage_title": "Manage Wallets",
"no_results_found": "No results found.",
"please_continue_scanning": "Please continue scanning.", "please_continue_scanning": "Please continue scanning.",
"select_no_bitcoin": "There are currently no Bitcoin wallets available.", "select_no_bitcoin": "There are currently no Bitcoin wallets available.",
"select_no_bitcoin_exp": "A Bitcoin wallet is required to refill Lightning wallets. Please create or import one.", "select_no_bitcoin_exp": "A Bitcoin wallet is required to refill Lightning wallets. Please create or import one.",
@ -478,7 +479,7 @@
"add_ln_wallet_first": "You must first add a Lightning wallet.", "add_ln_wallet_first": "You must first add a Lightning wallet.",
"identity_pubkey": "Identity Pubkey", "identity_pubkey": "Identity Pubkey",
"xpub_title": "Wallet XPUB", "xpub_title": "Wallet XPUB",
"search_wallets": "Search Wallets" "manage_wallets_search_placeholder": "Search wallets, memos"
}, },
"multisig": { "multisig": {
"multisig_vault": "Vault", "multisig_vault": "Vault",

View file

@ -52,7 +52,6 @@ import {
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack'; import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack'; import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
import ReceiveDetailsStackRoot from './ReceiveDetailsStack'; import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
import ReorderWalletsStackRoot from './ReorderWalletsStack';
import ScanLndInvoiceRoot from './ScanLndInvoiceStack'; import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
import ScanQRCodeStackRoot from './ScanQRCodeStack'; import ScanQRCodeStackRoot from './ScanQRCodeStack';
import SendDetailsStack from './SendDetailsStack'; import SendDetailsStack from './SendDetailsStack';
@ -63,6 +62,7 @@ import WalletXpubStackRoot from './WalletXpubStack';
import PlusIcon from '../components/icons/PlusIcon'; import PlusIcon from '../components/icons/PlusIcon';
import SettingsButton from '../components/icons/SettingsButton'; import SettingsButton from '../components/icons/SettingsButton';
import ExportMultisigCoordinationSetupStack from './ExportMultisigCoordinationSetupStack'; import ExportMultisigCoordinationSetupStack from './ExportMultisigCoordinationSetupStack';
import ManageWallets from '../screen/wallets/ManageWallets';
const DetailViewStackScreensStack = () => { const DetailViewStackScreensStack = () => {
const theme = useTheme(); const theme = useTheme();
@ -362,13 +362,15 @@ const DetailViewStackScreensStack = () => {
}} }}
/> />
<DetailViewStack.Screen <DetailViewStack.Screen
name="ReorderWallets" name="ManageWallets"
component={ReorderWalletsStackRoot} component={ManageWallets}
options={{ options={navigationStyle({
headerShown: false, headerBackVisible: false,
headerLargeTitle: true,
gestureEnabled: false, gestureEnabled: false,
presentation: 'modal', presentation: 'modal',
}} title: loc.wallets.manage_title,
})(theme)}
/> />
</DetailViewStack.Navigator> </DetailViewStack.Navigator>
); );

View file

@ -100,5 +100,5 @@ export type DetailViewStackParamList = {
paymentCode: string; paymentCode: string;
walletID: string; walletID: string;
}; };
ReorderWallets: undefined; ManageWallets: undefined;
}; };

View file

@ -1,30 +0,0 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import loc from '../loc';
import ReorderWallets from '../screen/wallets/ReorderWallets';
const Stack = createNativeStackNavigator();
const ReorderWalletsStackRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ReorderWalletsScreen"
component={ReorderWallets}
options={navigationStyle({
headerBackVisible: false,
headerLargeTitle: true,
headerTitle: loc.wallets.reorder_title,
})(theme)}
/>
</Stack.Navigator>
);
};
export default ReorderWalletsStackRoot;

View file

@ -210,5 +210,5 @@ export type DetailViewStackParamList = {
paymentCode: string; paymentCode: string;
walletID: string; walletID: string;
}; };
ReorderWallets: undefined; ManageWallets: undefined;
}; };

View file

@ -124,7 +124,7 @@ const DrawerList: React.FC<DrawerListProps> = memo(({ navigation }) => {
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {
if (state.wallets.length > 1) { if (state.wallets.length > 1) {
navigation.navigate('ReorderWallets'); navigation.navigate('ManageWallets');
} else { } else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
} }

View file

@ -0,0 +1,377 @@
import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react';
import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native';
// @ts-ignore: no declaration file
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';
import { WalletCarouselItem } from '../../components/WalletsCarousel';
import { TransactionListItem } from '../../components/TransactionListItem';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import useDebounce from '../../hooks/useDebounce';
import { TTXMetadata } from '../../class';
import { ExtendedTransaction, LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
import { BitcoinUnit } from '../../models/bitcoinUnits';
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;
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';
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;
payload: TWallet[];
}
interface SetTxMetadataAction {
type: typeof SET_TX_METADATA;
payload: TTXMetadata;
}
interface SetOrderAction {
type: typeof SET_ORDER;
payload: Item[];
}
type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction | SetTxMetadataAction | SetOrderAction;
interface State {
searchQuery: string;
isSearchFocused: boolean;
walletData: TWallet[];
txMetadata: TTXMetadata;
order: Item[];
}
const initialState: State = {
searchQuery: '',
isSearchFocused: false,
walletData: [],
txMetadata: {},
order: [],
};
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 };
default:
return state;
}
};
const ManageWallets: React.FC = () => {
const sortableList = useRef(null);
const { colors, closeImage } = useTheme();
const { wallets, setWalletsWithNewOrder, txMetadata } = useStorage();
const colorScheme = useColorScheme();
const { navigate, setOptions, goBack } = useExtendedNavigation();
const [state, dispatch] = useReducer(reducer, initialState);
const stylesHook = {
root: {
backgroundColor: colors.elevated,
},
tip: {
backgroundColor: colors.ballOutgoingExpired,
},
noResultsText: {
color: colors.foregroundColor,
},
};
useEffect(() => {
const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
dispatch({ type: SET_WALLET_DATA, payload: wallets });
dispatch({ type: SET_TX_METADATA, payload: txMetadata });
dispatch({ type: SET_ORDER, payload: initialOrder });
}, [wallets, txMetadata]);
const handleClose = useCallback(() => {
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],
);
useEffect(() => {
setOptions({
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
headerRight: () => HeaderRightButton,
});
}, [colorScheme, setOptions, HeaderRightButton]);
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
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()),
);
// 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 })),
];
dispatch({ type: SET_WALLET_DATA, payload: filteredWallets });
dispatch({ type: SET_TX_METADATA, payload: Object.fromEntries(filteredTxMetadata) });
dispatch({ type: SET_ORDER, payload: filteredOrder });
} else {
const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
dispatch({ type: SET_WALLET_DATA, payload: wallets });
dispatch({ type: SET_TX_METADATA, payload: {} });
dispatch({ type: SET_ORDER, payload: initialOrder });
}
}, [wallets, txMetadata, debouncedSearchQuery]);
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 }),
placeholder: loc.wallets.manage_wallets_search_placeholder,
},
});
}, [setOptions]);
const navigateToWallet = useCallback(
(wallet: TWallet) => {
const walletID = wallet.getID();
goBack();
navigate('WalletTransactions', {
walletID,
walletType: wallet.type,
});
},
[goBack, navigate],
);
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) =>
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>
) : (
<Text key={`${index}-${query}`} style={query ? iStyles.dimmedText : iStyles.defaultText}>
{part}
</Text>
),
)}
</Text>
);
},
[bounceAnim],
);
const renderItem = useCallback(
// eslint-disable-next-line react/no-unused-prop-types
({ item, drag, isActive }: { item: Item; drag: () => void; isActive: boolean }) => {
if (item.type === ItemType.TransactionSection && item.data) {
const w = wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash));
const walletID = w ? w.getID() : '';
return (
<TransactionListItem
item={item.data}
itemPriceUnit={item.data.walletPreferredBalanceUnit || BitcoinUnit.BTC}
walletID={walletID}
searchQuery={state.searchQuery}
renderHighlightedText={renderHighlightedText}
/>
);
} else if (item.type === ItemType.WalletSection) {
return (
<ScaleDecorator>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
isActive={isActive}
onPress={() => navigateToWallet(item.data)}
customStyle={styles.padding16}
searchQuery={state.searchQuery}
renderHighlightedText={renderHighlightedText}
/>
</ScaleDecorator>
);
}
return null;
},
[wallets, isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText],
);
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 });
}, []);
const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []);
const renderHeader = useMemo(() => {
if (!state.searchQuery) return null;
const hasWallets = state.walletData.length > 0;
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
);
const hasTransactions = filteredTxMetadata.length > 0;
return (
!hasWallets &&
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
);
}, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]);
return (
<GestureHandlerRootView style={[styles.root, stylesHook.root]}>
<DraggableFlatList
ref={sortableList}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
data={state.order}
keyExtractor={_keyExtractor}
renderItem={renderItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onRelease={onRelease}
onDragEnd={onDragEnd}
containerStyle={styles.root}
ListHeaderComponent={renderHeader}
/>
</GestureHandlerRootView>
);
};
export default ManageWallets;
const styles = StyleSheet.create({
root: {
flex: 1,
},
padding16: {
padding: 16,
},
button: {
padding: 16,
},
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,
alignSelf: 'flex-start',
textDecorationLine: 'underline',
textDecorationStyle: 'double',
textShadowColor: '#000',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 1,
},
highlighted: {
color: 'black',
fontSize: 19,
fontWeight: '600',
},
defaultText: {
fontSize: 19,
},
dimmedText: {
opacity: 0.8,
},
});

View file

@ -1,192 +0,0 @@
import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback } from 'react';
import { Platform, StyleSheet, useColorScheme } from 'react-native';
// @ts-ignore: fix later
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';
import { WalletCarouselItem } from '../../components/WalletsCarousel';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
// Action Types
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED';
const SET_WALLET_DATA = 'SET_WALLET_DATA';
// Action Interfaces
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;
payload: any[];
}
type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction;
// State Interface
interface State {
searchQuery: string;
isSearchFocused: boolean;
walletData: any[];
}
// Initial State
const initialState: State = {
searchQuery: '',
isSearchFocused: false,
walletData: [],
};
// Reducer
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 };
default:
return state;
}
};
const ReorderWallets: React.FC = () => {
const sortableList = useRef(null);
const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder } = useStorage();
const colorScheme = useColorScheme();
const { navigate, setOptions, goBack } = useExtendedNavigation();
const [state, dispatch] = useReducer(reducer, initialState);
const stylesHook = {
root: {
backgroundColor: colors.elevated,
},
tip: {
backgroundColor: colors.ballOutgoingExpired,
},
};
useEffect(() => {
dispatch({ type: SET_WALLET_DATA, payload: wallets });
}, [wallets]);
useEffect(() => {
setOptions({
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
});
}, [colorScheme, setOptions]);
useEffect(() => {
const filteredWallets = wallets.filter(wallet => wallet.getLabel().toLowerCase().includes(state.searchQuery.toLowerCase()));
dispatch({ type: SET_WALLET_DATA, payload: filteredWallets });
}, [wallets, state.searchQuery]);
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 }),
placeholder: loc.wallets.search_wallets,
},
});
}, [setOptions]);
const navigateToWallet = useCallback(
(wallet: any) => {
const walletID = wallet.getID();
goBack();
navigate('WalletTransactions', {
walletID,
walletType: wallet.type,
});
},
[goBack, navigate],
);
const isDraggingDisabled = state.searchQuery.length > 0 || state.isSearchFocused;
const renderItem = useCallback(
({ item, drag, isActive }: any) => {
const itemOpacity = isActive ? 1 : 0.5;
return (
<ScaleDecorator>
<WalletCarouselItem
item={item}
handleLongPress={isDraggingDisabled ? null : drag}
isActive={isActive}
onPress={navigateToWallet}
customStyle={StyleSheet.flatten([styles.padding16, { opacity: itemOpacity }])}
/>
</ScaleDecorator>
);
},
[isDraggingDisabled, navigateToWallet],
);
const onChangeOrder = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
}, []);
const onDragBegin = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.Selection);
}, []);
const onRelease = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}, []);
const onDragEnd = useCallback(
({ data }: any) => {
setWalletsWithNewOrder(data);
dispatch({ type: SET_WALLET_DATA, payload: data });
},
[setWalletsWithNewOrder],
);
const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []);
return (
<GestureHandlerRootView style={[styles.root, stylesHook.root]}>
<DraggableFlatList
ref={sortableList}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
data={state.walletData}
keyExtractor={_keyExtractor}
renderItem={renderItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onRelease={onRelease}
onDragEnd={onDragEnd}
containerStyle={styles.root}
/>
</GestureHandlerRootView>
);
};
export default ReorderWallets;
const styles = StyleSheet.create({
root: {
flex: 1,
},
padding16: {
padding: 16,
},
});

View file

@ -239,7 +239,7 @@ const WalletsList: React.FC = () => {
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {
if (wallets.length > 1) { if (wallets.length > 1) {
navigate('ReorderWallets'); navigate('ManageWallets');
} else { } else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
} }