From 0530212b21cbd7cdf0fdc2003fedcdf44f900541 Mon Sep 17 00:00:00 2001 From: Dimitris Apostolou Date: Sun, 4 Nov 2018 22:38:52 +0200 Subject: [PATCH 1/7] Fix typos and cleanup --- README.md | 10 +++++----- src/mainwindow.cpp | 20 ++++++++++---------- src/rpc.cpp | 8 ++++---- src/scripts/mkrelease.sh | 2 +- src/sendtab.cpp | 10 +++++----- src/senttxstore.cpp | 4 ++-- src/turnstile.cpp | 10 +++++----- src/utils.cpp | 6 +++--- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f331a1c..73702a3 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ tar -xvf zec-qt-wallet-v0.2.7.tar.gz Unzip the release binary and double click on zec-qt-wallet to start. ## Prerequisites: zcashd -zec-qt-wallet needs a Zcash node running zcashd. Linux users should download the zcash node software +zec-qt-wallet needs a Zcash node running zcashd. Linux users should download the Zcash node software from [https://z.cash/download/](https://z.cash/download/), configure `zcash.conf`, download the parameters and start zcashd according to the [official documentation](https://zcash.readthedocs.io/en/latest/rtd_pages/user_guide.html). There is currently no official zcashd build for Windows so Windows users may either [cross-compile from source on Linux](https://zcash.readthedocs.io/en/latest/rtd_pages/user_guide.html#installation) to generate the necessary zcashd executables or simply download community hosted pre-compiled executables such as those hosted by WinZEC developer [@radix42](https://github.com/radix42) at https://zcash.dl.mercerweiss.com/zcash-win-v2.0.1b.zip. -Alternitavely run zcashd inside [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). +Alternatively run zcashd inside [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). For all installations zcashd needs to run with RPC enabled (`server=1`, which is the default) and with a RPC username/password set. Add the following entries into `~/.zcash/zcash.conf` for Linux or` C:\Users\your-username\AppData\Roaming\Zcash\zcash.conf` on Windows replacing the default values with a strong password. zec-qt-wallet should detect these settings but if that fails you may edit the connection settings manually via the `File->Settings` menu. @@ -31,7 +31,7 @@ rpcuser=username rpcpassword=password ``` -Additionaly for Windows users the Zcash parameters must be manually downloaded and placed in `C:\Users\your-username\AppData\Roaming\ZcashParams`. The following files are required (and are around ~1.7GB in total). +Additionally for Windows users the Zcash parameters must be manually downloaded and placed in `C:\Users\your-username\AppData\Roaming\ZcashParams`. The following files are required (and are around ~1.7GB in total). ``` https://z.cash/downloads/sapling-spend.params @@ -87,10 +87,10 @@ The easiest way to connect to a remote node is probably to ssh to it with port f ssh -L8232:127.0.0.1:8232 user@remotehost ``` ### 2. "Not enough balance" when sending transactions -The most likely cause for this is that you are trying to spend unconfirmed funds. Unlike bitcoin, the zcash protocol doesn't let you spent unconfirmed funds yet. Please wait for +The most likely cause for this is that you are trying to spend unconfirmed funds. Unlike Bitcoin, the Zcash protocol doesn't let you spent unconfirmed funds yet. Please wait for 1-2 blocks for the funds to confirm and retry the transaction. ### Support or other questions Tweet at [@zecqtwallet](https://twitter.com/zecqtwallet) for help. -_PS: zec-qt-wallet is NOT an official wallet, and is not affiliated with the Zerocoin Electric Coin Company in any way._ \ No newline at end of file +_PS: zec-qt-wallet is NOT an official wallet, and is not affiliated with the Zerocoin Electric Coin Company in any way._ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dd7ad6f..aff366c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -344,7 +344,7 @@ void MainWindow::setupSettingsModal() { // Setup clear button QObject::connect(settings.btnClearSaved, &QCheckBox::clicked, [=]() { if (QMessageBox::warning(this, "Clear saved history?", - "Shielded z-Address transactions are stored locally in your wallet, outside zcashd. You may delete this saved information safely any time for your privacy.\nDo you want to delete the saved shielded transactions now ?", + "Shielded z-Address transactions are stored locally in your wallet, outside zcashd. You may delete this saved information safely any time for your privacy.\nDo you want to delete the saved shielded transactions now?", QMessageBox::Yes, QMessageBox::Cancel)) { SentTxStore::deleteHistory(); // Reload after the clear button so existing txs disappear @@ -640,10 +640,10 @@ void MainWindow::setupTransactionsTab() { void MainWindow::addNewZaddr(bool sapling) { rpc->newZaddr(sapling, [=] (json reply) { QString addr = QString::fromStdString(reply.get()); - // Make sure the RPC class reloads the Z-addrs for future use + // Make sure the RPC class reloads the z-addrs for future use rpc->refreshAddresses(); - // Just double make sure the Z-address is still checked + // Just double make sure the z-address is still checked if (( sapling && ui->rdioZSAddr->isChecked()) || (!sapling && ui->rdioZAddr->isChecked())) { ui->listRecieveAddresses->insertItem(0, addr); @@ -684,7 +684,7 @@ void MainWindow::setupRecieveTab() { rpc->newTaddr([=] (json reply) { QString addr = QString::fromStdString(reply.get()); - // Just double make sure the T-address is still checked + // Just double make sure the t-address is still checked if (ui->rdioTAddr->isChecked()) { ui->listRecieveAddresses->insertItem(0, addr); ui->listRecieveAddresses->setCurrentIndex(0); @@ -696,8 +696,8 @@ void MainWindow::setupRecieveTab() { // Connect t-addr radio button QObject::connect(ui->rdioTAddr, &QRadioButton::toggled, [=] (bool checked) { - // Whenever the T-address is selected, we generate a new address, because we don't - // want to reuse T-addrs + // Whenever the t-address is selected, we generate a new address, because we don't + // want to reuse t-addrs if (checked && this->rpc->getUTXOs() != nullptr) { auto utxos = this->rpc->getUTXOs(); ui->listRecieveAddresses->clear(); @@ -729,12 +729,12 @@ void MainWindow::setupRecieveTab() { } }); - // Focus enter for the Recieve Tab + // Focus enter for the Receive Tab QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int tab) { if (tab == 2) { - // Switched to recieve tab, so update everything. + // Switched to receive tab, so update everything. - // Hide Sapling radio button if sapling is not active + // Hide Sapling radio button if Sapling is not active if (Settings::getInstance()->isSaplingActive()) { ui->rdioZSAddr->setVisible(true); ui->rdioZSAddr->setChecked(true); @@ -742,7 +742,7 @@ void MainWindow::setupRecieveTab() { } else { ui->rdioZSAddr->setVisible(false); ui->rdioZAddr->setChecked(true); - ui->rdioZAddr->setText("z-Addr"); // Don't use the "Sprout" label if there's no sapling + ui->rdioZAddr->setText("z-Addr"); // Don't use the "Sprout" label if there's no Sapling } // And then select the first one diff --git a/src/rpc.cpp b/src/rpc.cpp index f3dc59c..e739c80 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -212,7 +212,7 @@ void RPC::getAllPrivKeys(const std::function> // A special function that will call the callback when two lists have been added auto holder = new QPair>>(); - holder->first = 0; // This is the number of times the callback has been called, initalized to 0 + holder->first = 0; // This is the number of times the callback has been called, initialized to 0 auto fnCombineTwoLists = [=] (QList> list) { // Increment the callback counter holder->first++; @@ -269,7 +269,7 @@ void RPC::getAllPrivKeys(const std::function> }; - // First get all the T and Z addresses. + // First get all the t and z addresses. json payloadT = { {"jsonrpc", "1.0"}, {"id", "someid"}, @@ -699,7 +699,7 @@ void RPC::watchTxStatus() { conn->doRPCWithDefaultErrorHandling(payload, [=] (const json& reply) { // There's an array for each item in the status for (auto& it : reply.get()) { - // If we were watching this Tx and it's status became "success", then we'll show a status bar alert + // If we were watching this Tx and its status became "success", then we'll show a status bar alert QString id = QString::fromStdString(it["id"]); if (watchingOps.contains(id)) { // And if it ended up successful @@ -755,7 +755,7 @@ void RPC::watchTxStatus() { // Get the ZEC->USD price from coinmarketcap using their API void RPC::refreshZECPrice() { - qDebug() << QString::fromStdString("Getting zec price"); + qDebug() << QString::fromStdString("Getting ZEC price"); if (conn == nullptr) return noConnection(); diff --git a/src/scripts/mkrelease.sh b/src/scripts/mkrelease.sh index c24929e..5b4d291 100755 --- a/src/scripts/mkrelease.sh +++ b/src/scripts/mkrelease.sh @@ -27,7 +27,7 @@ echo "Linux" echo -n "Configuring..." $QT_STATIC/bin/qmake zec-qt-wallet.pro -spec linux-clang CONFIG+=release > /dev/null -#Mingw seems to have trouble with precompiled heades, so strip that option from the .pro file +#Mingw seems to have trouble with precompiled headers, so strip that option from the .pro file echo "[OK]" diff --git a/src/sendtab.cpp b/src/sendtab.cpp index 1d68dcd..0471d12 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -217,7 +217,7 @@ void MainWindow::setMemoEnabled(int number, bool enabled) { memoBtn->setToolTip(""); } else { memoBtn->setEnabled(false); - memoBtn->setToolTip("Only Z addresses can have memos"); + memoBtn->setToolTip("Only z-addresses can have memos"); } } @@ -225,8 +225,8 @@ void MainWindow::memoButtonClicked(int number) { // Memos can only be used with zAddrs. So check that first auto addr = ui->sendToWidgets->findChild(QString("Address") + QString::number(number)); if (!addr->text().trimmed().startsWith("z")) { - QMessageBox msg(QMessageBox::Critical, "Memos can only be used with z Addresses", - "The Memo field can only be used with a z Address.\n" + addr->text() + "\ndoesn't look like a z Address", + QMessageBox msg(QMessageBox::Critical, "Memos can only be used with z-addresses", + "The memo field can only be used with a z-address.\n" + addr->text() + "\ndoesn't look like a z-address", QMessageBox::Ok, this); msg.exec(); @@ -261,7 +261,7 @@ void MainWindow::removeExtraAddresses() { // The last one is a spacer, so ignore that int totalItems = ui->sendToWidgets->children().size() - 2; - // Clear the first recepient fields + // Clear the first recipient fields auto addr = ui->sendToWidgets->findChild(QString("Address1")); addr->clear(); auto amt = ui->sendToWidgets->findChild(QString("Amount1")); @@ -511,7 +511,7 @@ void MainWindow::sendButton() { } QString MainWindow::doSendTxValidations(Tx tx) { - // 1. Addresses are valid format. + // 1. Addresses have valid format. QRegExp zcexp("^z[a-z0-9]{94}$", Qt::CaseInsensitive); QRegExp zsexp("^z[a-z0-9]{77}$", Qt::CaseInsensitive); QRegExp ztsexp("^ztestsapling[a-z0-9]{76}", Qt::CaseInsensitive); diff --git a/src/senttxstore.cpp b/src/senttxstore.cpp index a0455e9..f5d587d 100644 --- a/src/senttxstore.cpp +++ b/src/senttxstore.cpp @@ -54,7 +54,7 @@ void SentTxStore::addToSentTx(Tx tx, QString txid) { if (!Settings::getInstance()->getSaveZtxs()) return; - // Also, only store outgoing Txs where the from address is a z-Addr. Else, regular zcashd + // Also, only store outgoing txs where the from address is a z-Addr. Else, regular zcashd // stores it just fine if (!tx.fromAddr.startsWith("z")) return; @@ -101,4 +101,4 @@ void SentTxStore::addToSentTx(Tx tx, QString txid) { writer.write(jsonDoc.toJson()); } writer.close(); -} \ No newline at end of file +} diff --git a/src/turnstile.cpp b/src/turnstile.cpp index d874160..fb2211f 100644 --- a/src/turnstile.cpp +++ b/src/turnstile.cpp @@ -90,7 +90,7 @@ void Turnstile::planMigration(QString zaddr, QString destAddr, int numsplits, in auto bal = rpc->getAllBalances()->value(zaddr); auto splits = splitAmount(bal, numsplits); - // Then, generate an intermediate t-Address for each part using getBatchRPC + // Then, generate an intermediate t-address for each part using getBatchRPC rpc->getConnection()->doBatchRPC(splits, [=] (double /*unused*/) { json payload = { @@ -186,7 +186,7 @@ void Turnstile::fillAmounts(QList& amounts, double amount, int count) { } // Get a random amount off the amount (between half and full) and call recursively. - // Multiply by hundered, because we'll operate on 0.01 ZEC minimum. We'll divide by 100 later + // Multiply by hundred, because we'll operate on 0.01 ZEC minimum. We'll divide by 100 later double curAmount = std::rand() % (int)std::floor(amount * 100); // Try to round it off @@ -319,14 +319,14 @@ void Turnstile::executeMigrationStep() { } else if (nextStep->status == TurnstileMigrationItemStatus::SentToT) { // First thing to do is check to see if the funds are confirmed. - // We'll check both the original sprout address and the intermediate T addr for safety. + // We'll check both the original sprout address and the intermediate t-addr for safety. if (fnHasUnconfirmed(nextStep->intTAddr) || fnHasUnconfirmed(nextStep->fromAddr)) { //qDebug() << QString("unconfirmed, waiting"); return; } if (!rpc->getAllBalances()->keys().contains(nextStep->intTAddr)) { - qDebug() << QString("The intermediate Taddress doesn't have balance, even though it is confirmed"); + qDebug() << QString("The intermediate t-address doesn't have balance, even though it is confirmed"); return; } @@ -369,4 +369,4 @@ void Turnstile::doSendTx(Tx tx, std::function cb) { cb(); }); -} \ No newline at end of file +} diff --git a/src/utils.cpp b/src/utils.cpp index 5edd13e..25234aa 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -47,7 +47,7 @@ const QString Utils::getDevAddr(Tx tx) { return devAddr; } - // t-Addr, find if it is going to a sprout or sapling address + // t-Addr, find if it is going to a Sprout or Sapling address for (const ToFields& to : tx.toAddrs) { devAddr = testnetAddrLookup(to.addr); if (!devAddr.isEmpty()) { @@ -55,7 +55,7 @@ const QString Utils::getDevAddr(Tx tx) { } } - // If this is a t-Addr -> t-Addr transaction, use the sapling address by default + // If this is a t-Addr -> t-Addr transaction, use the Sapling address by default return testnetAddrLookup("ztestsapling"); } else { // Mainnet doesn't have a fee yet! @@ -74,4 +74,4 @@ double Utils::getDevFee() { return 0; } } -double Utils::getTotalFee() { return getMinerFee() + getDevFee(); } \ No newline at end of file +double Utils::getTotalFee() { return getMinerFee() + getDevFee(); } From 47a01a09955a8d5150c342b46a7a848048a34841 Mon Sep 17 00:00:00 2001 From: adityapk Date: Mon, 5 Nov 2018 09:56:56 -0800 Subject: [PATCH 2/7] Add support for z-board.net --- src/mainwindow.cpp | 57 +++++++++++++++++ src/mainwindow.h | 1 + src/mainwindow.ui | 20 ++++-- src/memodialog.ui | 2 +- src/sendtab.cpp | 12 +++- src/utils.cpp | 13 ++++ src/utils.h | 2 + src/zboard.ui | 155 +++++++++++++++++++++++++++++++++++++++++++++ zec-qt-wallet.pro | 3 +- 9 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/zboard.ui diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index aff366c..2c41619 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "ui_mainwindow.h" +#include "ui_zboard.h" #include "ui_privkey.h" #include "ui_about.h" #include "ui_settings.h" @@ -46,6 +47,9 @@ MainWindow::MainWindow(QWidget *parent) : // Export All Private Keys QObject::connect(ui->actionExport_All_Private_Keys, &QAction::triggered, this, &MainWindow::exportAllKeys); + // z-Board.net + QObject::connect(ui->actionz_board_net, &QAction::triggered, this, &MainWindow::postToZBoard); + // Set up about action QObject::connect(ui->actionAbout, &QAction::triggered, [=] () { QDialog aboutDialog(this); @@ -417,6 +421,59 @@ void MainWindow::donate() { ui->tabWidget->setCurrentIndex(1); } +void MainWindow::postToZBoard() { + QDialog d(this); + Ui_zboard zb; + zb.setupUi(&d); + + // Fill the from field with sapling addresses. + for (auto i = rpc->getAllBalances()->keyBegin(); i != rpc->getAllBalances()->keyEnd(); i++) { + if (Settings::getInstance()->isSaplingAddress(*i) && rpc->getAllBalances()->value(*i) > 0) { + zb.fromAddr->addItem(*i); + } + } + + // Testnet warning + if (Settings::getInstance()->isTestnet()) { + zb.testnetWarning->setText("You are on testnet, your post won't actually appear on z-board.net"); + } + else { + zb.testnetWarning->setText(""); + } + + zb.feeAmount->setText(Settings::getInstance()->getZECUSDDisplayFormat(Utils::getZboardAmount() + Utils::getMinerFee())); + if (d.exec() == QDialog::Accepted) { + // Create a transaction. + Tx tx; + + // Send from your first sapling address that has a balance. + tx.fromAddr = zb.fromAddr->currentText(); + if (tx.fromAddr.isEmpty()) { + QMessageBox::critical(this, "Error Posting Message", "You need a sapling address with available balance to post", QMessageBox::Ok); + } + + auto memo = zb.memoTxt->toPlainText().trimmed(); + if (!zb.postAs->text().trimmed().isEmpty()) + memo = "Name::" + zb.postAs->text().trimmed() + " " + memo; + + tx.toAddrs.push_back(ToFields{ Utils::getZboardAddr(), Utils::getZboardAmount(), memo, memo.toUtf8().toHex() }); + tx.fee = Utils::getMinerFee(); + + json params = json::array(); + rpc->fillTxJsonParams(params, tx); + std::cout << std::setw(2) << params << std::endl; + + // And send the Tx + rpc->sendZTransaction(params, [=](const json& reply) { + QString opid = QString::fromStdString(reply.get()); + ui->statusBar->showMessage("Computing Tx: " % opid); + + // And then start monitoring the transaction + rpc->addNewTxToWatch(tx, opid); + }); + } +} + void MainWindow::doImport(QList* keys) { qDebug() << keys->size(); if (keys->isEmpty()) { diff --git a/src/mainwindow.h b/src/mainwindow.h index a7a6871..d1abc5d 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -81,6 +81,7 @@ private: QString doSendTxValidations(Tx tx); void donate(); + void postToZBoard(); void importPrivKey(); void exportAllKeys(); void doImport(QList* keys); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index cda1018..2febbd1 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -316,8 +316,8 @@ 0 0 - 841 - 321 + 843 + 363 @@ -718,7 +718,7 @@ 0 0 889 - 22 + 21 @@ -727,7 +727,6 @@ - @@ -740,7 +739,15 @@ + + + Apps + + + + + @@ -787,6 +794,11 @@ Export All Private Keys + + + z-board.net + + diff --git a/src/memodialog.ui b/src/memodialog.ui index e659854..f34dc6a 100644 --- a/src/memodialog.ui +++ b/src/memodialog.ui @@ -11,7 +11,7 @@ - Dialog + Memo diff --git a/src/sendtab.cpp b/src/sendtab.cpp index 0471d12..61ceabd 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -245,7 +245,17 @@ void MainWindow::memoButtonClicked(int number) { QString txt = memoDialog.memoTxt->toPlainText(); memoDialog.memoSize->setText(QString::number(txt.toUtf8().size()) + "/512"); - memoDialog.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(txt.toUtf8().size() <= 512); + if (txt.toUtf8().size() <= 512) { + // Everything is fine + memoDialog.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + memoDialog.memoSize->setStyleSheet(""); + } + else { + // Overweight + memoDialog.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + memoDialog.memoSize->setStyleSheet("color: red;"); + } + }); memoDialog.memoTxt->setPlainText(currentMemo); diff --git a/src/utils.cpp b/src/utils.cpp index 25234aa..55380af 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -67,6 +67,19 @@ const QString Utils::getDevAddr(Tx tx) { double Utils::getMinerFee() { return 0.0001; } + +double Utils::getZboardAmount() { + return 0.0001; +} + +QString Utils::getZboardAddr() { + if (Settings::getInstance()->isTestnet()) { + return getDonationAddr(true); + } + else { + return "zs10m00rvkhfm4f7n23e4sxsx275r7ptnggx39ygl0vy46j9mdll5c97gl6dxgpk0njuptg2mn9w5s"; + } +} double Utils::getDevFee() { if (Settings::getInstance()->isTestnet()) { return 0.0001; diff --git a/src/utils.h b/src/utils.h index cbe841b..19d1ed5 100644 --- a/src/utils.h +++ b/src/utils.h @@ -17,6 +17,8 @@ public: static const QString getDonationAddr(bool sapling); static double getMinerFee(); + static double getZboardAmount(); + static QString getZboardAddr(); static double getDevFee(); static double getTotalFee(); diff --git a/src/zboard.ui b/src/zboard.ui new file mode 100644 index 0000000..ccacbe7 --- /dev/null +++ b/src/zboard.ui @@ -0,0 +1,155 @@ + + + zboard + + + + 0 + 0 + 520 + 376 + + + + Post to z-board.net + + + + + + + + + Send From + + + + + + + Qt::Horizontal + + + + + + + (optional) + + + + + + + Memo + + + + + + + 0 / 512 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Post As: + + + + + + + Total Fee + + + + + + + + + + feeamount + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <html><head/><body><p>ZBoard is Fully anonymous and untraceable chat messages based on the ZCash blockchain. <a href="http://www.z-board.net/"><span style=" text-decoration: underline; color:#0000ff;">http://www.z-board.net/</span></a></p><p>Posting to ZBoard: #Main_Area</p></body></html> + + + true + + + true + + + + + + + color:red; + + + TextLabel + + + + + + + + + buttonBox + accepted() + zboard + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + zboard + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/zec-qt-wallet.pro b/zec-qt-wallet.pro index 2ece2d0..3f7bcc5 100644 --- a/zec-qt-wallet.pro +++ b/zec-qt-wallet.pro @@ -82,7 +82,8 @@ FORMS += \ src/turnstileprogress.ui \ src/privkey.ui \ src/memodialog.ui \ - src/connection.ui + src/connection.ui \ + src/zboard.ui win32: RC_ICONS = res/icon.ico From 226c121fc9bd84962fc17daafdb4684882119400 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 5 Nov 2018 10:16:41 -0800 Subject: [PATCH 3/7] small fixes for zboard.net support --- src/mainwindow.cpp | 4 +++- src/mainwindow.ui | 16 +++++++++++----- src/zboard.ui | 9 +++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2c41619..1205f25 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -442,6 +442,8 @@ void MainWindow::postToZBoard() { } zb.feeAmount->setText(Settings::getInstance()->getZECUSDDisplayFormat(Utils::getZboardAmount() + Utils::getMinerFee())); + zb.memoTxt->setFocus(); + if (d.exec() == QDialog::Accepted) { // Create a transaction. Tx tx; @@ -454,7 +456,7 @@ void MainWindow::postToZBoard() { auto memo = zb.memoTxt->toPlainText().trimmed(); if (!zb.postAs->text().trimmed().isEmpty()) - memo = "Name::" + zb.postAs->text().trimmed() + " " + memo; + memo = zb.postAs->text().trimmed() + "::" + memo; tx.toAddrs.push_back(ToFields{ Utils::getZboardAddr(), Utils::getZboardAmount(), memo, memo.toUtf8().toHex() }); tx.fee = Utils::getMinerFee(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 2febbd1..6534465 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -316,8 +316,8 @@ 0 0 - 843 - 363 + 841 + 321 @@ -718,7 +718,7 @@ 0 0 889 - 21 + 22 @@ -741,7 +741,7 @@ - Apps + &Apps @@ -783,6 +783,9 @@ Sapling &Turnstile + + Ctrl+A, Ctrl+T + @@ -796,7 +799,10 @@ - z-board.net + &z-board.net + + + Ctrl+A, Ctrl+Z diff --git a/src/zboard.ui b/src/zboard.ui index ccacbe7..a37e8f7 100644 --- a/src/zboard.ui +++ b/src/zboard.ui @@ -6,8 +6,8 @@ 0 0 - 520 - 376 + 588 + 431 @@ -117,6 +117,11 @@ + + fromAddr + postAs + memoTxt + From b2f758242dc8ee4f3900e7d5b44ab027e2126ff2 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 5 Nov 2018 12:09:06 -0800 Subject: [PATCH 4/7] #23 Basic address book support for sending addresses zboard small fixes --- src/addressbook.cpp | 192 ++++++++++++++++++++++++++++++++++++++++++++ src/addressbook.h | 39 +++++++++ src/addressbook.ui | 133 ++++++++++++++++++++++++++++++ src/mainwindow.cpp | 22 ++++- src/mainwindow.h | 1 + src/mainwindow.ui | 26 +++++- src/precompiled.h | 3 + src/sendtab.cpp | 19 ++++- src/utils.cpp | 11 +++ src/utils.h | 2 + src/zboard.ui | 2 +- zec-qt-wallet.pro | 9 ++- 12 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 src/addressbook.cpp create mode 100644 src/addressbook.h create mode 100644 src/addressbook.ui diff --git a/src/addressbook.cpp b/src/addressbook.cpp new file mode 100644 index 0000000..27f98dc --- /dev/null +++ b/src/addressbook.cpp @@ -0,0 +1,192 @@ +#include "addressbook.h" +#include "ui_addressbook.h" +#include "ui_mainwindow.h" +#include "settings.h" +#include "mainwindow.h" +#include "utils.h" + +AddressBookModel::AddressBookModel(QTableView *parent) + : QAbstractTableModel(parent) { + headers << "Label" << "Address"; + + this->parent = parent; + loadDataFromStorage(); +} + +AddressBookModel::~AddressBookModel() { + if (labels != nullptr) + saveDataToStorage(); + + delete labels; +} + +void AddressBookModel::saveDataToStorage() { + QFile file(writeableFile()); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + QDataStream out(&file); // we will serialize the data into the file + out << QString("v1") << *labels; + file.close(); + + // Save column positions + QSettings().setValue("addresstablegeometry", parent->horizontalHeader()->saveState()); +} + + +void AddressBookModel::loadDataFromStorage() { + QFile file(writeableFile()); + + delete labels; + labels = new QList>(); + + file.open(QIODevice::ReadOnly); + QDataStream in(&file); // read the data serialized from the file + QString version; + in >> version >> *labels; + + file.close(); + + parent->horizontalHeader()->restoreState(QSettings().value("addresstablegeometry").toByteArray()); +} + +void AddressBookModel::addNewLabel(QString label, QString addr) { + labels->push_back(QPair(label, addr)); + + dataChanged(index(0, 0), index(labels->size()-1, columnCount(index(0,0))-1)); + layoutChanged(); +} + +void AddressBookModel::removeItemAt(int row) { + if (row >= labels->size()) + return; + labels->removeAt(row); + + dataChanged(index(0, 0), index(labels->size()-1, columnCount(index(0,0))-1)); + layoutChanged(); +} + +QPair AddressBookModel::itemAt(int row) { + if (row >= labels->size()) return QPair(); + + return labels->at(row); +} + +QString AddressBookModel::writeableFile() { + auto filename = QStringLiteral("addresslabels.dat"); + + auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!dir.exists()) + QDir().mkpath(dir.absolutePath()); + + if (Settings::getInstance()->isTestnet()) { + return dir.filePath("testnet-" % filename); + } else { + return dir.filePath(filename); + } +} + +int AddressBookModel::rowCount(const QModelIndex&) const { + if (labels == nullptr) return 0; + return labels->size(); +} + +int AddressBookModel::columnCount(const QModelIndex&) const { + return headers.size(); +} + + +QVariant AddressBookModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole) { + switch(index.column()) { + case 0: return labels->at(index.row()).first; + case 1: return labels->at(index.row()).second; + } + } + return QVariant(); +} + + +QVariant AddressBookModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + return headers.at(section); + } + + return QVariant(); +} + +void AddressBook::open(MainWindow* parent, QLineEdit* target) { + QDialog d(parent); + Ui_addressBook ab; + ab.setupUi(&d); + + AddressBookModel model(ab.addresses); + ab.addresses->setModel(&model); + + // If there is no target, the we'll call the button "Ok", else "Pick" + if (target != nullptr) { + ab.buttonBox->button(QDialogButtonBox::Ok)->setText("Pick"); + } + + // If there is a target then make it the addr for the "Add to" button + if (target != nullptr && Utils::isValidAddress(target->text())) { + ab.addr->setText(target->text()); + ab.label->setFocus(); + } + + // Add new address button + QObject::connect(ab.addNew, &QPushButton::clicked, [&] () { + auto addr = ab.addr->text().trimmed(); + if (!addr.isEmpty() && !ab.label->text().isEmpty()) { + // Test if address is valid. + if (!Utils::isValidAddress(addr)) { + QMessageBox::critical(parent, "Address Format Error", addr + " doesn't seem to be a valid Zcash address.", QMessageBox::Ok); + } else { + model.addNewLabel(ab.label->text(), ab.addr->text()); + } + } + }); + + // Double-Click picks the item + QObject::connect(ab.addresses, &QTableView::doubleClicked, [&] (auto index) { + if (index.row() < 0) return; + + QString addr = model.itemAt(index.row()).second; + d.accept(); + target->setText(addr); + }); + + // Right-Click + ab.addresses->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(ab.addresses, &QTableView::customContextMenuRequested, [&] (QPoint pos) { + QModelIndex index = ab.addresses->indexAt(pos); + + if (index.row() < 0) return; + + QString addr = model.itemAt(index.row()).second; + + QMenu menu(parent); + + if (target != nullptr) { + menu.addAction("Pick", [&] () { + target->setText(addr); + }); + } + + menu.addAction("Copy Address", [&] () { + QGuiApplication::clipboard()->setText(addr); + parent->ui->statusBar->showMessage("Copied to clipboard", 3 * 1000); + }); + + menu.addAction("Delete Label", [&] () { + model.removeItemAt(index.row()); + }); + + menu.exec(ab.addresses->viewport()->mapToGlobal(pos)); + }); + + if (d.exec() == QDialog::Accepted && target != nullptr) { + auto selection = ab.addresses->selectionModel(); + if (selection->hasSelection()) { + target->setText(model.itemAt(selection->selectedRows().at(0).row()).second); + } + }; +} \ No newline at end of file diff --git a/src/addressbook.h b/src/addressbook.h new file mode 100644 index 0000000..5fe7666 --- /dev/null +++ b/src/addressbook.h @@ -0,0 +1,39 @@ +#ifndef ADDRESSBOOK_H +#define ADDRESSBOOK_H + +#include "precompiled.h" + +class MainWindow; + +class AddressBookModel : public QAbstractTableModel { + +public: + AddressBookModel(QTableView* parent); + ~AddressBookModel(); + + void addNewLabel(QString label, QString addr); + void removeItemAt(int row); + QPair itemAt(int row); + + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + +private: + void loadDataFromStorage(); + void saveDataToStorage(); + + QString writeableFile(); + + QTableView* parent; + QList>* labels = nullptr; + QStringList headers; +}; + +class AddressBook { +public: + static void open(MainWindow* parent, QLineEdit* target = nullptr); +}; + +#endif // ADDRESSBOOK_H \ No newline at end of file diff --git a/src/addressbook.ui b/src/addressbook.ui new file mode 100644 index 0000000..974cd79 --- /dev/null +++ b/src/addressbook.ui @@ -0,0 +1,133 @@ + + + addressBook + + + + 0 + 0 + 690 + 562 + + + + Address Book + + + + + + Add New Address + + + + + + Address (z-Addr or t-Addr) + + + + + + + + + + Label + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add to Address Book + + + + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + addressBook + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + addressBook + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1205f25..8646895 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,7 @@ #include "mainwindow.h" +#include "addressbook.h" #include "ui_mainwindow.h" +#include "ui_addressbook.h" #include "ui_zboard.h" #include "ui_privkey.h" #include "ui_about.h" @@ -50,6 +52,9 @@ MainWindow::MainWindow(QWidget *parent) : // z-Board.net QObject::connect(ui->actionz_board_net, &QAction::triggered, this, &MainWindow::postToZBoard); + // Address Book + QObject::connect(ui->action_Address_Book, &QAction::triggered, this, &MainWindow::addressBook); + // Set up about action QObject::connect(ui->actionAbout, &QAction::triggered, [=] () { QDialog aboutDialog(this); @@ -404,9 +409,23 @@ void MainWindow::setupSettingsModal() { } }; }); +} + +void MainWindow::addressBook() { + // Check to see if there is a target. + QRegExp re("Address[0-9]+", Qt::CaseInsensitive); + for (auto target: ui->sendToWidgets->findChildren(re)) { + if (target->hasFocus()) { + AddressBook::open(this, target); + return; + } + }; + // If there was no target, then just run with no target. + AddressBook::open(this); } + void MainWindow::donate() { // Set up a donation to me :) ui->Address1->setText(Utils::getDonationAddr( @@ -452,11 +471,12 @@ void MainWindow::postToZBoard() { tx.fromAddr = zb.fromAddr->currentText(); if (tx.fromAddr.isEmpty()) { QMessageBox::critical(this, "Error Posting Message", "You need a sapling address with available balance to post", QMessageBox::Ok); + return; } auto memo = zb.memoTxt->toPlainText().trimmed(); if (!zb.postAs->text().trimmed().isEmpty()) - memo = zb.postAs->text().trimmed() + "::" + memo; + memo = zb.postAs->text().trimmed() + ":: " + memo; tx.toAddrs.push_back(ToFields{ Utils::getZboardAddr(), Utils::getZboardAmount(), memo, memo.toUtf8().toHex() }); tx.fee = Utils::getMinerFee(); diff --git a/src/mainwindow.h b/src/mainwindow.h index d1abc5d..4f5005c 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -81,6 +81,7 @@ private: QString doSendTxValidations(Tx tx); void donate(); + void addressBook(); void postToZBoard(); void importPrivKey(); void exportAllKeys(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 6534465..838cd52 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -22,7 +22,7 @@ - 2 + 1 @@ -343,6 +343,13 @@ + + + + Address Book + + + @@ -727,7 +734,6 @@ - @@ -746,7 +752,15 @@ + + + &Edit + + + + + @@ -805,6 +819,14 @@ Ctrl+A, Ctrl+Z + + + Address &Book + + + Ctrl+B + + diff --git a/src/precompiled.h b/src/precompiled.h index 34e5b02..9210ca5 100644 --- a/src/precompiled.h +++ b/src/precompiled.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include #include #include diff --git a/src/sendtab.cpp b/src/sendtab.cpp index 61ceabd..146f556 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "ui_mainwindow.h" +#include "addressbook.h" #include "ui_confirm.h" #include "ui_memodialog.h" #include "settings.h" @@ -45,6 +46,12 @@ void MainWindow::setupSendTab() { this->addressChanged(1, text); }); + // The first address book button + QObject::connect(ui->AddressBook1, &QPushButton::clicked, [=] () { + AddressBook::open(this, ui->Address1); + }); + + // The first Amount button QObject::connect(ui->Amount1, &QLineEdit::textChanged, [=] (auto text) { this->amountChanged(1, text); @@ -143,6 +150,16 @@ void MainWindow::addAddressSection() { }); horizontalLayout_12->addWidget(Address1); + + auto addressBook1 = new QPushButton(verticalGroupBox); + addressBook1->setObjectName(QStringLiteral("AddressBook") % QString::number(itemNumber)); + addressBook1->setText("Address Book"); + QObject::connect(addressBook1, &QPushButton::clicked, [=] () { + AddressBook::open(this, Address1); + }); + + horizontalLayout_12->addWidget(addressBook1); + sendAddressLayout->addLayout(horizontalLayout_12); auto horizontalLayout_13 = new QHBoxLayout(); @@ -546,7 +563,5 @@ QString MainWindow::doSendTxValidations(Tx tx) { void MainWindow::cancelButton() { removeExtraAddresses(); - // Back to the balances tab - ui->tabWidget->setCurrentIndex(0); } diff --git a/src/utils.cpp b/src/utils.cpp index 55380af..bffe3e9 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -87,4 +87,15 @@ double Utils::getDevFee() { return 0; } } + double Utils::getTotalFee() { return getMinerFee() + getDevFee(); } + +bool Utils::isValidAddress(QString addr) { + QRegExp zcexp("^z[a-z0-9]{94}$", Qt::CaseInsensitive); + QRegExp zsexp("^z[a-z0-9]{77}$", Qt::CaseInsensitive); + QRegExp ztsexp("^ztestsapling[a-z0-9]{76}", Qt::CaseInsensitive); + QRegExp texp("^t[a-z0-9]{34}$", Qt::CaseInsensitive); + + return zcexp.exactMatch(addr) || texp.exactMatch(addr) || + ztsexp.exactMatch(addr) || zsexp.exactMatch(addr); +} diff --git a/src/utils.h b/src/utils.h index 19d1ed5..d7f9796 100644 --- a/src/utils.h +++ b/src/utils.h @@ -22,6 +22,8 @@ public: static double getDevFee(); static double getTotalFee(); + static bool isValidAddress(QString addr); + static const int updateSpeed = 20 * 1000; // 20 sec static const int quickUpdateSpeed = 5 * 1000; // 5 sec static const int priceRefreshSpeed = 60 * 60 * 1000; // 1 hr diff --git a/src/zboard.ui b/src/zboard.ui index a37e8f7..504d251 100644 --- a/src/zboard.ui +++ b/src/zboard.ui @@ -95,7 +95,7 @@ - <html><head/><body><p>ZBoard is Fully anonymous and untraceable chat messages based on the ZCash blockchain. <a href="http://www.z-board.net/"><span style=" text-decoration: underline; color:#0000ff;">http://www.z-board.net/</span></a></p><p>Posting to ZBoard: #Main_Area</p></body></html> + <html><head/><body><p>ZBoard is a fully anonymous and untraceable chat messages based on the ZCash blockchain. <a href="http://www.z-board.net/"><span style=" text-decoration: underline; color:#0000ff;">http://www.z-board.net/</span></a></p><p>Posting to ZBoard: #Main_Area</p></body></html> true diff --git a/zec-qt-wallet.pro b/zec-qt-wallet.pro index 3f7bcc5..c4088dc 100644 --- a/zec-qt-wallet.pro +++ b/zec-qt-wallet.pro @@ -53,7 +53,8 @@ SOURCES += \ src/turnstile.cpp \ src/utils.cpp \ src/qrcodelabel.cpp \ - src/connection.cpp + src/connection.cpp \ + src/addressbook.cpp HEADERS += \ src/mainwindow.h \ @@ -71,7 +72,8 @@ HEADERS += \ src/turnstile.h \ src/utils.h \ src/qrcodelabel.h \ - src/connection.h + src/connection.h \ + src/addressbook.h FORMS += \ src/mainwindow.ui \ @@ -83,7 +85,8 @@ FORMS += \ src/privkey.ui \ src/memodialog.ui \ src/connection.ui \ - src/zboard.ui + src/zboard.ui \ + src/addressbook.ui win32: RC_ICONS = res/icon.ico From add9421ab911134e60d5de7226873afd28ebde32 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 5 Nov 2018 12:18:10 -0800 Subject: [PATCH 5/7] v0.2.8 --- README.md | 4 ++-- zec-qt-wallet.pro | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 73702a3..80814b9 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Head over to the releases page and grab the latest binary. https://github.com/ad ### Linux Extract and run the binary ``` -tar -xvf zec-qt-wallet-v0.2.7.tar.gz -./zec-qt-wallet-v0.2.7/zec-qt-wallet +tar -xvf zec-qt-wallet-v0.2.8.tar.gz +./zec-qt-wallet-v0.2.8/zec-qt-wallet ``` ### Windows diff --git a/zec-qt-wallet.pro b/zec-qt-wallet.pro index c4088dc..b95ee68 100644 --- a/zec-qt-wallet.pro +++ b/zec-qt-wallet.pro @@ -13,7 +13,7 @@ PRECOMPILED_HEADER = src/precompiled.h greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = zec-qt-wallet -APP_VERSION=\\\"0.2.7\\\" +APP_VERSION=\\\"0.2.8\\\" TEMPLATE = app From 63222c0d1229d3e62b3b2ade61e46a2e8acdd9a7 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 5 Nov 2018 12:20:24 -0800 Subject: [PATCH 6/7] Move settings to edit->settings --- src/connection.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connection.cpp b/src/connection.cpp index 1745e33..7eed231 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -43,7 +43,7 @@ void ConnectionLoader::loadConnection() { QString explanation = QString() % "A zcash.conf was not found on this machine.\n\n" % "If you are connecting to a remote/non-standard node " - % "please set the host/port and user/password in the File->Settings menu."; + % "please set the host/port and user/password in the Edit->Settings menu."; showError(explanation); doRPCSetConnection(nullptr); @@ -105,13 +105,13 @@ void ConnectionLoader::refreshZcashdState(Connection* connection) { % (isZcashConfFound ? "A zcash.conf file was found, but a" : "A") % " connection to zcashd could not be established.\n\n" % "If you are connecting to a remote/non-standard node " - % "please set the host/port and user/password in the File->Settings menu"; + % "please set the host/port and user/password in the Edit->Settings menu"; this->showError(explanation); } else if (err == QNetworkReply::NetworkError::AuthenticationRequiredError) { QString explanation = QString() % "Authentication failed. The username / password you specified was " - % "not accepted by zcashd. Try changing it in the File->Settings menu"; + % "not accepted by zcashd. Try changing it in the Edit->Settings menu"; this->showError(explanation); } else if (err == QNetworkReply::NetworkError::InternalServerError && !res.is_discarded()) { From 24dca812f8753e34044689aabf7a497884b0a672 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 5 Nov 2018 12:49:57 -0800 Subject: [PATCH 7/7] ui cleanup --- src/mainwindow.cpp | 18 ++++++++++ src/zboard.ui | 84 +++++++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8646895..20afdb9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -461,6 +461,24 @@ void MainWindow::postToZBoard() { } zb.feeAmount->setText(Settings::getInstance()->getZECUSDDisplayFormat(Utils::getZboardAmount() + Utils::getMinerFee())); + + QObject::connect(zb.memoTxt, &QPlainTextEdit::textChanged, [=] () { + QString txt = zb.memoTxt->toPlainText(); + zb.memoSize->setText(QString::number(txt.toUtf8().size()) + "/512"); + + if (txt.toUtf8().size() <= 512) { + // Everything is fine + zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + zb.memoSize->setStyleSheet(""); + } + else { + // Overweight + zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + zb.memoSize->setStyleSheet("color: red;"); + } + + }); + zb.memoTxt->setFocus(); if (d.exec() == QDialog::Accepted) { diff --git a/src/zboard.ui b/src/zboard.ui index 504d251..7b8e466 100644 --- a/src/zboard.ui +++ b/src/zboard.ui @@ -14,38 +14,24 @@ Post to z-board.net - - - - - + + - Send From - - - - - - - Qt::Horizontal - - - - - - - (optional) + Total Fee - - + + - Memo + feeamount + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 0 / 512 @@ -55,7 +41,7 @@ - + Qt::Horizontal @@ -65,37 +51,38 @@ - - + + - Post As: + Memo - - - - Total Fee + + + + (optional) - - - - - + + - feeamount + Send From - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + Post As: - <html><head/><body><p>ZBoard is a fully anonymous and untraceable chat messages based on the ZCash blockchain. <a href="http://www.z-board.net/"><span style=" text-decoration: underline; color:#0000ff;">http://www.z-board.net/</span></a></p><p>Posting to ZBoard: #Main_Area</p></body></html> + <html><head/><body><p>ZBoard: Fully anonymous and untraceable chat messages based on the ZCash blockchain. <a href="http://www.z-board.net/"><span style=" text-decoration: underline; color:#0000ff;">http://www.z-board.net/</span></a></p><p>Posting to ZBoard: #Main_Area</p></body></html> true @@ -105,7 +92,13 @@ - + + + + + + + color:red; @@ -115,6 +108,13 @@ + + + + Qt::Horizontal + + +