mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
feat: CoinControl sorting
This commit is contained in:
parent
96a59666b7
commit
35e201e132
@ -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],
|
||||
);
|
||||
|
||||
|
10
loc/en.json
10
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<SendDetailsStackParamList, 'CoinControl'>;
|
||||
type RouteProps = RouteProp<SendDetailsStackParamList, 'CoinControl'>;
|
||||
@ -256,6 +259,18 @@ const OutputModalContent: React.FC<TOutputModalContentProps> = ({ 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<NavigationProps>();
|
||||
@ -263,15 +278,42 @@ const CoinControl: React.FC = () => {
|
||||
const bottomModalRef = useRef<BottomModalHandle | null>(null);
|
||||
const { walletID } = useRoute<RouteProps>().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>(ESortDirections.asc);
|
||||
const [sortType, setSortType] = useState<ESortTypes>(ESortTypes.height);
|
||||
const wallet = useMemo(() => wallets.find(w => w.getID() === walletID) as TWallet, [walletID, wallets]);
|
||||
const [frozen, setFrozen] = useState<string[]>(
|
||||
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<Utxo | undefined>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [frozen, setFrozen] = useState<string[]>(
|
||||
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(
|
||||
() => <HeaderMenuButton onPressMenuItem={toolTipOnPressMenuItem} actions={toolTipActions} />,
|
||||
[toolTipOnPressMenuItem, toolTipActions],
|
||||
);
|
||||
|
||||
// Adding the ToolTipMenu to the header
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => HeaderRight,
|
||||
});
|
||||
}, [HeaderRight, navigation]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeArea style={[styles.center, { backgroundColor: colors.elevated }]}>
|
||||
@ -438,6 +519,7 @@ const CoinControl: React.FC = () => {
|
||||
setOutput(undefined);
|
||||
}}
|
||||
backgroundColor={colors.elevated}
|
||||
contentContainerStyle={styles.modalMinHeight}
|
||||
footer={
|
||||
<View style={mStyles.buttonContainer}>
|
||||
<Button
|
||||
@ -451,7 +533,6 @@ const CoinControl: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
contentContainerStyle={styles.modalMinHeight}
|
||||
>
|
||||
{renderOutputModalContent(output)}
|
||||
</BottomModal>
|
||||
@ -460,7 +541,7 @@ const CoinControl: React.FC = () => {
|
||||
data={utxos}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => `${item.txid}:${item.vout}`}
|
||||
contentInset={{ top: 0, left: 0, bottom: 70, right: 0 }}
|
||||
contentInset={styles.listContent}
|
||||
/>
|
||||
|
||||
{selectionStarted && (
|
||||
@ -512,6 +593,12 @@ const styles = StyleSheet.create({
|
||||
badges: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
listContent: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 70,
|
||||
right: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default CoinControl;
|
||||
|
@ -509,9 +509,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
|
||||
const hideShareModal = () => {};
|
||||
|
||||
const toolTipActions = useMemo(() => {
|
||||
const passphrase = CommonToolTipActions.Passphrase;
|
||||
passphrase.menuState = askPassphrase;
|
||||
return [passphrase];
|
||||
return [{ ...CommonToolTipActions.Passphrase, menuState: askPassphrase }];
|
||||
}, [askPassphrase]);
|
||||
|
||||
const renderProvideMnemonicsModal = () => {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Platform } from 'react-native';
|
||||
import loc from '../loc';
|
||||
import { Action } from '../components/types';
|
||||
|
||||
const keys = {
|
||||
CopyTXID: 'copyTX_ID',
|
||||
@ -45,9 +44,15 @@ const keys = {
|
||||
ExportPrivateKey: 'exportPrivateKey',
|
||||
PasteFromClipboard: 'pasteFromClipboard',
|
||||
Hide: 'hide',
|
||||
};
|
||||
SortASC: 'sortASC',
|
||||
SortDESC: 'sortDESC',
|
||||
SortHeight: 'sortHeight',
|
||||
SortValue: 'sortValue',
|
||||
SortLabel: 'sortLabel',
|
||||
SortStatus: 'sortStatus',
|
||||
} as const;
|
||||
|
||||
const icons: { [key: string]: { iconValue: string } } = {
|
||||
const icons = {
|
||||
Eye: { iconValue: 'eye' },
|
||||
EyeSlash: { iconValue: 'eye.slash' },
|
||||
Link: { iconValue: 'link' },
|
||||
@ -87,9 +92,11 @@ const icons: { [key: string]: { iconValue: string } } = {
|
||||
ImportFile: { iconValue: 'document.viewfinder' },
|
||||
Hide: { iconValue: 'eye.slash' },
|
||||
ClearClipboard: { iconValue: 'clipboard' },
|
||||
};
|
||||
SortASC: { iconValue: 'arrow.down.to.line' },
|
||||
SortDESC: { iconValue: 'arrow.up.to.line' },
|
||||
} as const;
|
||||
|
||||
export const CommonToolTipActions: { [key: string]: Action } = {
|
||||
export const CommonToolTipActions = {
|
||||
CopyTXID: {
|
||||
id: keys.CopyTXID,
|
||||
text: loc.transactions.details_copy_txid,
|
||||
@ -318,4 +325,30 @@ export const CommonToolTipActions: { [key: string]: Action } = {
|
||||
icon: icons.Clipboard,
|
||||
menuState: true,
|
||||
},
|
||||
};
|
||||
SortASC: {
|
||||
id: keys.SortASC,
|
||||
text: loc.cc.sort_asc,
|
||||
icon: icons.SortASC,
|
||||
},
|
||||
SortDESC: {
|
||||
id: keys.SortDESC,
|
||||
text: loc.cc.sort_desc,
|
||||
icon: icons.SortDESC,
|
||||
},
|
||||
SortHeight: {
|
||||
id: keys.SortHeight,
|
||||
text: loc.cc.sort_height,
|
||||
},
|
||||
SortValue: {
|
||||
id: keys.SortValue,
|
||||
text: loc.cc.sort_value,
|
||||
},
|
||||
SortLabel: {
|
||||
id: keys.SortLabel,
|
||||
text: loc.cc.sort_label,
|
||||
},
|
||||
SortStatus: {
|
||||
id: keys.SortStatus,
|
||||
text: loc.cc.sort_status,
|
||||
},
|
||||
} as const;
|
||||
|
Loading…
Reference in New Issue
Block a user