diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index a477f388..302b0bef 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -62,6 +62,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa
// Websocket notifier
connect(websocketNotifier(), &WebsocketNotifier::CCSReceived, ui->ccsWidget->model(), &CCSModel::updateEntries);
+ connect(websocketNotifier(), &WebsocketNotifier::BountyReceived, ui->bountiesWidget->model(), &BountiesModel::updateBounties);
connect(websocketNotifier(), &WebsocketNotifier::RedditReceived, ui->redditWidget->model(), &RedditModel::updatePosts);
connect(websocketNotifier(), &WebsocketNotifier::RevuoReceived, ui->revuoWidget, &RevuoWidget::updateItems);
connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, this, &MainWindow::onUpdatesAvailable);
@@ -373,6 +374,7 @@ void MainWindow::initHome() {
ui->fiatTickerLayout->addWidget(m_balanceTickerWidget);
connect(ui->ccsWidget, &CCSWidget::selected, this, &MainWindow::showSendScreen);
+ connect(ui->bountiesWidget, &BountiesWidget::donate, this, &MainWindow::fillSendTab);
connect(ui->redditWidget, &RedditWidget::setStatusText, this, &MainWindow::setStatusText);
connect(ui->revuoWidget, &RevuoWidget::donate, [this](const QString &address, const QString &description){
m_sendWidget->fill(address, description);
@@ -935,6 +937,11 @@ void MainWindow::showSendTab() {
ui->tabWidget->setCurrentIndex(Tabs::SEND);
}
+void MainWindow::fillSendTab(const QString &address, const QString &description) {
+ m_sendWidget->fill(address, description);
+ ui->tabWidget->setCurrentIndex(Tabs::SEND);
+}
+
void MainWindow::showCalcWindow() {
m_windowCalc->show();
}
diff --git a/src/MainWindow.h b/src/MainWindow.h
index cdfe795b..b8fdfa2f 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -215,6 +215,7 @@ private:
void updateWidgetIcons();
bool verifyPassword();
void patchStylesheetMac();
+ void fillSendTab(const QString &address, const QString &description);
void userActivity();
void checkUserActivity();
diff --git a/src/MainWindow.ui b/src/MainWindow.ui
index 035666ec..513a251f 100644
--- a/src/MainWindow.ui
+++ b/src/MainWindow.ui
@@ -123,6 +123,28 @@
+
+
+ Bounties
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
/r/Monero
@@ -899,6 +921,12 @@
1
+
+ BountiesWidget
+ QWidget
+
+ 1
+
diff --git a/src/model/BountiesModel.cpp b/src/model/BountiesModel.cpp
new file mode 100644
index 00000000..8f999bff
--- /dev/null
+++ b/src/model/BountiesModel.cpp
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#include "BountiesModel.h"
+
+BountiesModel::BountiesModel(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+
+}
+
+void BountiesModel::clear() {
+ beginResetModel();
+
+ m_bounties.clear();
+
+ endResetModel();
+}
+
+void BountiesModel::updateBounties(const QList> &posts) {
+ beginResetModel();
+
+ m_bounties.clear();
+ for (const auto& post : posts) {
+ m_bounties.push_back(post);
+ }
+
+ endResetModel();
+}
+
+int BountiesModel::rowCount(const QModelIndex &parent) const{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return m_bounties.count();
+}
+
+int BountiesModel::columnCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return ModelColumn::COUNT;
+}
+
+QVariant BountiesModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_bounties.count())
+ return {};
+
+ QSharedPointer post = m_bounties.at(index.row());
+
+ if(role == Qt::DisplayRole) {
+ switch(index.column()) {
+ case Votes:
+ return QString::number(post->votes);
+ case Title:
+ return post->title;
+ case Status:
+ return post->status;
+ case Bounty: {
+ if (post->bountyAmount > 0) {
+ return QString("%1 XMR").arg(QString::number(post->bountyAmount, 'f', 5));
+ }
+ return "None";
+ }
+ default:
+ return {};
+ }
+ }
+ else if (role == Qt::TextAlignmentRole) {
+ switch(index.column()) {
+ case Votes:
+ case Status:
+ case Bounty:
+ return Qt::AlignRight;
+ default:
+ return {};
+ }
+ }
+ return {};
+}
+
+QVariant BountiesModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role != Qt::DisplayRole) {
+ return QVariant();
+ }
+ if (orientation == Qt::Horizontal)
+ {
+ switch(section) {
+ case Votes:
+ return QString("🡅");
+ case Title:
+ return QString("Title");
+ case Status:
+ return QString("Status");
+ case Bounty:
+ return QString("Bounty");
+ default:
+ return QVariant();
+ }
+ }
+ return QVariant();
+}
+
+QSharedPointer BountiesModel::post(int row) {
+ if (row < 0 || row >= m_bounties.size()) {
+ qCritical("%s: no reddit post for index %d", __FUNCTION__, row);
+ return QSharedPointer();
+ }
+
+ return m_bounties.at(row);
+}
\ No newline at end of file
diff --git a/src/model/BountiesModel.h b/src/model/BountiesModel.h
new file mode 100644
index 00000000..21aecb73
--- /dev/null
+++ b/src/model/BountiesModel.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTIESMODEL_H
+#define FEATHER_BOUNTIESMODEL_H
+
+#include
+#include
+
+#include "widgets/Bounty.h"
+
+class BountiesModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum ModelColumn
+ {
+ Title = 0,
+ Votes,
+ Status,
+ Bounty,
+ COUNT
+ };
+
+ explicit BountiesModel(QObject *parent);
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ void clear();
+ void updateBounties(const QList>& posts);
+
+ QSharedPointer post(int row);
+
+private:
+ QList> m_bounties;
+};
+
+
+#endif //FEATHER_BOUNTIESMODEL_H
diff --git a/src/utils/WebsocketNotifier.cpp b/src/utils/WebsocketNotifier.cpp
index 5ba833ed..6414a70a 100644
--- a/src/utils/WebsocketNotifier.cpp
+++ b/src/utils/WebsocketNotifier.cpp
@@ -55,6 +55,11 @@ void WebsocketNotifier::onWSMessage(const QJsonObject &msg) {
this->onWSCCS(ccs_data);
}
+ else if(cmd == "bounties") {
+ auto data = msg.value("data").toArray();
+ this->onWSBounties(data);
+ }
+
else if(cmd == "txFiatHistory") {
auto txFiatHistory_data = msg.value("data").toObject();
emit TxFiatHistoryReceived(txFiatHistory_data);
@@ -151,10 +156,6 @@ void WebsocketNotifier::onWSReddit(const QJsonArray& reddit_data) {
void WebsocketNotifier::onWSCCS(const QJsonArray &ccs_data) {
QList> l;
- QStringList fonts = {"state", "address", "author", "date",
- "title", "target_amount", "raised_amount",
- "percentage_funded", "contributions"};
-
for (auto &&entry: ccs_data) {
auto obj = entry.toObject();
auto c = QSharedPointer(new CCSEntry());
@@ -201,6 +202,24 @@ void WebsocketNotifier::onWSRevuo(const QJsonArray &revuo_data) {
emit RevuoReceived(l);
}
+void WebsocketNotifier::onWSBounties(const QJsonArray &bounties_data) {
+ QList> l;
+
+ for (const auto& entry : bounties_data) {
+ QJsonObject obj = entry.toObject();
+ auto bounty = new BountyEntry(obj.value("votes").toInt(),
+ obj.value("title").toString(),
+ obj.value("amount").toDouble(),
+ obj.value("link").toString(),
+ obj.value("address").toString(),
+ obj.value("status").toString());
+ QSharedPointer b = QSharedPointer(bounty);
+ l.append(b);
+ }
+
+ emit BountyReceived(l);
+}
+
void WebsocketNotifier::onWSUpdates(const QJsonObject &updates) {
emit UpdatesReceived(updates);
}
diff --git a/src/utils/WebsocketNotifier.h b/src/utils/WebsocketNotifier.h
index a0fff999..deeef78a 100644
--- a/src/utils/WebsocketNotifier.h
+++ b/src/utils/WebsocketNotifier.h
@@ -11,6 +11,7 @@
#include "networktype.h"
#include "nodes.h"
#include "prices.h"
+#include "widgets/Bounty.h"
#include "widgets/RedditPost.h"
#include "widgets/CCSEntry.h"
#include "widgets/RevuoItem.h"
@@ -37,6 +38,7 @@ signals:
void FiatRatesReceived(const QJsonObject &fiat_rates);
void RedditReceived(QList> L);
void CCSReceived(QList> L);
+ void BountyReceived(QList> L);
void RevuoReceived(QList> L);
void TxFiatHistoryReceived(const QJsonObject &data);
void UpdatesReceived(const QJsonObject &updates);
@@ -51,6 +53,7 @@ private slots:
void onWSNodes(const QJsonArray &nodes);
void onWSReddit(const QJsonArray &reddit_data);
void onWSCCS(const QJsonArray &ccs_data);
+ void onWSBounties(const QJsonArray &bounties_data);
void onWSRevuo(const QJsonArray &revuo_data);
void onWSUpdates(const QJsonObject &updates);
void onWSXMRigDownloads(const QJsonObject &downloads);
diff --git a/src/utils/config.cpp b/src/utils/config.cpp
index 426f96bf..ed44c821 100644
--- a/src/utils/config.cpp
+++ b/src/utils/config.cpp
@@ -79,6 +79,7 @@ static const QHash configStrings = {
{Config::blockExplorer,{QS("blockExplorer"), "exploremonero.com"}},
{Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}},
{Config::localMoneroFrontend, {QS("localMoneroFrontend"), "https://localmonero.co"}},
+ {Config::bountiesFrontend, {QS("bountiesFrontend"), "https://bounties.monero.social"}},
{Config::fiatSymbols, {QS("fiatSymbols"), QStringList{"USD", "EUR", "GBP", "CAD", "AUD", "RUB"}}},
{Config::cryptoSymbols, {QS("cryptoSymbols"), QStringList{"BTC", "ETH", "LTC", "XMR", "ZEC"}}},
diff --git a/src/utils/config.h b/src/utils/config.h
index cc3ca7ff..e85cf5fe 100644
--- a/src/utils/config.h
+++ b/src/utils/config.h
@@ -83,6 +83,7 @@ public:
blockExplorer,
redditFrontend,
localMoneroFrontend,
+ bountiesFrontend,
fiatSymbols,
cryptoSymbols,
diff --git a/src/widgets/BountiesWidget.cpp b/src/widgets/BountiesWidget.cpp
new file mode 100644
index 00000000..152afd09
--- /dev/null
+++ b/src/widgets/BountiesWidget.cpp
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#include "BountiesWidget.h"
+#include "ui_BountiesWidget.h"
+
+#include
+#include
+#include
+
+#include "model/BountiesModel.h"
+#include "utils/Utils.h"
+#include "utils/config.h"
+
+BountiesWidget::BountiesWidget(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::BountiesWidget)
+ , m_model(new BountiesModel(this))
+ , m_contextMenu(new QMenu(this))
+{
+ ui->setupUi(this);
+ ui->tableView->setModel(m_model);
+ this->setupTable();
+
+ m_contextMenu->addAction("View Bounty", this, &BountiesWidget::linkClicked);
+ m_contextMenu->addAction("Donate", this, &BountiesWidget::donateClicked);
+ connect(ui->tableView, &QHeaderView::customContextMenuRequested, this, &BountiesWidget::showContextMenu);
+
+ connect(ui->tableView, &QTableView::doubleClicked, this, &BountiesWidget::linkClicked);
+
+ ui->tableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+}
+
+BountiesModel * BountiesWidget::model() {
+ return m_model;
+}
+
+void BountiesWidget::linkClicked() {
+ QModelIndex index = ui->tableView->currentIndex();
+ auto post = m_model->post(index.row());
+
+ if (post)
+ Utils::externalLinkWarning(this, this->getLink(post->link));
+}
+
+void BountiesWidget::donateClicked() {
+ QModelIndex index = ui->tableView->currentIndex();
+ auto bounty = m_model->post(index.row());
+
+ if (bounty) {
+ emit donate(bounty->donationAddress, QString("Bounty: %1").arg(bounty->title));
+ }
+}
+
+void BountiesWidget::setupTable() {
+ ui->tableView->verticalHeader()->setVisible(false);
+ ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
+
+ ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ ui->tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
+}
+
+void BountiesWidget::showContextMenu(const QPoint &pos) {
+ QModelIndex index = ui->tableView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+
+ m_contextMenu->exec(ui->tableView->viewport()->mapToGlobal(pos));
+}
+
+QString BountiesWidget::getLink(const QString &permaLink) {
+ QString frontend = config()->get(Config::bountiesFrontend).toString();
+ return QString("%1/%2").arg(frontend, permaLink);
+}
+
+BountiesWidget::~BountiesWidget() = default;
\ No newline at end of file
diff --git a/src/widgets/BountiesWidget.h b/src/widgets/BountiesWidget.h
new file mode 100644
index 00000000..548e1c21
--- /dev/null
+++ b/src/widgets/BountiesWidget.h
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTIESWIDGET_H
+#define FEATHER_BOUNTIESWIDGET_H
+
+#include
+#include
+#include
+
+#include "model/BountiesModel.h"
+
+namespace Ui {
+ class BountiesWidget;
+}
+
+class BountiesWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit BountiesWidget(QWidget *parent = nullptr);
+ ~BountiesWidget() override;
+ BountiesModel* model();
+
+public slots:
+ void linkClicked();
+
+signals:
+ void setStatusText(const QString &msg, bool override, int timeout);
+ void donate(const QString &address, const QString &description);
+
+private:
+ void setupTable();
+ void showContextMenu(const QPoint &pos);
+ void donateClicked();
+ QString getLink(const QString &permaLink);
+
+ QScopedPointer ui;
+ BountiesModel *m_model;
+ QMenu *m_contextMenu;
+};
+
+#endif //FEATHER_BOUNTIESWIDGET_H
diff --git a/src/widgets/BountiesWidget.ui b/src/widgets/BountiesWidget.ui
new file mode 100644
index 00000000..d4e985af
--- /dev/null
+++ b/src/widgets/BountiesWidget.ui
@@ -0,0 +1,40 @@
+
+
+ BountiesWidget
+
+
+
+ 0
+ 0
+ 566
+ 372
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Qt::CustomContextMenu
+
+
+
+
+
+
+
+
diff --git a/src/widgets/Bounty.h b/src/widgets/Bounty.h
new file mode 100644
index 00000000..7dfd9edd
--- /dev/null
+++ b/src/widgets/Bounty.h
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTY_H
+#define FEATHER_BOUNTY_H
+
+#include
+#include
+
+struct BountyEntry {
+ BountyEntry(int votes, QString title, double bountyAmount, QString link, QString donationAddress, QString status)
+ : votes(votes), title(std::move(title)), bountyAmount(bountyAmount), link(std::move(link)),
+ donationAddress(std::move(donationAddress)), status(std::move(status)){};
+
+ int votes;
+ QString title;
+ double bountyAmount;
+ QString link;
+ QString donationAddress;
+ QString status;
+};
+
+#endif //FEATHER_BOUNTY_H