work on toasts, unify article sanitization

This commit is contained in:
Martin Rotter 2023-09-22 07:24:55 +02:00
parent 69dc7e0a32
commit 46fe50df91
33 changed files with 470 additions and 142 deletions

View File

@ -8,6 +8,8 @@
<file>./graphics/Breeze/categories/32/applications-system.svg</file>
<file>./graphics/Breeze/actions/32/arrow-down.svg</file>
<file>./graphics/Breeze/actions/32/arrow-down-double.svg</file>
<file>./graphics/Breeze/actions/32/arrow-left.svg</file>
<file>./graphics/Breeze/actions/32/arrow-right.svg</file>
<file>./graphics/Breeze/actions/32/arrow-up.svg</file>
<file>./graphics/Breeze/actions/32/arrow-up-double.svg</file>
<file>./graphics/Breeze/actions/32/call-start.svg</file>
@ -99,6 +101,8 @@
<file>./graphics/Breeze Dark/categories/32/applications-system.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-down.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-down-double.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-left.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-right.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-up.svg</file>
<file>./graphics/Breeze Dark/actions/32/arrow-up-double.svg</file>
<file>./graphics/Breeze Dark/actions/32/call-start.svg</file>
@ -274,6 +278,8 @@
<file>./graphics/Numix/22/categories/applications-system.svg</file>
<file>./graphics/Numix/22/actions/arrow-down.svg</file>
<file>./graphics/Numix/22/actions/arrow-down-double.svg</file>
<file>./graphics/Numix/22/actions/arrow-left.svg</file>
<file>./graphics/Numix/22/actions/arrow-right.svg</file>
<file>./graphics/Numix/22/actions/arrow-up.svg</file>
<file>./graphics/Numix/22/actions/arrow-up-double.svg</file>
<file>./graphics/Numix/22/actions/browser-download.svg</file>

View File

