// 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);
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);
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();
}
void updateWidgetIcons();
bool verifyPassword();
void patchStylesheetMac();
+ void fillSendTab(const QString &address, const QString &description);
void userActivity();
void checkUserActivity();
</item>
</layout>
</widget>
+ <widget class="QWidget" name="tab_4">
+ <attribute name="title">
+ <string>Bounties</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="BountiesWidget" name="bountiesWidget" native="true"/>
+ </item>
+ </layout>
+ </widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>/r/Monero</string>
<header>widgets/RevuoWidget.h</header>
<container>1</container>
</customwidget>
+ <customwidget>
+ <class>BountiesWidget</class>
+ <extends>QWidget</extends>
+ <header>widgets/BountiesWidget.h</header>
+ <container>1</container>
+ </customwidget>
</customwidgets>
<resources>
<include location="assets.qrc"/>
--- /dev/null
+// 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<QSharedPointer<BountyEntry>> &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<BountyEntry> 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<BountyEntry> BountiesModel::post(int row) {
+ if (row < 0 || row >= m_bounties.size()) {
+ qCritical("%s: no reddit post for index %d", __FUNCTION__, row);
+ return QSharedPointer<BountyEntry>();
+ }
+
+ return m_bounties.at(row);
+}
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTIESMODEL_H
+#define FEATHER_BOUNTIESMODEL_H
+
+#include <QAbstractTableModel>
+#include <QSharedPointer>
+
+#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<QSharedPointer<BountyEntry>>& posts);
+
+ QSharedPointer<BountyEntry> post(int row);
+
+private:
+ QList<QSharedPointer<BountyEntry>> m_bounties;
+};
+
+
+#endif //FEATHER_BOUNTIESMODEL_H
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);
void WebsocketNotifier::onWSCCS(const QJsonArray &ccs_data) {
QList<QSharedPointer<CCSEntry>> 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<CCSEntry>(new CCSEntry());
emit RevuoReceived(l);
}
+void WebsocketNotifier::onWSBounties(const QJsonArray &bounties_data) {
+ QList<QSharedPointer<BountyEntry>> 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<BountyEntry> b = QSharedPointer<BountyEntry>(bounty);
+ l.append(b);
+ }
+
+ emit BountyReceived(l);
+}
+
void WebsocketNotifier::onWSUpdates(const QJsonObject &updates) {
emit UpdatesReceived(updates);
}
#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"
void FiatRatesReceived(const QJsonObject &fiat_rates);
void RedditReceived(QList<QSharedPointer<RedditPost>> L);
void CCSReceived(QList<QSharedPointer<CCSEntry>> L);
+ void BountyReceived(QList<QSharedPointer<BountyEntry>> L);
void RevuoReceived(QList<QSharedPointer<RevuoItem>> L);
void TxFiatHistoryReceived(const QJsonObject &data);
void UpdatesReceived(const QJsonObject &updates);
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);
{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"}}},
blockExplorer,
redditFrontend,
localMoneroFrontend,
+ bountiesFrontend,
fiatSymbols,
cryptoSymbols,
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#include "BountiesWidget.h"
+#include "ui_BountiesWidget.h"
+
+#include <QDesktopServices>
+#include <QStandardItemModel>
+#include <QTableWidget>
+
+#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
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTIESWIDGET_H
+#define FEATHER_BOUNTIESWIDGET_H
+
+#include <QItemSelection>
+#include <QMenu>
+#include <QWidget>
+
+#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::BountiesWidget> ui;
+ BountiesModel *m_model;
+ QMenu *m_contextMenu;
+};
+
+#endif //FEATHER_BOUNTIESWIDGET_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BountiesWidget</class>
+ <widget class="QWidget" name="BountiesWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>566</width>
+ <height>372</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTableView" name="tableView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright (c) 2020-2021, The Monero Project.
+
+#ifndef FEATHER_BOUNTY_H
+#define FEATHER_BOUNTY_H
+
+#include <QString>
+#include <utility>
+
+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