Pay payment requests
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<qresource prefix="/icons">
|
||||
<file>res/connected.gif</file>
|
||||
<file>res/loading.gif</file>
|
||||
<file>res/paymentreq.gif</file>
|
||||
<file>res/icon.ico</file>
|
||||
</qresource>
|
||||
<qresource prefix="/img">
|
||||
|
||||
BIN
res/paymentreq.gif
Normal file
BIN
res/paymentreq.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -804,12 +804,6 @@ void MainWindow::payZcashURI(QString uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Error to display if something goes wrong.
|
||||
auto payZcashURIError = [=] (QString errorDetail = "") {
|
||||
QMessageBox::critical(this, tr("Error paying zcash URI"),
|
||||
tr("URI should be of the form 'zcash:<addr>?amt=x&memo=y") + "\n" + errorDetail);
|
||||
};
|
||||
|
||||
// If there was no URI passed, ask the user for one.
|
||||
if (uri.isEmpty()) {
|
||||
uri = QInputDialog::getText(this, tr("Paste Zcash URI"),
|
||||
@@ -820,70 +814,21 @@ void MainWindow::payZcashURI(QString uri) {
|
||||
if (uri.isEmpty())
|
||||
return;
|
||||
|
||||
// URI should be of the form zcash://address?amt=x&memo=y
|
||||
if (!uri.startsWith("zcash:")) {
|
||||
payZcashURIError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the address
|
||||
qDebug() << "Recieved URI " << uri;
|
||||
uri = uri.right(uri.length() - QString("zcash:").length());
|
||||
|
||||
QRegExp re("([a-zA-Z0-9]+)");
|
||||
int pos;
|
||||
if ( (pos = re.indexIn(uri)) == -1 ) {
|
||||
payZcashURIError();
|
||||
PaymentURI paymentInfo = Settings::parseURI(uri);
|
||||
if (!paymentInfo.error.isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error paying zcash URI"),
|
||||
tr("URI should be of the form 'zcash:<addr>?amt=x&memo=y") + "\n" + paymentInfo.error);
|
||||
return;
|
||||
}
|
||||
|
||||
QString addr = re.cap(1);
|
||||
if (!Settings::isValidAddress(addr)) {
|
||||
payZcashURIError(tr("Could not understand address"));
|
||||
return;
|
||||
}
|
||||
uri = uri.right(uri.length() - addr.length());
|
||||
|
||||
double amount = 0.0;
|
||||
QString memo = "";
|
||||
|
||||
if (!uri.isEmpty()) {
|
||||
uri = uri.right(uri.length() - 1); // Eat the "?"
|
||||
|
||||
QStringList args = uri.split("&");
|
||||
for (QString arg: args) {
|
||||
QStringList kv = arg.split("=");
|
||||
if (kv.length() != 2) {
|
||||
payZcashURIError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (kv[0].toLower() == "amt" || kv[0].toLower() == "amount") {
|
||||
amount = kv[1].toDouble();
|
||||
} else if (kv[0].toLower() == "memo" || kv[0].toLower() == "message" || kv[0].toLower() == "msg") {
|
||||
memo = kv[1];
|
||||
// Test if this is hex
|
||||
|
||||
QRegularExpression hexMatcher("^[0-9A-F]+$",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch match = hexMatcher.match(memo);
|
||||
if (match.hasMatch()) {
|
||||
// Encoded as hex, convert to string
|
||||
memo = QByteArray::fromHex(memo.toUtf8());
|
||||
}
|
||||
} else {
|
||||
payZcashURIError(tr("Unknown field in URI:") + kv[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, set the fields on the send tab
|
||||
removeExtraAddresses();
|
||||
ui->Address1->setText(addr);
|
||||
ui->Address1->setText(paymentInfo.addr);
|
||||
ui->Address1->setCursorPosition(0);
|
||||
ui->Amount1->setText(QString::number(amount));
|
||||
ui->MemoTxt1->setText(memo);
|
||||
ui->Amount1->setText(Settings::getDecimalString(paymentInfo.amt.toDouble()));
|
||||
ui->MemoTxt1->setText(paymentInfo.memo);
|
||||
|
||||
// And switch to the send tab.
|
||||
ui->tabWidget->setCurrentIndex(1);
|
||||
@@ -891,7 +836,7 @@ void MainWindow::payZcashURI(QString uri) {
|
||||
|
||||
// And click the send button if the amount is > 0, to validate everything. If everything is OK, it will show the confirm box
|
||||
// else, show the error message;
|
||||
if (amount > 0) {
|
||||
if (paymentInfo.amt > 0) {
|
||||
sendButton();
|
||||
}
|
||||
}
|
||||
@@ -1214,8 +1159,16 @@ void MainWindow::setupTransactionsTab() {
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
});
|
||||
|
||||
// Payment Request
|
||||
if (!memo.isEmpty() && memo.startsWith("zcash:")) {
|
||||
menu.addAction(tr("View Payment Request"), [=] () {
|
||||
RequestDialog::showPaymentConfirmation(this, memo);
|
||||
});
|
||||
}
|
||||
|
||||
// View Memo
|
||||
if (!memo.isEmpty()) {
|
||||
menu.addAction(tr("View Memo"), [=] () {
|
||||
menu.addAction(tr("View Memo"), [=] () {
|
||||
QMessageBox mb(QMessageBox::Information, tr("Memo"), memo, QMessageBox::Ok, this);
|
||||
mb.setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
||||
mb.exec();
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include "ui_requestdialog.h"
|
||||
#include "settings.h"
|
||||
#include "addressbook.h"
|
||||
#include "mainwindow.h"
|
||||
#include "rpc.h"
|
||||
#include "settings.h"
|
||||
|
||||
|
||||
RequestDialog::RequestDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
@@ -15,6 +19,51 @@ RequestDialog::~RequestDialog()
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void RequestDialog::setupDialog(QDialog* d, Ui_RequestDialog* req) {
|
||||
req->setupUi(d);
|
||||
Settings::saveRestore(d);
|
||||
|
||||
// Setup
|
||||
req->txtMemo->setLenDisplayLabel(req->lblMemoLen);
|
||||
req->lblAmount->setText(req->lblAmount->text() + Settings::getTokenName());
|
||||
}
|
||||
|
||||
// Static method that shows an incoming payment request and prompts the user to pay it
|
||||
void RequestDialog::showPaymentConfirmation(MainWindow* main, QString paymentURI) {
|
||||
PaymentURI payInfo = Settings::parseURI(paymentURI);
|
||||
if (!payInfo.error.isEmpty()) {
|
||||
QMessageBox::critical(main, tr("Error paying zcash URI"),
|
||||
tr("URI should be of the form 'zcash:<addr>?amt=x&memo=y") + "\n" + payInfo.error);
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog d(main);
|
||||
Ui_RequestDialog req;
|
||||
setupDialog(&d, &req);
|
||||
|
||||
// In the view mode, all fields are read-only
|
||||
req.txtAmount->setReadOnly(true);
|
||||
req.txtFrom->setReadOnly(true);
|
||||
req.txtMemo->setReadOnly(true);
|
||||
|
||||
// Payment is "to"
|
||||
req.lblAddress->setText(tr("Pay To"));
|
||||
|
||||
// No Addressbook
|
||||
req.btnAddressBook->setVisible(false);
|
||||
|
||||
req.txtFrom->setText(payInfo.addr);
|
||||
req.txtMemo->setPlainText(payInfo.memo);
|
||||
req.txtAmount->setText(payInfo.amt);
|
||||
req.txtAmountUSD->setText(Settings::getUSDFormat(req.txtAmount->text().toDouble()));
|
||||
|
||||
req.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Pay"));
|
||||
|
||||
if (d.exec() == QDialog::Accepted) {
|
||||
main->payZcashURI(paymentURI);
|
||||
}
|
||||
}
|
||||
|
||||
// Static method that shows the request dialog
|
||||
void RequestDialog::showRequestZcash(MainWindow* main) {
|
||||
QDialog d(main);
|
||||
@@ -55,9 +104,15 @@ void RequestDialog::showRequestZcash(MainWindow* main) {
|
||||
|
||||
if (d.exec() == QDialog::Accepted) {
|
||||
// Construct a zcash Payment URI with the data and pay it immediately.
|
||||
QString paymentURI = "zcash:" + AddressBook::addressFromAddressLabel(req.txtFrom->text())
|
||||
QString memoURI = "zcash:" + main->getRPC()->getDefaultSaplingAddress()
|
||||
+ "?amt=" + Settings::getDecimalString(req.txtAmount->text().toDouble())
|
||||
+ "&memo=" + QUrl::toPercentEncoding(req.txtMemo->toPlainText());
|
||||
main->payZcashURI(paymentURI);
|
||||
|
||||
QString sendURI = "zcash:" + AddressBook::addressFromAddressLabel(req.txtFrom->text())
|
||||
+ "?amt=0.0001"
|
||||
+ "&memo=" + QUrl::toPercentEncoding(memoURI);
|
||||
|
||||
qDebug() << "Paying " << sendURI;
|
||||
main->payZcashURI(sendURI);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QDialog>
|
||||
#include "mainwindow.h"
|
||||
#include "ui_requestdialog.h"
|
||||
|
||||
namespace Ui {
|
||||
class RequestDialog;
|
||||
@@ -17,7 +18,8 @@ public:
|
||||
~RequestDialog();
|
||||
|
||||
static void showRequestZcash(MainWindow* main);
|
||||
|
||||
static void showPaymentConfirmation(MainWindow* main, QString paymentURI);
|
||||
static void setupDialog(QDialog* d, Ui_RequestDialog* req);
|
||||
private:
|
||||
Ui::RequestDialog *ui;
|
||||
};
|
||||
|
||||
@@ -6,41 +6,41 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>714</width>
|
||||
<height>524</height>
|
||||
<width>1052</width>
|
||||
<height>509</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
<string>Payment Request</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="8" column="1" colspan="3">
|
||||
<widget class="MemoEdit" name="txtMemo"/>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="3">
|
||||
<widget class="QLineEdit" name="txtAmount">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Amount</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" colspan="2">
|
||||
<widget class="QLabel" name="lblSaplingWarning">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: red;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="2" colspan="2">
|
||||
<item row="13" column="2" colspan="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@@ -67,8 +67,38 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<item row="1" column="1" colspan="3">
|
||||
<widget class="QLineEdit" name="txtFrom">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>z address</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="lblAddress">
|
||||
<property name="text">
|
||||
<string>Request From</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="3">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnAddressBook">
|
||||
<property name="text">
|
||||
<string>AddressBook</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
@@ -82,35 +112,25 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnAddressBook">
|
||||
<property name="text">
|
||||
<string>AddressBook</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="3">
|
||||
<widget class="QLineEdit" name="txtFrom">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<item row="0" column="1" colspan="3">
|
||||
<widget class="QLabel" name="lblSaplingWarning">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: red;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>z address</string>
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Request From</string>
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -120,24 +140,37 @@
|
||||
<string>Amount USD</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<widget class="QLabel" name="lblAmount">
|
||||
<property name="text">
|
||||
<string>Amount in ZEC</string>
|
||||
<string>Amount in </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="4">
|
||||
<item row="12" column="0" colspan="4">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="3">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
@@ -147,6 +180,11 @@
|
||||
<header>memoedit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>txtFrom</tabstop>
|
||||
<tabstop>txtAmount</tabstop>
|
||||
<tabstop>txtMemo</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
|
||||
@@ -161,10 +161,7 @@ void Settings::saveRestore(QDialog* d) {
|
||||
}
|
||||
|
||||
QString Settings::getUSDFormat(double bal) {
|
||||
if (!Settings::getInstance()->isTestnet() && Settings::getInstance()->getZECPrice() > 0)
|
||||
return "$" + QLocale(QLocale::English).toString(bal * Settings::getInstance()->getZECPrice(), 'f', 2);
|
||||
else
|
||||
return QString();
|
||||
return "$" + QLocale(QLocale::English).toString(bal * Settings::getInstance()->getZECPrice(), 'f', 2);
|
||||
}
|
||||
|
||||
QString Settings::getDecimalString(double amt) {
|
||||
@@ -289,4 +286,60 @@ bool Settings::isValidAddress(QString addr) {
|
||||
ztsexp.exactMatch(addr) || zsexp.exactMatch(addr);
|
||||
}
|
||||
|
||||
// Get a pretty string representation of this Payment URI
|
||||
QString Settings::paymentURIPretty(PaymentURI uri) {
|
||||
return QString() + "Payment Request\n" + "Pay: " + uri.addr + "\nAmount: " + getZECDisplayFormat(uri.amt.toDouble())
|
||||
+ "\nMemo:" + QUrl::fromPercentEncoding(uri.memo.toUtf8());
|
||||
}
|
||||
|
||||
// Parse a payment URI string into its components
|
||||
PaymentURI Settings::parseURI(QString uri) {
|
||||
PaymentURI ans;
|
||||
|
||||
if (!uri.startsWith("zcash:")) {
|
||||
ans.error = "Not a zcash payment URI";
|
||||
return ans;
|
||||
}
|
||||
|
||||
uri = uri.right(uri.length() - QString("zcash:").length());
|
||||
|
||||
QRegExp re("([a-zA-Z0-9]+)");
|
||||
int pos;
|
||||
if ( (pos = re.indexIn(uri)) == -1 ) {
|
||||
ans.error = "Couldn't find an address";
|
||||
return ans;
|
||||
}
|
||||
|
||||
ans.addr = re.cap(1);
|
||||
if (!Settings::isValidAddress(ans.addr)) {
|
||||
ans.error = "Could not understand address";
|
||||
return ans;
|
||||
}
|
||||
uri = uri.right(uri.length() - ans.addr.length());
|
||||
|
||||
if (!uri.isEmpty()) {
|
||||
uri = uri.right(uri.length() - 1); // Eat the "?"
|
||||
|
||||
QStringList args = uri.split("&");
|
||||
for (QString arg: args) {
|
||||
QStringList kv = arg.split("=");
|
||||
if (kv.length() != 2) {
|
||||
ans.error = "No value argument was seen";
|
||||
return ans;
|
||||
}
|
||||
|
||||
if (kv[0].toLower() == "amt" || kv[0].toLower() == "amount") {
|
||||
ans.amt = kv[1];
|
||||
} else if (kv[0].toLower() == "memo" || kv[0].toLower() == "message" || kv[0].toLower() == "msg") {
|
||||
ans.memo = QUrl::fromPercentEncoding(kv[1].toUtf8());
|
||||
} else {
|
||||
ans.error = "Unknown field in URI:" + kv[0];
|
||||
return ans;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
const QString Settings::labelRegExp("[a-zA-Z0-9\\-_]{0,40}");
|
||||
|
||||
@@ -13,6 +13,15 @@ struct Config {
|
||||
struct ToFields;
|
||||
struct Tx;
|
||||
|
||||
struct PaymentURI {
|
||||
QString addr;
|
||||
QString amt;
|
||||
QString memo;
|
||||
|
||||
// Any errors are stored here
|
||||
QString error;
|
||||
};
|
||||
|
||||
class Settings
|
||||
{
|
||||
public:
|
||||
@@ -68,6 +77,9 @@ public:
|
||||
|
||||
static void saveRestore(QDialog* d);
|
||||
|
||||
static PaymentURI parseURI(QString paymentURI);
|
||||
static QString paymentURIPretty(PaymentURI);
|
||||
|
||||
static bool isZAddress(QString addr);
|
||||
static bool isTAddress(QString addr);
|
||||
|
||||
|
||||
@@ -138,8 +138,14 @@ void TxTableModel::updateAllData() {
|
||||
|
||||
if (role == Qt::ToolTipRole) {
|
||||
switch (index.column()) {
|
||||
case 0: return modeldata->at(index.row()).type +
|
||||
(dat.memo.isEmpty() ? "" : " tx memo: \"" + dat.memo + "\"");
|
||||
case 0: {
|
||||
if (dat.memo.startsWith("zcash:")) {
|
||||
return Settings::paymentURIPretty(Settings::parseURI(dat.memo));
|
||||
} else {
|
||||
return modeldata->at(index.row()).type +
|
||||
(dat.memo.isEmpty() ? "" : " tx memo: \"" + dat.memo + "\"");
|
||||
}
|
||||
}
|
||||
case 1: {
|
||||
auto addr = modeldata->at(index.row()).address;
|
||||
if (addr.trimmed().isEmpty())
|
||||
@@ -154,9 +160,15 @@ void TxTableModel::updateAllData() {
|
||||
|
||||
if (role == Qt::DecorationRole && index.column() == 0) {
|
||||
if (!dat.memo.isEmpty()) {
|
||||
// Return the info pixmap to indicate memo
|
||||
QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation);
|
||||
return QVariant(icon.pixmap(16, 16));
|
||||
// If the memo is a Payment URI, then show a payment request icon
|
||||
if (dat.memo.startsWith("zcash:")) {
|
||||
QIcon icon(":/icons/res/paymentreq.gif");
|
||||
return QVariant(icon.pixmap(16, 16));
|
||||
} else {
|
||||
// Return the info pixmap to indicate memo
|
||||
QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation);
|
||||
return QVariant(icon.pixmap(16, 16));
|
||||
}
|
||||
} else {
|
||||
// Empty pixmap to make it align
|
||||
QPixmap p(16, 16);
|
||||
|
||||
Reference in New Issue
Block a user