diff --git a/loc/en.json b/loc/en.json
index b6df5a7a0..e019ad0d0 100644
--- a/loc/en.json
+++ b/loc/en.json
@@ -491,8 +491,10 @@
"empty": "This wallet doesn't have any coins at the moment",
"freeze": "Freeze",
"freezeLabel": "Freeze",
+ "freezeLabel_un": "Unfreeze",
"header": "Coin Control",
"use_coin": "Use Coin",
+ "use_coins": "Use Coins",
"tip": "Allows you to see, label, freeze or select coins for improved wallet management."
}
}
diff --git a/screen/send/coinControl.js b/screen/send/coinControl.js
index 36bdd8bb3..37f85ad0c 100644
--- a/screen/send/coinControl.js
+++ b/screen/send/coinControl.js
@@ -1,17 +1,20 @@
import React, { useMemo, useState, useContext, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
-import { ListItem, Avatar, Badge } from 'react-native-elements';
+import { Avatar, Badge, Icon, ListItem } from 'react-native-elements';
import {
ActivityIndicator,
FlatList,
Keyboard,
KeyboardAvoidingView,
+ LayoutAnimation,
+ PixelRatio,
Platform,
StyleSheet,
Text,
TextInput,
TouchableWithoutFeedback,
useColorScheme,
+ useWindowDimensions,
View,
} from 'react-native';
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
@@ -20,6 +23,7 @@ import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { BlueNavigationStyle, SafeBlueArea, BlueSpacing10, BlueSpacing20, BlueButton, BlueListItem } from '../../BlueComponents';
import BottomModal from '../../components/BottomModal';
+import { FContainer, FButton } from '../../components/FloatButtons';
import { BlueStorageContext } from '../../blue_modules/storage-context';
// https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
@@ -35,87 +39,157 @@ const debounce = (func, wait) => {
};
};
-const Output = ({
- item: { address, txid, value, vout },
- balanceUnit = BitcoinUnit.BTC,
- oMemo,
- frozen,
- change = false,
- full = false,
- onPress,
-}) => {
+const FrozenBadge = () => {
const { colors } = useTheme();
- const { txMetadata } = useContext(BlueStorageContext);
const cs = useColorScheme();
- const memo = oMemo || txMetadata[txid]?.memo || '';
- const fullId = `${txid}:${vout}`;
- const shortId = `${address.substring(0, 9)}...${address.substr(address.length - 9)}`;
- const color = `#${txid.substring(0, 6)}`;
- const amount = formatBalance(value, balanceUnit, true);
-
const oStyles = StyleSheet.create({
- containerFull: { paddingHorizontal: 0 },
- avatar: { borderColor: 'white', borderWidth: 1 },
- amount: { fontWeight: 'bold' },
- memo: { fontSize: 13, marginTop: 3 },
- changeLight: { backgroundColor: colors.buttonDisabledBackgroundColor },
- changeDark: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 },
- changeText: { color: colors.alternativeTextColor },
freezeLight: { backgroundColor: colors.redBG },
freezeDark: { backgroundColor: colors.redBG, borderWidth: 0 },
freezeText: { color: colors.redText },
});
+ return ;
+};
+
+const ChangeBadge = () => {
+ const { colors } = useTheme();
+ const cs = useColorScheme();
+ const oStyles = StyleSheet.create({
+ changeLight: { backgroundColor: colors.buttonDisabledBackgroundColor },
+ changeDark: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 },
+ changeText: { color: colors.alternativeTextColor },
+ });
+
+ return ;
+};
+
+const OutputList = ({
+ item: { address, txid, value, vout, confirmations },
+ balanceUnit = BitcoinUnit.BTC,
+ oMemo,
+ frozen,
+ change,
+ onOpen,
+ selected,
+ selectionStarted,
+ onSelect,
+ onDeSelect,
+}) => {
+ const { colors } = useTheme();
+ const { txMetadata } = useContext(BlueStorageContext);
+ const memo = oMemo || txMetadata[txid]?.memo || '';
+ const shortId = `${address.substring(0, 9)}...${address.substr(address.length - 9)}`;
+ const color = `#${txid.substring(0, 6)}`;
+ const amount = formatBalance(value, balanceUnit, true);
+
+ const oStyles = StyleSheet.create({
+ container: { borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated },
+ containerSelected: {
+ borderBottomColor: 'rgba(0, 0, 0, 0)',
+ backgroundColor: colors.ballOutgoingExpired,
+ borderTopLeftRadius: 10,
+ borderBottomLeftRadius: 10,
+ },
+ avatar: { borderColor: 'white', borderWidth: 1, backgroundColor: color },
+ amount: { fontWeight: 'bold', color: colors.foregroundColor },
+ memo: { fontSize: 13, marginTop: 3, color: colors.alternativeTextColor },
+ });
+
+ let onPress = onOpen;
+ if (selectionStarted) {
+ onPress = selected ? onDeSelect : onSelect;
+ }
+
return (
-
-
+
+
- {amount}
- {full ? (
- <>
- {memo ? (
- <>
- {memo}
-
- >
- ) : null}
- {address}
-
- {fullId}
- >
- ) : (
-
- {memo || shortId}
-
- )}
+ {amount}
+
+ {memo || shortId}
+
- {change && (
-
- )}
- {frozen && (
-
- )}
+ {change && }
+ {frozen && }
);
};
-Output.propTypes = {
+OutputList.propTypes = {
item: PropTypes.shape({
address: PropTypes.string.isRequired,
txid: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
vout: PropTypes.number.isRequired,
+ confirmations: PropTypes.number.isRequired,
}),
balanceUnit: PropTypes.string,
oMemo: PropTypes.string,
frozen: PropTypes.bool,
change: PropTypes.bool,
- full: PropTypes.bool,
- onPress: PropTypes.func,
+ onOpen: PropTypes.func,
+ selected: PropTypes.bool,
+ selectionStarted: PropTypes.bool,
+ onSelect: PropTypes.func,
+ onDeSelect: PropTypes.func,
+};
+
+const OutputModal = ({ item: { address, txid, value, vout, confirmations }, balanceUnit = BitcoinUnit.BTC, oMemo }) => {
+ const { colors } = useTheme();
+ const { txMetadata } = useContext(BlueStorageContext);
+ const memo = oMemo || txMetadata[txid]?.memo || '';
+ const fullId = `${txid}:${vout}`;
+ const color = `#${txid.substring(0, 6)}`;
+ const amount = formatBalance(value, balanceUnit, true);
+
+ const oStyles = StyleSheet.create({
+ container: { paddingHorizontal: 0, borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated },
+ avatar: { borderColor: 'white', borderWidth: 1, backgroundColor: color },
+ amount: { fontWeight: 'bold', color: colors.foregroundColor },
+ tranContainer: { paddingLeft: 20 },
+ tranText: { fontWeight: 'normal', fontSize: 13, color: colors.alternativeTextColor },
+ memo: { fontSize: 13, marginTop: 3, color: colors.alternativeTextColor },
+ });
+
+ return (
+
+
+
+
+ {amount}
+
+ {loc.formatString(loc.transactions.list_conf, { number: confirmations })}
+
+
+ {memo ? (
+ <>
+ {memo}
+
+ >
+ ) : null}
+ {address}
+
+ {fullId}
+
+
+ );
+};
+
+OutputModal.propTypes = {
+ item: PropTypes.shape({
+ address: PropTypes.string.isRequired,
+ txid: PropTypes.string.isRequired,
+ value: PropTypes.number.isRequired,
+ vout: PropTypes.number.isRequired,
+ confirmations: PropTypes.number.isRequired,
+ }),
+ balanceUnit: PropTypes.string,
+ oMemo: PropTypes.string,
};
const mStyles = StyleSheet.create({
@@ -136,28 +210,27 @@ const mStyles = StyleSheet.create({
},
});
-const OutputModalContent = ({ output, wallet, onUseCoin }) => {
+const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) => {
const { colors } = useTheme();
const { txMetadata, saveToDisk } = useContext(BlueStorageContext);
- const [frozen, setFrozen] = useState(wallet.getUTXOMetadata(output.txid, output.vout).frozen || false);
const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || '');
const onMemoChange = value => setMemo(value);
const switchValue = useMemo(() => ({ value: frozen, onValueChange: value => setFrozen(value) }), [frozen, setFrozen]);
// save on form change. Because effect called on each event, debounce it.
- const debouncedSave = useRef(
- debounce(async (frozen, memo) => {
- wallet.setUTXOMetadata(output.txid, output.vout, { frozen, memo });
+ const debouncedSaveMemo = useRef(
+ debounce(async memo => {
+ wallet.setUTXOMetadata(output.txid, output.vout, { memo });
await saveToDisk();
}, 500),
);
useEffect(() => {
- debouncedSave.current(frozen, memo);
- }, [frozen, memo]);
+ debouncedSaveMemo.current(memo);
+ }, [memo]);
return (
<>
-
+
{
const { colors } = useTheme();
const navigation = useNavigation();
+ const { width } = useWindowDimensions();
const { walletId, onUTXOChoose } = useRoute().params;
- const { wallets } = useContext(BlueStorageContext);
+ const { wallets, saveToDisk } = useContext(BlueStorageContext);
const wallet = wallets.find(w => w.getID() === walletId);
// sort by height ascending, txid , vout ascending
const utxo = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout);
const [output, setOutput] = useState();
const [loading, setLoading] = useState(true);
+ const [selected, setSelected] = useState([]);
+ const [frozen, setFrozen] = useState(
+ utxo.filter(output => wallet.getUTXOMetadata(output.txid, output.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`),
+ );
+
+ // save frozen status. Because effect called on each event, debounce it.
+ const debouncedSaveFronen = useRef(
+ debounce(async frozen => {
+ utxo.forEach(({ txid, vout }) => {
+ wallet.setUTXOMetadata(txid, vout, { frozen: frozen.includes(`${txid}:${vout}`) });
+ });
+ await saveToDisk();
+ }, 500),
+ );
+ useEffect(() => {
+ debouncedSaveFronen.current(frozen);
+ }, [frozen]);
+
+ useEffect(() => {
+ wallet.fetchUtxo().then(() => {
+ const freshUtxo = wallet.getUtxo(true);
+ setFrozen(
+ freshUtxo.filter(output => wallet.getUTXOMetadata(output.txid, output.vout).frozen).map(({ txid, vout }) => `${txid}:${vout}`),
+ );
+ setLoading(false);
+ });
+ }, [wallet, setLoading]);
const stylesHook = StyleSheet.create({
tip: {
@@ -217,10 +320,6 @@ const CoinControl = () => {
);
};
- useEffect(() => {
- wallet.fetchUtxo().then(() => setLoading(false));
- }, [wallet, setLoading]);
-
const handleChoose = item => setOutput(item);
const handleUseCoin = utxo => {
@@ -229,21 +328,60 @@ const CoinControl = () => {
onUTXOChoose(utxo);
};
+ const handleMassFreeze = () => {
+ if (allFrozen) {
+ setFrozen(f => f.filter(i => !selected.includes(i))); // unfreeze
+ } else {
+ setFrozen(f => [...new Set([...f, ...selected])]); // freeze
+ }
+ };
+
+ const handleMassUse = () => {
+ const fUtxo = utxo.filter(({ txid, vout }) => selected.includes(`${txid}:${vout}`));
+ handleUseCoin(fUtxo);
+ };
+
+ // check if any outputs are selected
+ const selectionStarted = selected.length > 0;
+ // check if all selected items are frozen
+ const allFrozen = selectionStarted && selected.reduce((prev, curr) => (prev ? frozen.includes(curr) : false), true);
+ const buttonFontSize = PixelRatio.roundToNearestPixel(width / 26) > 22 ? 22 : PixelRatio.roundToNearestPixel(width / 26);
+
const renderItem = p => {
- const { memo, frozen } = wallet.getUTXOMetadata(p.item.txid, p.item.vout);
+ const { memo } = wallet.getUTXOMetadata(p.item.txid, p.item.vout);
const change = wallet.addressIsChange(p.item.address);
+ const oFrozen = frozen.includes(`${p.item.txid}:${p.item.vout}`);
return (
-