mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-12 10:30:36 +01:00
Merge pull request #6819 from BlueWallet/search
ADD: Search wallets, memos
This commit is contained in:
commit
c3d3e33824
13 changed files with 730 additions and 526 deletions
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -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,
|
||||||
)}
|
)}
|
||||||
|
|
26
hooks/useBounceAnimation.ts
Normal file
26
hooks/useBounceAnimation.ts
Normal 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;
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -100,5 +100,5 @@ export type DetailViewStackParamList = {
|
||||||
paymentCode: string;
|
paymentCode: string;
|
||||||
walletID: string;
|
walletID: string;
|
||||||
};
|
};
|
||||||
ReorderWallets: undefined;
|
ManageWallets: undefined;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
|
@ -210,5 +210,5 @@ export type DetailViewStackParamList = {
|
||||||
paymentCode: string;
|
paymentCode: string;
|
||||||
walletID: string;
|
walletID: string;
|
||||||
};
|
};
|
||||||
ReorderWallets: undefined;
|
ManageWallets: undefined;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
377
screen/wallets/ManageWallets.tsx
Normal file
377
screen/wallets/ManageWallets.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue