TxConfirmationDialog: implement multiple recipients support, layout fixes

This commit is contained in:
xiphon 2021-02-02 16:00:46 +00:00
parent c073867657
commit 92a2ae1a11
6 changed files with 133 additions and 103 deletions

View File

@ -51,7 +51,7 @@ Rectangle {
property alias flickable: mainFlickable property alias flickable: mainFlickable
property Transfer transferView: Transfer { property Transfer transferView: Transfer {
onPaymentClicked: root.paymentClicked(address, paymentId, amount, mixinCount, priority, description) onPaymentClicked: root.paymentClicked(recipients, paymentId, mixinCount, priority, description)
onSweepUnmixableClicked: root.sweepUnmixableClicked() onSweepUnmixableClicked: root.sweepUnmixableClicked()
} }
property Receive receiveView: Receive { } property Receive receiveView: Receive { }
@ -66,7 +66,7 @@ Rectangle {
property Keys keysView: Keys { } property Keys keysView: Keys { }
property Account accountView: Account { } property Account accountView: Account { }
signal paymentClicked(string address, string paymentId, string amount, int mixinCount, int priority, string description) signal paymentClicked(var recipients, string paymentId, int mixinCount, int priority, string description)
signal sweepUnmixableClicked() signal sweepUnmixableClicked()
signal generatePaymentIdInvoked() signal generatePaymentIdInvoked()
signal getProofClicked(string txid, string address, string message); signal getProofClicked(string txid, string address, string message);

View File

@ -27,7 +27,8 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4 as QtQuickControls1
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import "../components" as MoneroComponents import "../components" as MoneroComponents
@ -35,11 +36,14 @@ import FontAwesome 1.0
Rectangle { Rectangle {
id: root id: root
property int margins: 25
x: parent.width/2 - root.width/2 x: parent.width/2 - root.width/2
y: parent.height/2 - root.height/2 y: parent.height/2 - root.height/2
// TODO: implement without hardcoding sizes // TODO: implement without hardcoding sizes
width: 580 width: 590
height: 400 height: layout.height + layout.anchors.margins * 2
color: MoneroComponents.Style.blackTheme ? "black" : "white" color: MoneroComponents.Style.blackTheme ? "black" : "white"
visible: false visible: false
radius: 10 radius: 10
@ -53,8 +57,8 @@ Rectangle {
} }
KeyNavigation.tab: confirmButton KeyNavigation.tab: confirmButton
property var recipients: []
property var transactionAmount: "" property var transactionAmount: ""
property var transactionAddress: ""
property var transactionDescription: "" property var transactionDescription: ""
property var transactionFee: "" property var transactionFee: ""
property var transactionPriority: "" property var transactionPriority: ""
@ -127,8 +131,8 @@ Rectangle {
} }
function clearFields() { function clearFields() {
root.recipients = [];
root.transactionAmount = ""; root.transactionAmount = "";
root.transactionAddress = "";
root.transactionDescription = ""; root.transactionDescription = "";
root.transactionFee = ""; root.transactionFee = "";
root.transactionPriority = ""; root.transactionPriority = "";
@ -141,9 +145,12 @@ Rectangle {
} }
ColumnLayout { ColumnLayout {
id: layout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: parent.margins
spacing: 10 spacing: 10
anchors.fill: parent
anchors.margins: 25
RowLayout { RowLayout {
Layout.topMargin: 10 Layout.topMargin: 10
@ -181,10 +188,10 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 71 Layout.preferredHeight: 71
BusyIndicator { QtQuickControls1.BusyIndicator {
id: txAmountBusyIndicator id: txAmountBusyIndicator
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment : Qt.AlignTop | Qt.AlignLeft
running: root.transactionAmount == "(all)" running: root.transactionAmount == "(all)"
} }
@ -220,17 +227,11 @@ Rectangle {
columnSpacing: 15 columnSpacing: 15
rowSpacing: 16 rowSpacing: 16
ColumnLayout {
Layout.fillWidth: true
Layout.alignment : Qt.AlignTop | Qt.AlignLeft
Text { Text {
Layout.fillWidth: true
color: MoneroComponents.Style.dimmedFontColor color: MoneroComponents.Style.dimmedFontColor
text: qsTr("From") + ":" + translationManager.emptyString text: qsTr("From") + ":" + translationManager.emptyString
font.pixelSize: 15 font.pixelSize: 15
} }
}
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -266,58 +267,71 @@ Rectangle {
} }
} }
ColumnLayout {
Layout.fillWidth: true
Layout.alignment : Qt.AlignTop | Qt.AlignLeft
Text { Text {
Layout.fillWidth: true
font.pixelSize: 15 font.pixelSize: 15
color: MoneroComponents.Style.dimmedFontColor color: MoneroComponents.Style.dimmedFontColor
text: qsTr("To") + ":" + translationManager.emptyString text: qsTr("To") + ":" + translationManager.emptyString
} }
}
ColumnLayout { Flickable {
id: flickable
property int linesInMultipleRecipientsMode: 7
Layout.fillWidth: true Layout.fillWidth: true
spacing: 16 Layout.preferredHeight: recipients.length > 1
? linesInMultipleRecipientsMode * (recipientsArea.contentHeight / recipientsArea.lineCount)
: recipientsArea.contentHeight
boundsBehavior: isMac ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds
clip: true
Text { TextArea.flickable: TextArea {
Layout.fillWidth: true id : recipientsArea
font.pixelSize: 15
font.family: MoneroComponents.Style.fontRegular.name
textFormat: Text.RichText
wrapMode: Text.Wrap
color: MoneroComponents.Style.defaultFontColor color: MoneroComponents.Style.defaultFontColor
font.family: MoneroComponents.Style.fontMonoRegular.name
font.pixelSize: 14
topPadding: 0
bottomPadding: 0
leftPadding: 0
textMargin: 0
readOnly: true
selectByKeyboard: true
selectByMouse: true
selectionColor: MoneroComponents.Style.textSelectionColor
textFormat: TextEdit.RichText
wrapMode: TextEdit.Wrap
text: { text: {
if (root.transactionAddress) { return recipients.map(function (recipient, index) {
const addressBookName = currentWallet ? currentWallet.addressBook.getDescription(root.transactionAddress) : null; var addressBookName = null;
var fulladdress = root.transactionAddress; if (currentWallet) {
var spacedaddress = fulladdress.match(/.{1,4}/g); addressBookName = currentWallet.addressBook.getDescription(recipient.address);
var spacedaddress = spacedaddress.join(' '); }
if (!addressBookName) { var title;
return qsTr("Monero address") + "<br>" + spacedaddress + translationManager.emptyString; if (addressBookName) {
title = FontAwesome.addressBook + " " + addressBookName;
} else { } else {
return FontAwesome.addressBook + " " + addressBookName + "<br>" + spacedaddress; title = qsTr("Monero address") + translationManager.emptyString;
} }
} else { if (recipients.length > 1) {
return ""; title = "%1. %2 - %3 XMR".arg(index + 1).arg(title).arg(recipient.amount);
if (persistentSettings.fiatPriceEnabled) {
title += " (%1)".arg(showFiatConversion(recipient.amount));
} }
} }
const spacedaddress = recipient.address.match(/.{1,4}/g).join(' ');
return title + "<br>" + spacedaddress;
}).join("<br><br>");
} }
} }
ColumnLayout { ScrollBar.vertical: ScrollBar {
Layout.fillWidth: true policy: recipientsArea.contentHeight > flickable.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
Layout.alignment : Qt.AlignTop | Qt.AlignLeft }
}
Text { Text {
Layout.fillWidth: true
color: MoneroComponents.Style.dimmedFontColor color: MoneroComponents.Style.dimmedFontColor
text: qsTr("Fee") + ":" + translationManager.emptyString text: qsTr("Fee") + ":" + translationManager.emptyString
font.pixelSize: 15 font.pixelSize: 15
} }
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -364,7 +378,7 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 50 Layout.preferredHeight: 50
BusyIndicator { QtQuickControls1.BusyIndicator {
visible: !bottomTextAnimation.running visible: !bottomTextAnimation.running
running: !bottomTextAnimation.running running: !bottomTextAnimation.running
scale: .5 scale: .5

View File

@ -852,48 +852,49 @@ ApplicationWindow {
} }
} }
function getDisplayAmountTotal(recipients) {
const amounts = recipients.map(function (recipient) {
return recipient.amount;
});
const total = walletManager.amountsSumFromStrings(amounts);
return Utils.removeTrailingZeros(walletManager.displayAmount(total));
}
// called on "transfer" // called on "transfer"
function handlePayment(address, paymentId, amount, mixinCount, priority, description, createFile) { function handlePayment(recipients, paymentId, mixinCount, priority, description, createFile) {
console.log("Creating transaction: ") console.log("Creating transaction: ")
console.log("\taddress: ", address, console.log("\trecipients: ", recipients,
", payment_id: ", paymentId, ", payment_id: ", paymentId,
", amount: ", amount,
", mixins: ", mixinCount, ", mixins: ", mixinCount,
", priority: ", priority, ", priority: ", priority,
", description: ", description); ", description: ", description);
txConfirmationPopup.bottomTextAnimation.running = false
const recipientAll = recipients.find(function (recipient) {
return recipient.amount == "(all)";
});
if (recipientAll && recipients.length > 1) {
throw "Sending all requires one destination address";
}
txConfirmationPopup.bottomTextAnimation.running = false;
txConfirmationPopup.bottomText.text = qsTr("Creating transaction...") + translationManager.emptyString; txConfirmationPopup.bottomText.text = qsTr("Creating transaction...") + translationManager.emptyString;
txConfirmationPopup.transactionAddress = address; txConfirmationPopup.recipients = recipients;
txConfirmationPopup.transactionAmount = Utils.removeTrailingZeros(amount); txConfirmationPopup.transactionAmount = recipientAll ? "(all)" : getDisplayAmountTotal(recipients);
txConfirmationPopup.transactionPriority = priority; txConfirmationPopup.transactionPriority = priority;
txConfirmationPopup.transactionDescription = description; txConfirmationPopup.transactionDescription = description;
// validate amount;
if (amount !== "(all)") {
var amountxmr = walletManager.amountFromString(amount);
console.log("integer amount: ", amountxmr);
console.log("integer unlocked", currentWallet.unlockedBalance())
if (amountxmr <= 0) {
txConfirmationPopup.errorText.text = qsTr("Amount is wrong: expected number from %1 to %2")
.arg(walletManager.displayAmount(0))
.arg(walletManager.displayAmount(currentWallet.unlockedBalance()))
+ translationManager.emptyString;
return;
} else if (amountxmr > currentWallet.unlockedBalance()) {
txConfirmationPopup.errorText.text = qsTr("Insufficient funds. Unlocked balance: %1")
.arg(walletManager.displayAmount(currentWallet.unlockedBalance()))
+ translationManager.emptyString;
return;
}
}
txConfirmationPopup.open(); txConfirmationPopup.open();
if (amount === "(all)") if (recipientAll) {
currentWallet.createTransactionAllAsync(address, paymentId, mixinCount, priority); currentWallet.createTransactionAllAsync(recipientAll.address, paymentId, mixinCount, priority);
else } else {
currentWallet.createTransactionAsync([address], paymentId, [amountxmr], mixinCount, priority); const addresses = recipients.map(function (recipient) {
return recipient.address;
});
const amountsxmr = recipients.map(function (recipient) {
return walletManager.amountFromString(recipient.amount);
});
currentWallet.createTransactionAsync(addresses, paymentId, amountsxmr, mixinCount, priority);
}
} }
//Choose where to save transaction //Choose where to save transaction

View File

@ -44,8 +44,7 @@ import "../js/Utils.js" as Utils
Rectangle { Rectangle {
id: root id: root
signal paymentClicked(string address, string paymentId, string amount, int mixinCount, signal paymentClicked(var recipients, string paymentId, int mixinCount, int priority, string description)
int priority, string description)
signal sweepUnmixableClicked() signal sweepUnmixableClicked()
color: "transparent" color: "transparent"
@ -124,6 +123,10 @@ Rectangle {
priorityDropdown.currentIndex = 0 priorityDropdown.currentIndex = 0
} }
function getRecipients() {
return [{address: addressLine.text, amount: amountLine.text}];
}
// Information dialog // Information dialog
StandardDialog { StandardDialog {
// dynamically change onclose handler // dynamically change onclose handler
@ -523,7 +526,7 @@ Rectangle {
console.log("amount: " + amountLine.text) console.log("amount: " + amountLine.text)
addressLine.text = addressLine.text.trim() addressLine.text = addressLine.text.trim()
setPaymentId(paymentIdLine.text.trim()); setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
} }
} }
} }
@ -597,7 +600,7 @@ Rectangle {
console.log("amount: " + amountLine.text) console.log("amount: " + amountLine.text)
addressLine.text = addressLine.text.trim() addressLine.text = addressLine.text.trim()
setPaymentId(paymentIdLine.text.trim()); setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
} }
button2.text: qsTr("Sign (offline)") + translationManager.emptyString button2.text: qsTr("Sign (offline)") + translationManager.emptyString
button2.enabled: !appWindow.viewOnly button2.enabled: !appWindow.viewOnly

View File

@ -251,7 +251,7 @@ QString WalletManager::errorString() const
return tr("Unknown error"); return tr("Unknown error");
} }
quint64 WalletManager::maximumAllowedAmount() const quint64 WalletManager::maximumAllowedAmount()
{ {
return Monero::Wallet::maximumAllowedAmount(); return Monero::Wallet::maximumAllowedAmount();
} }
@ -266,7 +266,7 @@ QString WalletManager::displayAmount(quint64 amount)
return QString::fromStdString(Monero::Wallet::displayAmount(amount)); return QString::fromStdString(Monero::Wallet::displayAmount(amount));
} }
quint64 WalletManager::amountFromString(const QString &amount) const quint64 WalletManager::amountFromString(const QString &amount)
{ {
return Monero::Wallet::amountFromString(amount.toStdString()); return Monero::Wallet::amountFromString(amount.toStdString());
} }
@ -276,6 +276,17 @@ quint64 WalletManager::amountFromDouble(double amount) const
return Monero::Wallet::amountFromDouble(amount); return Monero::Wallet::amountFromDouble(amount);
} }
QString WalletManager::amountsSumFromStrings(const QVector<QString> &amounts)
{
quint64 sum = 0;
for (const auto &amountString : amounts)
{
const quint64 amount = amountFromString(amountString);
sum = sum + std::min(maximumAllowedAmount() - sum, amount);
}
return QString::number(sum);
}
bool WalletManager::paymentIdValid(const QString &payment_id) const bool WalletManager::paymentIdValid(const QString &payment_id) const
{ {
return Monero::Wallet::paymentIdValid(payment_id.toStdString()); return Monero::Wallet::paymentIdValid(payment_id.toStdString());

View File

@ -133,9 +133,10 @@ public:
//! since we can't call static method from QML, move it to this class //! since we can't call static method from QML, move it to this class
Q_INVOKABLE static QString displayAmount(quint64 amount); Q_INVOKABLE static QString displayAmount(quint64 amount);
Q_INVOKABLE quint64 amountFromString(const QString &amount) const; Q_INVOKABLE static quint64 amountFromString(const QString &amount);
Q_INVOKABLE quint64 amountFromDouble(double amount) const; Q_INVOKABLE quint64 amountFromDouble(double amount) const;
Q_INVOKABLE quint64 maximumAllowedAmount() const; Q_INVOKABLE static QString amountsSumFromStrings(const QVector<QString> &amounts);
Q_INVOKABLE static quint64 maximumAllowedAmount();
// QML JS engine doesn't support unsigned integers // QML JS engine doesn't support unsigned integers
Q_INVOKABLE QString maximumAllowedAmountAsString() const; Q_INVOKABLE QString maximumAllowedAmountAsString() const;