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
-
+
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..1ee0bb8235 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,24 @@ 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) {
+ expiring_polls << poll.second.m_title;
+ poll.second.m_expire_notified = true;
+ }
+ }
+ return expiring_polls;
}
std::vector VotingModel::buildPollTable(const PollFilterFlag flags)
@@ -271,6 +291,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 +302,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 +327,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;