diff --git a/components/addresses/AddressItem.tsx b/components/addresses/AddressItem.tsx index 4216853f9..74a009a1b 100644 --- a/components/addresses/AddressItem.tsx +++ b/components/addresses/AddressItem.tsx @@ -89,7 +89,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad ...CommonToolTipActions.ExportPrivateKey, hidden: !allowSignVerifyMessage, }, - ].filter(action => !action.hidden), + ].filter(action => 'hidden' in action && !action.hidden), [allowSignVerifyMessage], ); diff --git a/loc/en.json b/loc/en.json index 565b18cab..b21f836c2 100644 --- a/loc/en.json +++ b/loc/en.json @@ -592,7 +592,13 @@ "header": "Coin Control", "use_coin": "Use Coin", "use_coins": "Use Coins", - "tip": "This feature allows you to see, label, freeze or select coins for improved wallet management. You can select multiple coins by tapping on the colored circles." + "tip": "This feature allows you to see, label, freeze or select coins for improved wallet management. You can select multiple coins by tapping on the colored circles.", + "sort_asc": "Ascending", + "sort_desc": "Descending", + "sort_height": "by Height", + "sort_value": "by Value", + "sort_label": "by Label", + "sort_status": "by Status" }, "units": { "BTC": "BTC", @@ -654,4 +660,4 @@ "notif_tx": "Notification transaction", "not_found": "Payment code not found" } -} \ No newline at end of file +} diff --git a/screen/send/CoinControl.tsx b/screen/send/CoinControl.tsx index e111993fe..a49a403bc 100644 --- a/screen/send/CoinControl.tsx +++ b/screen/send/CoinControl.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { RouteProp, useRoute } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Avatar, Badge, Icon, ListItem as RNElementsListItem } from '@rneui/themed'; @@ -24,14 +24,17 @@ import { TWallet, Utxo } from '../../class/wallets/types'; import BottomModal, { BottomModalHandle } from '../../components/BottomModal'; import Button from '../../components/Button'; import { FButton, FContainer } from '../../components/FloatButtons'; +import HeaderMenuButton from '../../components/HeaderMenuButton'; import ListItem from '../../components/ListItem'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; +import { Action } from '../../components/types'; import { useStorage } from '../../hooks/context/useStorage'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc, { formatBalance } from '../../loc'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList'; +import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; type NavigationProps = NativeStackNavigationProp; type RouteProps = RouteProp; @@ -256,6 +259,18 @@ const OutputModalContent: React.FC = ({ output, wallet ); }; +enum ESortDirections { + asc = 'asc', + desc = 'desc', +} + +enum ESortTypes { + height = 'height', + label = 'label', + value = 'value', + frozen = 'frozen', +} + const CoinControl: React.FC = () => { const { colors } = useTheme(); const navigation = useExtendedNavigation(); @@ -263,15 +278,42 @@ const CoinControl: React.FC = () => { const bottomModalRef = useRef(null); const { walletID } = useRoute().params; const { wallets, saveToDisk, sleep } = useStorage(); - const wallet = wallets.find(w => w.getID() === walletID) as TWallet; - // sort by height ascending, txid , vout ascending - const utxos: Utxo[] = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout); + const [sortDirection, setSortDirection] = useState(ESortDirections.asc); + const [sortType, setSortType] = useState(ESortTypes.height); + const wallet = useMemo(() => wallets.find(w => w.getID() === walletID) as TWallet, [walletID, wallets]); + const [frozen, setFrozen] = useState( + wallet + .getUtxo(true) + .filter(out => wallet.getUTXOMetadata(out.txid, out.vout).frozen) + .map(({ txid, vout }) => `${txid}:${vout}`), + ); + const utxos: Utxo[] = useMemo(() => { + const res = wallet.getUtxo(true).sort((a, b) => { + switch (sortType) { + case ESortTypes.height: + return a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout; + case ESortTypes.value: + return a.value - b.value || a.txid.localeCompare(b.txid) || a.vout - b.vout; + case ESortTypes.label: { + const aMemo = wallet.getUTXOMetadata(a.txid, a.vout).memo || ''; + const bMemo = wallet.getUTXOMetadata(b.txid, b.vout).memo || ''; + return aMemo.localeCompare(bMemo) || a.txid.localeCompare(b.txid) || a.vout - b.vout; + } + case ESortTypes.frozen: { + const aF = frozen.includes(`${a.txid}:${a.vout}`); + const bF = frozen.includes(`${b.txid}:${b.vout}`); + return aF !== bF ? (aF ? -1 : 1) : a.txid.localeCompare(b.txid) || a.vout - b.vout; + } + default: + return 0; + } + }); + // invert if descending + return sortDirection === ESortDirections.desc ? res.reverse() : res; + }, [sortDirection, sortType, wallet, frozen]); const [output, setOutput] = useState(); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState([]); - const [frozen, setFrozen] = useState( - utxos.filter(out => wallet.getUTXOMetadata(out.txid, out.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`), - ); // save frozen status. Because effect called on each event, debounce it. const debouncedSaveFronen = useRef( @@ -415,6 +457,45 @@ const CoinControl: React.FC = () => { } }, [output]); + const toolTipActions = useMemo((): Action[] => { + return [ + sortDirection === ESortDirections.asc ? CommonToolTipActions.SortASC : CommonToolTipActions.SortDESC, + { ...CommonToolTipActions.SortHeight, menuState: sortType === ESortTypes.height }, + { ...CommonToolTipActions.SortValue, menuState: sortType === ESortTypes.value }, + { ...CommonToolTipActions.SortLabel, menuState: sortType === ESortTypes.label }, + { ...CommonToolTipActions.SortStatus, menuState: sortType === ESortTypes.frozen }, + ]; + }, [sortDirection, sortType]); + + const toolTipOnPressMenuItem = useCallback((menuItem: string) => { + Keyboard.dismiss(); + if (menuItem === CommonToolTipActions.SortASC.id) { + setSortDirection(ESortDirections.desc); + } else if (menuItem === CommonToolTipActions.SortDESC.id) { + setSortDirection(ESortDirections.asc); + } else if (menuItem === CommonToolTipActions.SortHeight.id) { + setSortType(ESortTypes.height); + } else if (menuItem === CommonToolTipActions.SortValue.id) { + setSortType(ESortTypes.value); + } else if (menuItem === CommonToolTipActions.SortLabel.id) { + setSortType(ESortTypes.label); + } else if (menuItem === CommonToolTipActions.SortStatus.id) { + setSortType(ESortTypes.frozen); + } + }, []); + + const HeaderRight = useMemo( + () => , + [toolTipOnPressMenuItem, toolTipActions], + ); + + // Adding the ToolTipMenu to the header + useEffect(() => { + navigation.setOptions({ + headerRight: () => HeaderRight, + }); + }, [HeaderRight, navigation]); + if (loading) { return ( @@ -438,6 +519,7 @@ const CoinControl: React.FC = () => { setOutput(undefined); }} backgroundColor={colors.elevated} + contentContainerStyle={styles.modalMinHeight} footer={