@ -1,4 +1,6 @@
set(SOURCES
core/articlelistnotificationmodel.cpp
core/articlelistnotificationmodel.h
core/feeddownloader.cpp
core/feeddownloader.h
core/feedsmodel.cpp

View File

@ -0,0 +1,23 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "core/articlelistnotificationmodel.h"
ArticleListNotificationModel::ArticleListNotificationModel(QObject* parent)
: QAbstractListModel(parent), m_currentPage(-1) {}
ArticleListNotificationModel::~ArticleListNotificationModel() {}
void ArticleListNotificationModel::setArticles(const QList<Message>& msgs) {
m_articles = msgs;
m_currentPage = 0;
}
void ArticleListNotificationModel::nextPage() {}
void ArticleListNotificationModel::previousPage() {}
int ArticleListNotificationModel::rowCount(const QModelIndex& parent) const {}
int ArticleListNotificationModel::columnCount(const QModelIndex& parent) const {}
QVariant ArticleListNotificationModel::data(const QModelIndex& index, int role) const {}

View File

@ -0,0 +1,33 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef ARTICLELISTNOTIFICATIONMODEL_H
#define ARTICLELISTNOTIFICATIONMODEL_H
#include <QAbstractListModel>
#include "core/message.h"
class ArticleListNotificationModel : public QAbstractListModel {
public:
explicit ArticleListNotificationModel(QObject* parent = nullptr);
virtual ~ArticleListNotificationModel();
void setArticles(const QList<Message>& msgs);
void nextPage();
void previousPage();
virtual int rowCount(const QModelIndex& parent) const;
virtual int columnCount(const QModelIndex& parent) const;
virtual QVariant data(const QModelIndex& index, int role) const;
signals:
void nextPagePossibleChanged(bool possible);
void previousPagePossibleChanged(bool possible);
private:
QList<Message> m_articles;
int m_currentPage;
};
#endif // ARTICLELISTNOTIFICATIONMODEL_H

View File

@ -413,15 +413,17 @@ void FeedDownloader::updateOneFeed(ServiceRoot* acc,
<< "microseconds.";
if (feed->status() != Feed::Status::NewMessages) {
feed->setStatus(updated_messages.first > 0 || updated_messages.second > 0 ? Feed::Status::NewMessages
: Feed::Status::Normal);
feed->setStatus((!updated_messages.m_all.isEmpty() || !updated_messages.m_unread.isEmpty())
? Feed::Status::NewMessages
: Feed::Status::Normal);
}
qDebugNN << LOGSEC_FEEDDOWNLOADER << updated_messages << " messages for feed " << feed->customId()
<< " stored in DB.";
qDebugNN << LOGSEC_FEEDDOWNLOADER << updated_messages.m_unread.size() << " unread messages and"
<< NONQUOTE_W_SPACE(updated_messages.m_all.size()) "total messages for feed"
<< QUOTE_W_SPACE(feed->customId()) << "stored in DB.";
if (updated_messages.first > 0) {
m_results.appendUpdatedFeed({feed, updated_messages.first});
if (!updated_messages.m_unread.isEmpty()) {
m_results.appendUpdatedFeed(feed, updated_messages.m_unread);
}
}
catch (const FeedFetchException& feed_ex) {
@ -445,7 +447,6 @@ void FeedDownloader::finalizeUpdate() {
qDebugNN << LOGSEC_FEEDDOWNLOADER << "Finished feed updates in thread"
<< QUOTE_W_SPACE_DOT(QThread::currentThreadId());
m_results.sort();
m_feeds.clear();
// Update of feeds has finished.
@ -528,7 +529,8 @@ QString FeedDownloadResults::overview(int how_many_feeds) const {
QStringList result;
for (int i = 0, number_items_output = qMin(how_many_feeds, m_updatedFeeds.size()); i < number_items_output; i++) {
result.append(m_updatedFeeds.at(i).first->title() + QSL(": ") + QString::number(m_updatedFeeds.at(i).second));
result.append(m_updatedFeeds.keys().at(i)->title() + QSL(": ") +
QString::number(m_updatedFeeds.value(m_updatedFeeds.keys().at(i)).size()));
}
QString res_str = result.join(QSL("\n"));
@ -540,22 +542,14 @@ QString FeedDownloadResults::overview(int how_many_feeds) const {
return res_str;
}
void FeedDownloadResults::appendUpdatedFeed(const QPair<Feed*, int>& feed) {
m_updatedFeeds.append(feed);
}
void FeedDownloadResults::sort() {
std::sort(m_updatedFeeds.begin(),
m_updatedFeeds.end(),
[](const QPair<Feed*, int>& lhs, const QPair<Feed*, int>& rhs) {
return lhs.second > rhs.second;
});
void FeedDownloadResults::appendUpdatedFeed(Feed* feed, const QList<Message>& updated_unread_msgs) {
m_updatedFeeds.insert(feed, updated_unread_msgs);
}
void FeedDownloadResults::clear() {
m_updatedFeeds.clear();
}
QList<QPair<Feed*, int>> FeedDownloadResults::updatedFeeds() const {
QHash<Feed*, QList<Message>> FeedDownloadResults::updatedFeeds() const {
return m_updatedFeeds;
}

View File

@ -5,29 +5,28 @@
#include <QObject>
#include <QFutureWatcher>
#include <QPair>
#include "core/message.h"
#include "exceptions/applicationexception.h"
#include "services/abstract/cacheforserviceroot.h"
#include "services/abstract/feed.h"
#include <QFutureWatcher>
#include <QHash>
#include <QPair>
class MessageFilter;
// Represents results of batch feed updates.
class FeedDownloadResults {
public:
QList<QPair<Feed*, int>> updatedFeeds() const;
QHash<Feed*, QList<Message>> updatedFeeds() const;
QString overview(int how_many_feeds) const;
void appendUpdatedFeed(const QPair<Feed*, int>& feed);
void sort();
void appendUpdatedFeed(Feed* feed, const QList<Message>& updated_unread_msgs);
void clear();
private:
// QString represents title if the feed, int represents count of newly downloaded messages.
QList<QPair<Feed*, int>> m_updatedFeeds;
QHash<Feed*, QList<Message>> m_updatedFeeds;
};
struct FeedUpdateRequest {

View File

@ -2,7 +2,9 @@
#include "core/message.h"
#include "miscellaneous/application.h"
#include "miscellaneous/textfactory.h"
#include "network-web/webfactory.h"
#include "services/abstract/feed.h"
#include "services/abstract/label.h"
@ -75,6 +77,8 @@ Message::Message() {
void Message::sanitize(const Feed* feed, bool fix_future_datetimes) {
// Sanitize title.
m_title = qApp->web()->stripTags(qApp->web()->unescapeHtml(m_title));
m_title = m_title
// Remove non-breaking spaces.
@ -89,6 +93,9 @@ void Message::sanitize(const Feed* feed, bool fix_future_datetimes) {
// Remove non-breaking zero-width spaces.
.remove(QChar(65279));
// Sanitize author.
m_author = qApp->web()->stripTags(qApp->web()->unescapeHtml(m_author));
// Sanitize URL.
m_url = m_url.trimmed();

View File

@ -1354,7 +1354,7 @@ QHash<QString, QStringList> DatabaseQueries::bagsOfMessages(const QSqlDatabase&
return ids;
}
QPair<int, int> DatabaseQueries::updateMessages(const QSqlDatabase& db,
UpdatedArticles DatabaseQueries::updateMessages(const QSqlDatabase& db,
QList<Message>& messages,
Feed* feed,
bool force_update,
@ -1362,10 +1362,10 @@ QPair<int, int> DatabaseQueries::updateMessages(const QSqlDatabase& db,
bool* ok) {
if (messages.isEmpty()) {
*ok = true;
return {0, 0};
return {};
}
QPair<int, int> updated_messages = {0, 0};
UpdatedArticles updated_messages;
int account_id = feed->getParentServiceRoot()->accountId();
auto feed_custom_id = feed->customId();
@ -1615,10 +1615,10 @@ QPair<int, int> DatabaseQueries::updateMessages(const QSqlDatabase& db,
<< QUOTE_W_SPACE(message.m_url) << "in DB.";
if (!message.m_isRead) {
updated_messages.first++;
updated_messages.m_unread.append(message);
}
updated_messages.second++;
updated_messages.m_all.append(message);
message.m_insertedUpdated = true;
}
else if (query_update.lastError().isValid()) {
@ -1655,10 +1655,10 @@ QPair<int, int> DatabaseQueries::updateMessages(const QSqlDatabase& db,
}
if (!msg->m_isRead) {
updated_messages.first++;
updated_messages.m_unread.append(*msg);
}
updated_messages.second++;
updated_messages.m_all.append(*msg);
msg->m_insertedUpdated = true;
vals.append(QSL("\n(':feed', ':title', :is_read, :is_important, :is_deleted, "
@ -1742,10 +1742,10 @@ QPair<int, int> DatabaseQueries::updateMessages(const QSqlDatabase& db,
// but its assigned labels were changed. Therefore we must count article
// as updated.
if (!message.m_isRead) {
updated_messages.first++;
updated_messages.m_unread.append(message);
}
updated_messages.second++;
updated_messages.m_all.append(message);
}
}
else {

View File

@ -21,11 +21,6 @@
#include <QSqlError>
#include <QSqlQuery>
struct ArticleCounts {
int m_total = -1;
int m_unread = -1;
};
class DatabaseQueries {
public:
static QMap<int, QString> messageTableAttributes(bool only_msg_table, bool is_sqlite);
@ -162,7 +157,7 @@ class DatabaseQueries {
static void createOverwriteAccount(const QSqlDatabase& db, ServiceRoot* account);
// Returns counts of updated messages <unread, all>.
static QPair<int, int> updateMessages(const QSqlDatabase& db,
static UpdatedArticles updateMessages(const QSqlDatabase& db,
QList<Message>& messages,
Feed* feed,
bool force_update,

View File

@ -100,9 +100,10 @@
#define MAX_THREADPOOL_THREADS 32
#define WEB_BROWSER_SCROLL_STEP 50.0
#define NOTIFICATIONS_MARGIN 16
#define NOTIFICATIONS_WIDTH 256
#define NOTIFICATIONS_TIMEOUT 15s
#define NOTIFICATIONS_MARGIN 16
#define NOTIFICATIONS_WIDTH 256
#define NOTIFICATIONS_TIMEOUT 15s
#define NOTIFICATIONS_PAGE_SIZE 10
#define GOOGLE_SEARCH_URL "https://www.google.com/search?q=%1&ie=utf-8&oe=utf-8"
#define GOOGLE_SUGGEST_URL "http://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q=%1"

View File

@ -14,4 +14,14 @@ typedef QList<QPair<int, RootItem*>> Assignment;
typedef QPair<int, RootItem*> AssignmentItem;
typedef QPair<Message, RootItem::Importance> ImportanceChange;
struct ArticleCounts {
int m_total = -1;
int m_unread = -1;
};
struct UpdatedArticles {
QList<Message> m_unread;
QList<Message> m_all;
};
#endif // TYPEDEFS_H

View File

@ -2,9 +2,21 @@
#include "gui/notifications/articlelistnotification.h"
#include "miscellaneous/iconfactory.h"
ArticleListNotification::ArticleListNotification(QWidget* parent) : BaseToastNotification(parent) {
m_ui.setupUi(this);
setupCloseButton(m_ui.m_btnClose);
setupTimedClosing();
m_ui.m_btnNextPage->setIcon(qApp->icons()->fromTheme(QSL("arrow-right"), QSL("stock_right")));
m_ui.m_btnPreviousPage->setIcon(qApp->icons()->fromTheme(QSL("arrow-left"), QSL("stock_left")));
m_ui.m_btnOpenArticleList->setIcon(qApp->icons()->fromTheme(QSL("view-list-details")));
m_ui.m_btnOpenWebBrowser->setIcon(qApp->icons()->fromTheme(QSL("document-open")));
}
void ArticleListNotification::loadResults(const QHash<Feed*, QList<Message>>& new_messages) {
setupTimedClosing();
m_ui.m_treeArticles->setModel()
}

View File

@ -5,14 +5,20 @@
#include "gui/notifications/basetoastnotification.h"
#include "core/message.h"
#include "ui_articlelistnotification.h"
class Feed;
class ArticleListNotification : public BaseToastNotification {
Q_OBJECT
public:
explicit ArticleListNotification(QWidget* parent = nullptr);
void loadResults(const QHash<Feed*, QList<Message>>& new_messages);
private:
Ui::ArticleListNotification m_ui;
};

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ArticleListNotification</class>
<widget class="QDialog" name="ArticleListNotification">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>338</width>
<height>271</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="1" column="0" colspan="2">
<widget class="QTreeView" name="m_treeArticles">
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>true</bool>
</attribute>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="m_titleLayout">
<item>
<widget class="QLabel" name="m_lblTitle">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PlainToolButton" name="m_btnClose">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="PlainToolButton" name="m_btnPreviousPage">
<property name="toolTip">
<string>Go to previous page</string>
</property>
</widget>
</item>
<item>
<widget class="PlainToolButton" name="m_btnNextPage">
<property name="toolTip">
<string>Go to next page</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="PlainToolButton" name="m_btnOpenArticleList">
<property name="toolTip">
<string>Open article in article list</string>
</property>
</widget>
</item>
<item>
<widget class="PlainToolButton" name="m_btnOpenWebBrowser">
<property name="toolTip">
<string>Open article in web browser</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PlainToolButton</class>
<extends>QToolButton</extends>
<header>plaintoolbutton.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>m_btnClose</tabstop>
<tabstop>m_treeArticles</tabstop>
<tabstop>m_btnPreviousPage</tabstop>
<tabstop>m_btnNextPage</tabstop>
<tabstop>m_btnOpenArticleList</tabstop>
<tabstop>m_btnOpenWebBrowser</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -6,12 +6,13 @@
#include <QCloseEvent>
#include <QTimer>
#include <QTimerEvent>
#include <chrono>
using namespace std::chrono_literals;
BaseToastNotification::BaseToastNotification(QWidget* parent) : QDialog(parent) {
BaseToastNotification::BaseToastNotification(QWidget* parent) : QDialog(parent), m_timerId(-1) {
setAttribute(Qt::WidgetAttribute::WA_ShowWithoutActivating);
setFixedWidth(NOTIFICATIONS_WIDTH);
setFocusPolicy(Qt::FocusPolicy::NoFocus);
@ -39,8 +40,15 @@ void BaseToastNotification::setupCloseButton(QAbstractButton* btn) {
connect(btn, &QAbstractButton::clicked, this, &BaseToastNotification::close);
}
void BaseToastNotification::stopTimedClosing() {
killTimer(m_timerId);
m_timerId = -1;
}
void BaseToastNotification::setupTimedClosing() {
QTimer::singleShot(NOTIFICATIONS_TIMEOUT, this, &BaseToastNotification::close);
if (m_timerId < 0) {
m_timerId = startTimer(NOTIFICATIONS_TIMEOUT);
}
}
bool BaseToastNotification::eventFilter(QObject* watched, QEvent* event) {
@ -48,12 +56,28 @@ bool BaseToastNotification::eventFilter(QObject* watched, QEvent* event) {
return true;
}
else {
if (event->type() == QEvent::Type::Enter) {
stopTimedClosing();
}
if (event->type() == QEvent::Type::Leave) {
setupTimedClosing();
}
return QDialog::eventFilter(watched, event);
}
}
void BaseToastNotification::closeEvent(QCloseEvent* event) {
stopTimedClosing();
emit closeRequested(this);
}
void BaseToastNotification::reject() {}
void BaseToastNotification::timerEvent(QTimerEvent* event) {
if (event->timerId() == m_timerId) {
stopTimedClosing();
emit closeRequested(this);
}
}

View File

@ -19,10 +19,12 @@ class BaseToastNotification : public QDialog {
protected:
virtual bool eventFilter(QObject* watched, QEvent* event);
virtual void timerEvent(QTimerEvent* event);
virtual void closeEvent(QCloseEvent* event);
void setupCloseButton(QAbstractButton* btn);
void setupTimedClosing();
void setupCloseButton(QAbstractButton* btn);
void stopTimedClosing();
signals:
void closeRequested(BaseToastNotification* notif);

View File

@ -43,7 +43,10 @@ void ToastNotification::loadNotification(Notification::Event event, const GuiMes
if (action.m_action) {
m_ui.m_btnAction->setText(action.m_title.isEmpty() ? tr("Do it!") : action.m_title);
connect(m_ui.m_btnAction, &QPushButton::clicked, this, action.m_action);
connect(m_ui.m_btnAction, &QPushButton::clicked, this, [this, action]() {
action.m_action();
emit closeRequested(this);
});
}
else {
m_ui.m_mainLayout->removeItem(m_ui.m_actionLayout);

View File

@ -10,9 +10,6 @@
<height>143</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QFormLayout" name="m_mainLayout">
<property name="leftMargin">
<number>6</number>

View File

@ -12,6 +12,23 @@
#include <QScreen>
#include <QWindow>
QString ToastNotificationsManager::textForPosition(ToastNotificationsManager::NotificationPosition pos) {
switch (pos) {
case TopLeft:
return QObject::tr("top-left");
case TopRight:
return QObject::tr("top-right");
case BottomLeft:
return QObject::tr("bottom-left");
case BottomRight:
default:
return QObject::tr("bottom-right");
}
}
ToastNotificationsManager::ToastNotificationsManager(NotificationPosition position, int screen, QObject* parent)
: QObject(parent), m_position(position), m_screen(screen), m_articleListNotification(nullptr) {}
@ -50,9 +67,26 @@ void ToastNotificationsManager::clear() {
void ToastNotificationsManager::showNotification(Notification::Event event,
const GuiMessage& msg,
const GuiAction& action) {
ToastNotification* notif = new ToastNotification(event, msg, action, qApp->mainFormWidget());
BaseToastNotification* notif;
hookNotification(notif);
if (!msg.m_feedFetchResults.updatedFeeds().isEmpty()) {
if (m_articleListNotification == nullptr) {
m_articleListNotification = new ArticleListNotification();
hookNotification(m_articleListNotification);
}
else if (m_activeNotifications.contains(m_articleListNotification)) {
// Article notification is somewhere in list, clear first to move it to first positon.
closeNotification(m_articleListNotification, false);
}
m_articleListNotification->loadResults(msg.m_feedFetchResults.updatedFeeds());
notif = m_articleListNotification;
}
else {
notif = new ToastNotification(event, msg, action, qApp->mainFormWidget());
hookNotification(notif);
}
auto* screen = moveToProperScreen(notif);
@ -76,39 +110,6 @@ void ToastNotificationsManager::showNotification(Notification::Event event,
m_activeNotifications.prepend(notif);
}
void ToastNotificationsManager::showNotification(const QList<Message>& new_messages) {
if (m_articleListNotification == nullptr) {
m_articleListNotification = new ArticleListNotification();
hookNotification(m_articleListNotification);
}
if (!m_activeNotifications.isEmpty() && m_activeNotifications.first() != m_articleListNotification) {
// Article notification is somewhere in list, clear first to move it to first positon.
closeNotification(m_articleListNotification, false);
}
auto* screen = moveToProperScreen(m_articleListNotification);
// Insert new notification into free space.
m_articleListNotification->show();
auto notif_new_pos = cornerForNewNotification(screen->availableGeometry());
// Make sure notification is finally resized.
m_articleListNotification->adjustSize();
qApp->processEvents();
// Move notification, at this point we already need to know its precise size.
moveNotificationToCorner(m_articleListNotification, notif_new_pos);
// Remove out-of-bounds old notifications and shift existing
// ones to make space for new notifications.
removeOutOfBoundsNotifications(m_articleListNotification->height());
makeSpaceForNotification(m_articleListNotification->height());
m_activeNotifications.prepend(m_articleListNotification);
}
void ToastNotificationsManager::closeNotification(BaseToastNotification* notif, bool delete_from_memory) {
auto notif_idx = m_activeNotifications.indexOf(notif);
@ -160,7 +161,7 @@ QPoint ToastNotificationsManager::cornerForNewNotification(QRect screen_rect) {
void ToastNotificationsManager::hookNotification(BaseToastNotification* notif) {
connect(notif, &BaseToastNotification::closeRequested, this, [this](BaseToastNotification* notif) {
closeNotification(notif, false);
closeNotification(notif, notif != m_articleListNotification);
});
}

View File

@ -23,6 +23,10 @@ class ToastNotificationsManager : public QObject {
BottomRight = 3
};
Q_ENUM(NotificationPosition)
static QString textForPosition(ToastNotificationsManager::NotificationPosition pos);
explicit ToastNotificationsManager(ToastNotificationsManager::NotificationPosition position,
int screen,
QObject* parent = nullptr);
@ -41,7 +45,6 @@ class ToastNotificationsManager : public QObject {
public slots:
void clear();
void showNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action);
void showNotification(const QList<Message>& new_messages);
private slots:
void closeNotification(BaseToastNotification* notif, bool delete_from_memory);

View File

@ -26,6 +26,7 @@ HelpSpoiler::HelpSpoiler(QWidget* parent)
m_btnToggle->setCheckable(true);
m_btnToggle->setChecked(false);
m_content->setStyleSheet(QSL("QScrollArea { border: 1px solid %1; }").arg(palette().windowText().color().name()));
m_content->setSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Fixed);
m_content->setMaximumHeight(0);
m_content->setMinimumHeight(0);

View File

@ -8,6 +8,8 @@
#include "miscellaneous/settings.h"
#include <QDir>
#include <QMetaEnum>
#include <QScreen>
SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent) : SettingsPanel(settings, parent) {
m_ui.setupUi(this);
@ -25,6 +27,14 @@ SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent
connect(m_ui.m_rbNativeNotifications, &QRadioButton::toggled, this, &SettingsNotifications::dirtifySettings);
connect(m_ui.m_rbNativeNotifications, &QRadioButton::toggled, this, &SettingsNotifications::requireRestart);
connect(m_ui.m_sbScreen, &QSpinBox::valueChanged, this, &SettingsNotifications::dirtifySettings);
connect(m_ui.m_sbScreen, &QSpinBox::valueChanged, this, &SettingsNotifications::requireRestart);
m_ui.m_sbScreen->setMinimum(-1);
m_ui.m_sbScreen->setMaximum(QGuiApplication::screens().size() - 1);
connect(m_ui.m_sbScreen, &QSpinBox::valueChanged, this, &SettingsNotifications::showScreenInfo);
connect(m_ui.m_cbCustomNotificationsPosition,
&QComboBox::currentIndexChanged,
this,
@ -33,6 +43,15 @@ SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent
&QComboBox::currentIndexChanged,
this,
&SettingsNotifications::requireRestart);
QMetaEnum enm = QMetaEnum::fromType<ToastNotificationsManager::NotificationPosition>();
for (int i = 0; i < enm.keyCount(); i++) {
m_ui.m_cbCustomNotificationsPosition
->addItem(ToastNotificationsManager::
textForPosition(ToastNotificationsManager::NotificationPosition(enm.value(i))),
enm.value(i));
}
}
void SettingsNotifications::loadSettings() {
@ -43,6 +62,16 @@ void SettingsNotifications::loadSettings() {
->setChecked(settings()->value(GROUP(GUI), SETTING(GUI::EnableNotifications)).toBool());
m_ui.m_editor->loadNotifications(qApp->notifications()->allNotifications());
m_ui.m_rbNativeNotifications
->setChecked(!settings()->value(GROUP(GUI), SETTING(GUI::UseToastNotifications)).toBool());
m_ui.m_sbScreen->setValue(settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsScreen)).toInt());
m_ui.m_cbCustomNotificationsPosition
->setCurrentIndex(m_ui.m_cbCustomNotificationsPosition
->findData(settings()
->value(GROUP(GUI), SETTING(GUI::ToastNotificationsPosition))
.value<ToastNotificationsManager::NotificationPosition>()));
onEndLoadSettings();
}
@ -53,5 +82,29 @@ void SettingsNotifications::saveSettings() {
settings()->setValue(GROUP(GUI), GUI::EnableNotifications, m_ui.m_checkEnableNotifications->isChecked());
qApp->notifications()->save(m_ui.m_editor->allNotifications(), settings());
settings()->setValue(GROUP(GUI), GUI::UseToastNotifications, m_ui.m_rbCustomNotifications->isChecked());
settings()->setValue(GROUP(GUI), GUI::ToastNotificationsScreen, m_ui.m_sbScreen->value());
settings()->setValue(GROUP(GUI),
GUI::ToastNotificationsPosition,
m_ui.m_cbCustomNotificationsPosition->currentData()
.value<ToastNotificationsManager::NotificationPosition>());
onEndSaveSettings();
}
void SettingsNotifications::showScreenInfo(int index) {
QScreen* scr;
if (index < 0 || index >= QGuiApplication::screens().size()) {
scr = QGuiApplication::primaryScreen();
}
else {
scr = QGuiApplication::screens().at(index);
}
m_ui.m_lblScreenInfo->setText(QSL("%1 (%2x%3)")
.arg(scr->name(),
QString::number(scr->virtualSize().width()),
QString::number(scr->virtualSize().height())));
}

View File

@ -10,7 +10,7 @@
class Settings;
class SettingsNotifications : public SettingsPanel {
Q_OBJECT
Q_OBJECT
public:
explicit SettingsNotifications(Settings* settings, QWidget* parent = nullptr);
@ -19,6 +19,9 @@ class SettingsNotifications : public SettingsPanel {
virtual void loadSettings();
virtual void saveSettings();
private slots:
void showScreenInfo(int index);
private:
Ui::SettingsNotifications m_ui;
};

View File

@ -45,9 +45,6 @@
<property name="text">
<string>Native notifications (tray icon must be enabled)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
@ -55,6 +52,9 @@
<property name="text">
<string>Custom notifications</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
@ -76,6 +76,27 @@
<item row="0" column="1">
<widget class="QComboBox" name="m_cbCustomNotificationsPosition"/>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="m_sbScreen">
<property name="value">
<number>99</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Screen</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="m_lblScreenInfo">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -134,7 +134,7 @@ PreparedHtml TextBrowserViewer::prepareHtmlForMessage(const QList<Message>& mess
html.m_html += is_plain ? Qt::convertFromPlainText(message.m_contents, Qt::WhiteSpaceMode::WhiteSpaceNormal)
: message.m_contents;
static QRegularExpression img_tag_rgx("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>",
static QRegularExpression img_tag_rgx(QSL("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>"),
QRegularExpression::PatternOption::CaseInsensitiveOption |
QRegularExpression::PatternOption::InvertedGreedinessOption);

View File

@ -111,11 +111,13 @@ Application::Application(const QString& id, int& argc, char** argv, const QStrin
m_downloadManager = nullptr;
m_notifications = new NotificationFactory(this);
m_toastNotifications =
new ToastNotificationsManager(settings()
->value(GROUP(GUI), SETTING(GUI::ToastNotificationsPosition))
.value<ToastNotificationsManager::NotificationPosition>(),
settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsScreen)).toInt(),
this);
settings()->value(GROUP(GUI), SETTING(GUI::UseToastNotifications)).toBool()
? new ToastNotificationsManager(settings()
->value(GROUP(GUI), SETTING(GUI::ToastNotificationsPosition))
.value<ToastNotificationsManager::NotificationPosition>(),
settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsScreen)).toInt(),
this)
: nullptr;
m_shouldRestart = false;
#if defined(Q_OS_WIN)
@ -695,22 +697,26 @@ void Application::showGuiMessageCore(Notification::Event event,
GuiMessageDestination dest,
const GuiAction& action,
QWidget* parent) {
m_toastNotifications->showNotification(event, msg, action);
return;
if (m_notifications->areNotificationsEnabled()) {
auto notification = m_notifications->notificationForEvent(event);
notification.playSound(this);
if (SystemTrayIcon::isSystemTrayDesired() && SystemTrayIcon::isSystemTrayAreaAvailable() &&
notification.balloonEnabled() && dest.m_tray) {
trayIcon()->showMessage(msg.m_title.simplified().isEmpty() ? Notification::nameForEvent(notification.event())
: msg.m_title,
msg.m_message,
msg.m_type,
TRAY_ICON_BUBBLE_TIMEOUT,
std::move(action.m_action));
if (notification.balloonEnabled() && dest.m_tray) {
if (m_toastNotifications != nullptr) {
// Toasts are enabled.
m_toastNotifications->showNotification(event, msg, action);
}
else if (SystemTrayIcon::isSystemTrayDesired() && SystemTrayIcon::isSystemTrayAreaAvailable()) {
// Use tray icon balloons (which are implemented as native notifications on most systems.
trayIcon()->showMessage(msg.m_title.simplified().isEmpty() ? Notification::nameForEvent(notification.event())
: msg.m_title,
msg.m_message,
msg.m_type,
TRAY_ICON_BUBBLE_TIMEOUT,
std::move(action.m_action));
}
return;
}
}
@ -999,15 +1005,24 @@ void Application::onFeedUpdatesProgress(const Feed* feed, int current, int total
}
void Application::onFeedUpdatesFinished(const FeedDownloadResults& results) {
auto fds = results.updatedFeeds();
bool some_unquiet_feed = boolinq::from(fds).any([](const QPair<Feed*, int>& fd) {
return !fd.first->isQuiet();
auto fds = results.updatedFeeds().keys();
bool some_unquiet_feed = boolinq::from(fds).any([](Feed* fd) {
return !fd->isQuiet();
});
if (some_unquiet_feed) {
// Now, inform about results via GUI message/notification.
qApp->showGuiMessage(Notification::Event::NewUnreadArticlesFetched,
{tr("Unread articles fetched"), results.overview(10), QSystemTrayIcon::MessageIcon::NoIcon});
GuiMessage msg = {tr("Unread articles fetched"), QString(), QSystemTrayIcon::MessageIcon::NoIcon};
if (m_toastNotifications != nullptr) {
// Show custom and richer overview of updated feeds and articles.
msg.m_feedFetchResults = results;
}
else {
// Show simpler overview of updated feeds.
msg.m_message = results.overview(10);
}
qApp->showGuiMessage(Notification::Event::NewUnreadArticlesFetched, msg);
}
#if defined(Q_OS_WIN)

View File

@ -60,6 +60,7 @@ struct GuiMessage {
QString m_title;
QString m_message;
QSystemTrayIcon::MessageIcon m_type;
FeedDownloadResults m_feedFetchResults;
};
Q_DECLARE_METATYPE(GuiMessage)

View File

@ -255,19 +255,6 @@ void FeedReader::removeMessageFilterToFeedAssignment(Feed* feed, MessageFilter*
}
void FeedReader::updateAllFeeds() {
qApp
->showGuiMessage(Notification::Event::GeneralEvent,
GuiMessage(QDateTime::currentDateTime().toString(),
"Quisque ullamcorper ut purus nec tempus. Vivamus eros dolor, sagittis ultrices augue "
"ut, posuere fringilla lorem. Donec posuere, enim sit amet fermentum dignissim, tellus "
"lectus laoreet lectus, vestibulum laoreet felis tortor eget nunc. Curabitur sagittis "
"quam in scelerisque placerat. Vivamus vel porta tortor. Vivamus nec volutpat sem",
QSystemTrayIcon::MessageIcon::Information),
GuiMessageDestination(),
GuiAction("test", []() {
qDebugNN << "aa";
}));
updateFeeds(m_feedsModel->rootItem()->getSubTreeFeeds());
}

View File

@ -1110,8 +1110,8 @@ ServiceRoot::LabelOperation operator&(ServiceRoot::LabelOperation lhs, ServiceRo
return static_cast<ServiceRoot::LabelOperation>(static_cast<char>(lhs) & static_cast<char>(rhs));
}
QPair<int, int> ServiceRoot::updateMessages(QList<Message>& messages, Feed* feed, bool force_update, QMutex* db_mutex) {
QPair<int, int> updated_messages = {0, 0};
UpdatedArticles ServiceRoot::updateMessages(QList<Message>& messages, Feed* feed, bool force_update, QMutex* db_mutex) {
UpdatedArticles updated_messages;
if (messages.isEmpty()) {
qDebugNN << "No messages to be updated/added in DB for feed" << QUOTE_W_SPACE_DOT(feed->customId());
@ -1125,7 +1125,7 @@ QPair<int, int> ServiceRoot::updateMessages(QList<Message>& messages, Feed* feed
updated_messages = DatabaseQueries::updateMessages(database, messages, feed, force_update, db_mutex, &ok);
if (updated_messages.first > 0 || updated_messages.second > 0) {
if (!updated_messages.m_unread.isEmpty() || !updated_messages.m_all.isEmpty()) {
QMutexLocker lck(db_mutex);
// Something was added or updated in the DB, update numbers.

View File

@ -6,7 +6,6 @@
#include "services/abstract/rootitem.h"
#include "core/message.h"
#include "core/messagefilter.h"
#include "definitions/typedefs.h"
#include <QJsonDocument>
@ -206,7 +205,7 @@ class ServiceRoot : public RootItem {
void completelyRemoveAllData();
// Returns counts of updated messages <unread, all>.
QPair<int, int> updateMessages(QList<Message>& messages, Feed* feed, bool force_update, QMutex* db_mutex);
UpdatedArticles updateMessages(QList<Message>& messages, Feed* feed, bool force_update, QMutex* db_mutex);
QIcon feedIconForMessage(const QString& feed_custom_id) const;

View File

@ -418,7 +418,7 @@ QList<Message> FeedlyNetwork::decodeStreamContents(const QByteArray& stream_cont
Message message;
message.m_feedId = entry_obj[QSL("origin")].toObject()[QSL("streamId")].toString();
message.m_title = qApp->web()->stripTags(entry_obj[QSL("title")].toString());
message.m_title = entry_obj[QSL("title")].toString();
message.m_author = entry_obj[QSL("author")].toString();
message.m_contents = entry_obj[QSL("content")].toObject()[QSL("content")].toString();
message.m_rawContents = QJsonDocument(entry_obj).toJson(QJsonDocument::JsonFormat::Compact);

View File

@ -957,8 +957,8 @@ QList<Message> GreaderNetwork::decodeStreamContents(ServiceRoot* root,
auto message_obj = obj.toObject();
Message message;
message.m_title = qApp->web()->unescapeHtml(message_obj[QSL("title")].toString());
message.m_author = qApp->web()->unescapeHtml(message_obj[QSL("author")].toString());
message.m_title = message_obj[QSL("title")].toString();
message.m_author = message_obj[QSL("author")].toString();
message.m_created = QDateTime::fromSecsSinceEpoch(message_obj[QSL("published")].toInt(), Qt::TimeSpec::UTC);
message.m_createdFromFeed = true;
message.m_customId = message_obj[QSL("id")].toString();

View File

@ -100,9 +100,9 @@ QList<Message> FeedParser::messages() {
Message new_message;
// Fill available data.
new_message.m_title = qApp->web()->stripTags(qApp->web()->unescapeHtml(xmlMessageTitle(message_item)));
new_message.m_title = xmlMessageTitle(message_item);
new_message.m_contents = xmlMessageDescription(message_item);
new_message.m_author = qApp->web()->stripTags(qApp->web()->unescapeHtml(xmlMessageAuthor(message_item)));
new_message.m_author = xmlMessageAuthor(message_item);
new_message.m_url = xmlMessageUrl(message_item);
new_message.m_created = xmlMessageDateCreated(message_item);
new_message.m_customId = xmlMessageId(message_item);
@ -128,9 +128,9 @@ QList<Message> FeedParser::messages() {
Message new_message;
// Fill available data.
new_message.m_title = qApp->web()->stripTags(qApp->web()->unescapeHtml(jsonMessageTitle(message_item)));
new_message.m_title = jsonMessageTitle(message_item);
new_message.m_contents = jsonMessageDescription(message_item);
new_message.m_author = qApp->web()->stripTags(qApp->web()->unescapeHtml(jsonMessageAuthor(message_item)));
new_message.m_author = jsonMessageAuthor(message_item);
new_message.m_url = jsonMessageUrl(message_item);
new_message.m_created = jsonMessageDateCreated(message_item);
new_message.m_customId = jsonMessageId(message_item);