From b0750212294446e7ea05be359f15d1b08a82a5d4 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sun, 19 Nov 2023 12:49:39 -0500 Subject: [PATCH] Implement GUI machinery for poll expiry notification --- src/qt/bitcoingui.cpp | 31 ++++++++++++- src/qt/bitcoingui.h | 1 + src/qt/forms/optionsdialog.ui | 33 +++++++++++++- src/qt/optionsdialog.cpp | 86 +++++++++++++++++++++++++++++++---- src/qt/optionsdialog.h | 4 ++ src/qt/optionsmodel.cpp | 12 +++++ src/qt/optionsmodel.h | 5 +- src/qt/voting/votingmodel.cpp | 30 +++++++++++- src/qt/voting/votingmodel.h | 17 +++++++ 9 files changed, 205 insertions(+), 14 deletions(-) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 60444b4fe7..bd0b96456f 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -1932,10 +1932,20 @@ void BitcoinGUI::handleNewPoll() overviewPage->setCurrentPollTitle(votingModel->getCurrentPollTitle()); } -void BitcoinGUI::handleExpiredPoll() +//! +//! \brief BitcoinGUI::extracted. Helper function to avoid container detach on range loop warning. +//! \param expiring_polls +//! \param notification +//! +void BitcoinGUI::extracted(QStringList& expiring_polls, QString& notification) { - // The only difference between this and handleNewPoll() is no call to the event notifier. + for (const auto& expiring_poll : expiring_polls) { + notification += expiring_poll + "\n"; + } +} +void BitcoinGUI::handleExpiredPoll() +{ if (!clientModel || !clientModel->getOptionsModel()) { return; } @@ -1944,6 +1954,23 @@ void BitcoinGUI::handleExpiredPoll() return; } + if (!clientModel->getOptionsModel()->getDisablePollNotifications()) { + QStringList expiring_polls = votingModel->getExpiringPollsNotNotified(); + + if (!expiring_polls.isEmpty()) { + QString notification = tr("The following poll(s) are about to expire:\n"); + + extracted(expiring_polls, notification); + + notification += tr("Open Gridcoin to vote."); + + notificator->notify( + Notificator::Information, + tr("Poll(s) about to expire"), + notification); + } + } + overviewPage->setCurrentPollTitle(votingModel->getCurrentPollTitle()); } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index eec17f954f..adbd9216e9 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -295,6 +295,7 @@ private slots: QString GetEstimatedStakingFrequency(unsigned int nEstimateTime); void handleNewPoll(); + void extracted(QStringList& expiring_polls, QString& notification); void handleExpiredPoll(); }; diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index ee3632e12c..9e95f8c909 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -369,6 +369,37 @@ + + + + + + Hours before poll expiry reminder + + + + + + + Valid values are between 0.25 and 24.0 hours. + + + + + + + Qt::Horizontal + + + + 80 + 20 + + + + + + @@ -585,7 +616,7 @@ QValidatedLineEdit QLineEdit -
qvalidatedlineedit.h
+
qvalidatedlineedit.h
QValueComboBox diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 697c489eaf..316f1e4a58 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -1,4 +1,5 @@ #include "optionsdialog.h" +#include "qevent.h" #include "ui_optionsdialog.h" #include "netbase.h" @@ -16,13 +17,16 @@ #include OptionsDialog::OptionsDialog(QWidget* parent) - : QDialog(parent) - , ui(new Ui::OptionsDialog) - , model(nullptr) - , mapper(nullptr) - , fRestartWarningDisplayed_Proxy(false) - , fRestartWarningDisplayed_Lang(false) - , fProxyIpValid(true) + : QDialog(parent) + , ui(new Ui::OptionsDialog) + , model(nullptr) + , mapper(nullptr) + , fRestartWarningDisplayed_Proxy(false) + , fRestartWarningDisplayed_Lang(false) + , fProxyIpValid(true) + , fStakingEfficiencyValid(true) + , fMinStakeSplitValueValid(true) + , fPollExpireNotifyValid(true) { ui->setupUi(this); @@ -44,6 +48,7 @@ OptionsDialog::OptionsDialog(QWidget* parent) ui->proxyIp->installEventFilter(this); ui->stakingEfficiency->installEventFilter(this); ui->minPostSplitOutputValue->installEventFilter(this); + ui->pollExpireNotifyLineEdit->installEventFilter(this); /* Window elements init */ #ifdef Q_OS_MAC @@ -103,17 +108,24 @@ OptionsDialog::OptionsDialog(QWidget* parent) connect(this, &OptionsDialog::proxyIpValid, this, &OptionsDialog::handleProxyIpValid); connect(this, &OptionsDialog::stakingEfficiencyValid, this, &OptionsDialog::handleStakingEfficiencyValid); connect(this, &OptionsDialog::minStakeSplitValueValid, this, &OptionsDialog::handleMinStakeSplitValueValid); + /** setup/change UI elements when poll expiry notification time window is valid/invalid */ + connect(this, &OptionsDialog::pollExpireNotifyValid, this, &OptionsDialog::handlePollExpireNotifyValid); if (fTestNet) ui->disableUpdateCheck->setHidden(true); ui->gridcoinAtStartupMinimised->setHidden(!ui->gridcoinAtStartup->isChecked()); ui->limitTxnDisplayDateEdit->setHidden(!ui->limitTxnDisplayCheckBox->isChecked()); + ui->pollExpireNotifyLabel->setHidden(ui->disablePollNotifications->isChecked()); + ui->pollExpireNotifyLineEdit->setHidden(ui->disablePollNotifications->isChecked()); + connect(ui->gridcoinAtStartup, &QCheckBox::toggled, this, &OptionsDialog::hideStartMinimized); connect(ui->gridcoinAtStartupMinimised, &QCheckBox::toggled, this, &OptionsDialog::hideStartMinimized); connect(ui->limitTxnDisplayCheckBox, &QCheckBox::toggled, this, &OptionsDialog::hideLimitTxnDisplayDate); + connect(ui->disablePollNotifications, &QCheckBox::toggled, this , &OptionsDialog::hidePollExpireNotify); + bool stake_split_enabled = ui->enableStakeSplit->isChecked(); ui->stakingEfficiencyLabel->setHidden(!stake_split_enabled); @@ -180,6 +192,7 @@ void OptionsDialog::setMapper() /* Window */ mapper->addMapping(ui->disableTransactionNotifications, OptionsModel::DisableTrxNotifications); mapper->addMapping(ui->disablePollNotifications, OptionsModel::DisablePollNotifications); + mapper->addMapping(ui->pollExpireNotifyLineEdit, OptionsModel::PollExpireNotification); #ifndef Q_OS_MAC if (QSystemTrayIcon::isSystemTrayAvailable()) { mapper->addMapping(ui->minimizeToTray, OptionsModel::MinimizeToTray); @@ -194,7 +207,7 @@ void OptionsDialog::setMapper() mapper->addMapping(ui->styleComboBox, OptionsModel::WalletStylesheet,"currentData"); mapper->addMapping(ui->limitTxnDisplayCheckBox, OptionsModel::LimitTxnDisplay); mapper->addMapping(ui->limitTxnDisplayDateEdit, OptionsModel::LimitTxnDate); - mapper->addMapping(ui->displayAddresses, OptionsModel::DisplayAddresses); + mapper->addMapping(ui->displayAddresses, OptionsModel::DisplayAddresses); } void OptionsDialog::enableApplyButton() @@ -298,6 +311,14 @@ void OptionsDialog::hideLimitTxnDisplayDate() } } +void OptionsDialog::hidePollExpireNotify() +{ + if (model) { + ui->pollExpireNotifyLabel->setHidden(ui->disablePollNotifications->isChecked()); + ui->pollExpireNotifyLineEdit->setHidden(ui->disablePollNotifications->isChecked()); + } +} + void OptionsDialog::hideStakeSplitting() { if (model) @@ -368,9 +389,40 @@ void OptionsDialog::handleMinStakeSplitValueValid(QValidatedLineEdit *object, bo } } +void OptionsDialog::handlePollExpireNotifyValid(QValidatedLineEdit *object, bool fState) +{ + // this is used in a check before re-enabling the save buttons + fPollExpireNotifyValid = fState; + + if (fPollExpireNotifyValid) { + enableSaveButtons(); + ui->statusLabel->clear(); + } else { + disableSaveButtons(); + object->setValid(fPollExpireNotifyValid); + ui->statusLabel->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel->setText(tr("The supplied time for notification before poll expires must " + "be between 0.25 and 24 hours.")); + } +} + bool OptionsDialog::eventFilter(QObject *object, QEvent *event) { - if (event->type() == QEvent::FocusOut) + bool filter_event = false; + + if (event->type() == QEvent::FocusOut) { + filter_event = true; + } + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { + filter_event = true; + } + } + + if (filter_event) { if (object == ui->proxyIp) { @@ -423,6 +475,22 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } } } + + if (object == ui->pollExpireNotifyLineEdit) { + bool ok = false; + double hours = ui->pollExpireNotifyLineEdit->text().toDouble(&ok); + + if (!ok) { + emit pollExpireNotifyValid(ui->pollExpireNotifyLineEdit, false); + } else { + if (hours >= 0.25 && hours <= 24.0) { + emit pollExpireNotifyValid(ui->pollExpireNotifyLineEdit, true); + } else { + emit pollExpireNotifyValid(ui->pollExpireNotifyLineEdit, false); + } + } + } } + return QDialog::eventFilter(object, event); } diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 1687c00f72..ae7d2adf6e 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -47,14 +47,17 @@ private slots: void hideStartMinimized(); void hideLimitTxnDisplayDate(); void hideStakeSplitting(); + void hidePollExpireNotify(); void handleProxyIpValid(QValidatedLineEdit *object, bool fState); void handleStakingEfficiencyValid(QValidatedLineEdit *object, bool fState); void handleMinStakeSplitValueValid(QValidatedLineEdit *object, bool fState); + void handlePollExpireNotifyValid(QValidatedLineEdit *object, bool fState); signals: void proxyIpValid(QValidatedLineEdit *object, bool fValid); void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); void minStakeSplitValueValid(QValidatedLineEdit *object, bool fValid); + void pollExpireNotifyValid(QValidatedLineEdit *object, bool fValid); private: Ui::OptionsDialog *ui; @@ -65,6 +68,7 @@ private slots: bool fProxyIpValid; bool fStakingEfficiencyValid; bool fMinStakeSplitValueValid; + bool fPollExpireNotifyValid; }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index f435c2369f..ce76357a0b 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -57,6 +57,7 @@ void OptionsModel::Init() fLimitTxnDisplay = settings.value("fLimitTxnDisplay", false).toBool(); fMaskValues = settings.value("fMaskValues", false).toBool(); limitTxnDate = settings.value("limitTxnDate", QDate()).toDate(); + pollExpireNotification = settings.value("pollExpireNotification", 1.0).toDouble(); nReserveBalance = settings.value("nReserveBalance").toLongLong(); language = settings.value("language", "").toString(); walletStylesheet = settings.value("walletStylesheet", "dark").toString(); @@ -142,6 +143,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return QVariant(fMaskValues); case LimitTxnDate: return QVariant(limitTxnDate); + case PollExpireNotification: + return QVariant(pollExpireNotification); case DisableUpdateCheck: return QVariant(gArgs.GetBoolArg("-disableupdatecheck", false)); case DataDir: @@ -284,6 +287,10 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in limitTxnDate = value.toDate(); settings.setValue("limitTxnDate", limitTxnDate); break; + case PollExpireNotification: + pollExpireNotification = value.toDouble(); + settings.setValue("pollExpireNotification", pollExpireNotification); + break; case DisableUpdateCheck: gArgs.ForceSetArg("-disableupdatecheck", value.toBool() ? "1" : "0"); settings.setValue("fDisableUpdateCheck", value.toBool()); @@ -380,6 +387,11 @@ int64_t OptionsModel::getLimitTxnDateTime() return limitTxnDateTime.toMSecsSinceEpoch() / 1000; } +double OptionsModel::getPollExpireNotification() +{ + return pollExpireNotification; +} + bool OptionsModel::getStartAtStartup() { return fStartAtStartup; diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index f9d94bc834..d80009e66f 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -43,6 +43,7 @@ class OptionsModel : public QAbstractListModel EnableStakeSplit, // bool StakingEfficiency, // double MinStakeSplitValue, // int + PollExpireNotification, // double ContractChangeToInput, // bool MaskValues, // bool OptionIDRowCount @@ -71,6 +72,7 @@ class OptionsModel : public QAbstractListModel bool getMaskValues(); QDate getLimitTxnDate(); int64_t getLimitTxnDateTime(); + double getPollExpireNotification(); QString getLanguage() { return language; } QString getCurrentStyle(); QString getDataDir(); @@ -87,13 +89,14 @@ class OptionsModel : public QAbstractListModel bool fStartMin; bool fDisableTrxNotifications; bool fDisablePollNotifications; - bool bDisplayAddresses; + bool bDisplayAddresses; bool fMinimizeOnClose; bool fConfirmOnClose; bool fCoinControlFeatures; bool fLimitTxnDisplay; bool fMaskValues; QDate limitTxnDate; + double pollExpireNotification; QString language; QString walletStylesheet; QString dataDir; diff --git a/src/qt/voting/votingmodel.cpp b/src/qt/voting/votingmodel.cpp index 769374f64d..ffdc671de0 100644 --- a/src/qt/voting/votingmodel.cpp +++ b/src/qt/voting/votingmodel.cpp @@ -15,6 +15,7 @@ #include "gridcoin/voting/payloads.h" #include "logging.h" #include "main.h" +#include "optionsmodel.h" #include "qt/clientmodel.h" #include "qt/voting/votingmodel.h" #include "qt/walletmodel.h" @@ -159,6 +160,8 @@ VotingModel::VotingModel( m_last_poll_time = std::max(m_last_poll_time, iter->Ref().Time()); } } + + m_poll_expire_warning = static_cast(m_options_model.getPollExpireNotification() * 3600.0 * 1000.0); } VotingModel::~VotingModel() @@ -248,7 +251,25 @@ QStringList VotingModel::getActiveProjectUrls() const } return Urls; +} + +QStringList VotingModel::getExpiringPollsNotNotified() +{ + QStringList expiring_polls; + + QDateTime now = QDateTime::fromMSecsSinceEpoch(GetAdjustedTime() * 1000); + + // Populate the list and mark the poll items included in the list m_expire_notified true. + for (auto& poll : m_pollitems) { + if (now.msecsTo(poll.second.m_expiration) <= m_poll_expire_warning + && !poll.second.m_expire_notified + && !poll.second.m_self_voted) { + expiring_polls << poll.second.m_title; + poll.second.m_expire_notified = true; + } + } + return expiring_polls; } std::vector VotingModel::buildPollTable(const PollFilterFlag flags) @@ -271,6 +292,7 @@ std::vector VotingModel::buildPollTable(const PollFilterFlag flags) // poll item into the results and move on. bool pollitem_needs_rebuild = true; + bool pollitem_expire_notified = false; auto pollitems_iter = m_pollitems.find(iter->Ref().Txid()); // Note that the NewVoteReceived core signal will also be fired during reorgs where votes are reverted, @@ -281,6 +303,10 @@ std::vector VotingModel::buildPollTable(const PollFilterFlag flags) // Not stale... the cache entry is good. Insert into items to return and go to the next one. items.push_back(pollitems_iter->second); pollitem_needs_rebuild = false; + } else { + // Retain state for expire notification in the case of a stale poll item that needs to be + // refreshed. + pollitem_expire_notified = pollitems_iter->second.m_expire_notified; } } @@ -302,7 +328,9 @@ std::vector VotingModel::buildPollTable(const PollFilterFlag flags) try { if (std::optional item = BuildPollItem(iter)) { // This will replace any stale existing entry in the cache with the freshly built item. - // It will also correctly add a new entry for a new item. + // It will also correctly add a new entry for a new item. The state of the pending expiry + // notification is retained from the stale entry to the refreshed one. + item->m_expire_notified = pollitem_expire_notified; m_pollitems[iter->Ref().Txid()] = *item; items.push_back(std::move(*item)); } diff --git a/src/qt/voting/votingmodel.h b/src/qt/voting/votingmodel.h index 68896d44c0..e489e967f6 100644 --- a/src/qt/voting/votingmodel.h +++ b/src/qt/voting/votingmodel.h @@ -88,6 +88,7 @@ class PollItem GRC::PollResult::VoteDetail m_self_vote_detail; bool m_stale = true; + bool m_expire_notified = false; }; //! @@ -134,6 +135,20 @@ class VotingModel : public QObject QString getCurrentPollTitle() const; QStringList getActiveProjectNames() const; QStringList getActiveProjectUrls() const; + + //! + //! \brief getExpiringPollsNotNotified. This method populates a QStringList with + //! the polls in the pollitems cache that are within the m_poll_expire_warning window + //! and which have not previously been notified to the user. Since this method is + //! to be used to have the GUI immediately provide notification to the user, it also + //! marks each of the polls in the QStringList m_expire_notified = true so that they + //! will not appear again on this list (unless the wallet is restarted). This accomplishes + //! a single shot notification for each poll that is about to expire. + //! + //! \return QStringList of polls that are about to expire (within m_poll_expire_warning of + //! expiration), and which have not previously been included on the list (i.e. notified). + //! + QStringList getExpiringPollsNotNotified(); std::vector buildPollTable(const GRC::PollFilterFlag flags); CAmount estimatePollFee() const; @@ -158,6 +173,8 @@ class VotingModel : public QObject void newVoteReceived(QString poll_txid_string); private: + qint64 m_poll_expire_warning; + GRC::PollRegistry& m_registry; ClientModel& m_client_model; OptionsModel& m_options_model;