Files
z-bar-qt/Plugins/ZShell/Services/notificationstore.hpp
T
zach 11e7aac195
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
feat: better notification storage and image caching + blobs
2026-11-02 19:00:43 +01:00

371 lines
10 KiB
C++

#pragma once
#include <QDateTime>
#include <QHash>
#include <QJSValue>
#include <QList>
#include <QObject>
#include <QPointer>
#include <qqmlintegration.h>
#include <QQmlListProperty>
#include <QSet>
#include <QString>
#include <QTimer>
namespace ZShell::config {
class Notifs;
}
namespace ZShell::Services {
class NotificationGroup;
class NotificationStore;
class NotifImageProvider;
class NotificationActionRow : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE(
"NotificationActionRow instances come from a NotificationStore")
Q_PROPERTY(bool invokable READ invokable NOTIFY invokableChanged)
Q_PROPERTY(QString identifier READ identifier CONSTANT)
Q_PROPERTY(QString text READ text CONSTANT)
public:
NotificationActionRow(
QObject* action,
const QString& identifier,
const QString& text,
QObject* parent = nullptr);
[[nodiscard]] bool invokable() const;
[[nodiscard]] QString identifier() const;
[[nodiscard]] QString text() const;
Q_INVOKABLE void invoke();
signals:
void invokableChanged();
private:
QPointer<QObject> m_action;
QString m_identifier;
QString m_text;
};
class NotificationRow : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("NotificationRow instances come from a NotificationStore")
Q_PROPERTY(
QQmlListProperty<NotificationActionRow> actions READ actions NOTIFY
actionsChanged)
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
Q_PROPERTY(QString appName READ appName NOTIFY appNameChanged)
Q_PROPERTY(QString body READ body NOTIFY bodyChanged)
Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)
Q_PROPERTY(
bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged)
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
Q_PROPERTY(int notificationId READ notificationId CONSTANT)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(bool popup READ popup WRITE setPopup NOTIFY popupChanged)
Q_PROPERTY(bool resident READ resident NOTIFY residentChanged)
Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged)
Q_PROPERTY(QDateTime time READ time CONSTANT)
Q_PROPERTY(QString timeStr READ timeStr NOTIFY timeStrChanged)
Q_PROPERTY(int urgency READ urgency NOTIFY urgencyChanged)
public:
explicit NotificationRow(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<NotificationActionRow> actions();
[[nodiscard]] QString appIcon() const;
[[nodiscard]] QString appName() const;
[[nodiscard]] QString body() const;
[[nodiscard]] bool closed() const;
[[nodiscard]] bool hasActionIcons() const;
[[nodiscard]] QString image() const;
[[nodiscard]] QString imageSource() const;
[[nodiscard]] int notificationId() const;
[[nodiscard]] int progress() const;
[[nodiscard]] bool popup() const;
void setPopup(bool popup);
[[nodiscard]] bool resident() const;
[[nodiscard]] QString summary() const;
[[nodiscard]] QDateTime time() const;
[[nodiscard]] QString timeStr() const;
[[nodiscard]] int urgency() const;
Q_INVOKABLE void close();
Q_INVOKABLE void dismiss();
Q_INVOKABLE void dismissPopup();
Q_INVOKABLE void holdPopup();
Q_INVOKABLE bool isSameAppIcon(const QString& path) const;
Q_INVOKABLE bool isSameImage(const QString& path) const;
Q_INVOKABLE void lock(QObject* item);
Q_INVOKABLE void pausePopup();
Q_INVOKABLE void ref(QObject* item);
Q_INVOKABLE void releasePopup();
Q_INVOKABLE void resumePopup();
Q_INVOKABLE void unref(QObject* item);
Q_INVOKABLE void unlock(QObject* item);
signals:
void actionsChanged();
void appIconChanged();
void appNameChanged();
void bodyChanged();
void closedChanged();
void hasActionIconsChanged();
void imageChanged();
void imageSourceChanged();
void popupChanged();
void progressChanged();
void residentChanged();
void summaryChanged();
void timeStrChanged();
void urgencyChanged();
private:
friend class NotificationGroup;
friend NotificationStore;
[[nodiscard]] bool expired(qint64 now) const;
void refreshTimeStr();
QList<NotificationActionRow*> m_actions;
NotificationGroup* m_group = nullptr;
NotificationGroup* m_popupGroup = nullptr;
NotificationStore* m_store = nullptr;
QString m_appIcon;
QString m_appName;
QString m_body;
QString m_digest;
QString m_image;
QString m_imageSource;
QString m_summary;
QString m_timeStr;
qint64 m_deadline = 0;
qint64 m_rowId = -1;
qint64 m_timeMs = 0;
QPointer<QObject> m_notification;
QSet<QObject*> m_locks;
QJSValue m_jsNotification;
// The store has already dropped this row from its lists, but a delegate is
// still animating it out; the delete waits for the last unlock.
bool m_deleteAfterUnlock = false;
bool m_closed = false;
bool m_hasActionIcons = false;
bool m_liveConnected = false;
bool m_popup = false;
bool m_popupPaused = false;
bool m_resident = false;
int m_notificationId = 0;
int m_progress = 0;
int m_urgency = 1;
};
class NotificationGroup : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("NotificationGroup instances come from a NotificationStore")
Q_PROPERTY(
QQmlListProperty<NotificationRow> activeNotifs READ activeNotifs NOTIFY
activeNotifsChanged)
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
Q_PROPERTY(QString appName READ appName CONSTANT)
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QString firstSummary READ firstSummary NOTIFY firstSummaryChanged)
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
Q_PROPERTY(
QQmlListProperty<NotificationRow> notifs READ notifs NOTIFY
notifsChanged)
Q_PROPERTY(QString timeStr READ timeStr NOTIFY timeStrChanged)
Q_PROPERTY(int urgency READ urgency NOTIFY urgencyChanged)
public:
NotificationGroup(const QString& appName, QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<NotificationRow> activeNotifs();
[[nodiscard]] QString appIcon() const;
[[nodiscard]] QString appName() const;
[[nodiscard]] int count() const;
[[nodiscard]] QString firstSummary() const;
[[nodiscard]] QString image() const;
[[nodiscard]] QQmlListProperty<NotificationRow> notifs();
[[nodiscard]] QString timeStr() const;
[[nodiscard]] int urgency() const;
Q_INVOKABLE bool isSameAppIcon(const QString& path) const;
Q_INVOKABLE bool isSameImage(const QString& path) const;
Q_INVOKABLE void lock(QObject* item);
Q_INVOKABLE void unlock(QObject* item);
signals:
void activeNotifsChanged();
void appIconChanged();
void countChanged();
void firstSummaryChanged();
void imageChanged();
void notifsChanged();
void timeStrChanged();
void urgencyChanged();
private:
friend NotificationStore;
void addRow(NotificationRow* row);
void refreshActive();
void release();
void removeRow(NotificationRow* row);
void sync();
QList<NotificationRow*> m_active;
QList<NotificationRow*> m_notifs;
// The group reads image/appIcon through the row it took them from, so a
// row swapping between its raw source and the cached blob keeps the card
// in sync instead of holding a stale copy.
QPointer<NotificationRow> m_iconSource;
QPointer<NotificationRow> m_imageSource;
QSet<QObject*> m_locks;
QString m_appName;
QString m_firstSummary;
QString m_timeStr;
int m_count = 0;
int m_urgency = 1;
bool m_releasing = false;
};
class NotificationStore : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
bool autoExpire READ autoExpire WRITE setAutoExpire NOTIFY
autoExpireChanged)
Q_PROPERTY(
QQmlListProperty<NotificationGroup> groups READ groups NOTIFY
groupsChanged)
Q_PROPERTY(
QQmlListProperty<NotificationGroup> popupGroups READ popupGroups NOTIFY
popupGroupsChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(
int popupTimeout READ popupTimeout WRITE setPopupTimeout NOTIFY
popupTimeoutChanged)
Q_PROPERTY(
QQmlListProperty<NotificationRow> rows READ rows NOTIFY rowsChanged)
public:
explicit NotificationStore(QObject* parent = nullptr);
~NotificationStore() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
[[nodiscard]] bool autoExpire() const;
void setAutoExpire(bool autoExpire);
[[nodiscard]] QQmlListProperty<NotificationGroup> groups();
[[nodiscard]] QQmlListProperty<NotificationGroup> popupGroups();
[[nodiscard]] int popupTimeout() const;
void setPopupTimeout(int timeout);
[[nodiscard]] QQmlListProperty<NotificationRow> rows();
Q_INVOKABLE NotificationRow* insert(
const QJSValue& notification, bool popup);
Q_INVOKABLE void clear();
signals:
void autoExpireChanged();
void groupsChanged();
void pathChanged();
void popupGroupsChanged();
void popupTimeoutChanged();
void rowsChanged();
private slots:
void handleLiveUpdate();
private:
friend NotificationRow;
void attachNotification(
NotificationRow* row, QObject* notification, const QJSValue& js);
void addGroup(NotificationRow* row);
void applyNotifConfig(const config::Notifs* notifs);
void closeDb();
void copyFields(NotificationRow* row, QObject* notification);
void flush();
void handleClosed(NotificationRow* row);
void insertDbRow(NotificationRow* row);
void loadRows();
void markDirty(NotificationRow* row);
void migrateLegacyJson();
[[nodiscard]] bool openDb();
void orphanCleanup();
void prune();
void addPopupGroup(NotificationRow* row);
void removePopupGroupRow(NotificationRow* row);
void updatePopupGroup(NotificationRow* row);
void registerProvider();
void removeGroupRow(NotificationRow* row);
void removeRow(NotificationRow* row);
// Coalesced rowsChanged: a burst of inserts/removes triggers one QML
// rebind on the next event loop turn instead of one per notification.
void scheduleRowsChanged();
[[nodiscard]] NotificationRow* rowForNotificationId(int id) const;
void startImageCache(NotificationRow* row);
void storeImageBlob(
const QPointer<NotificationRow>& row,
const QString& digest,
const QByteArray& png);
void tick();
void trimRows();
void updateDbRow(NotificationRow* row);
static QList<NotificationActionRow*> parseActions(
const QJSValue& actions, NotificationRow* row);
int m_popupTimeout = 5000;
qint64 m_nextPrune = 0;
qint64 m_nextTimeStrRefresh = 0;
bool m_autoExpire = true;
bool m_clearing = false;
bool m_rowsSignalPending = false;
QString m_uuid;
QString m_path;
QList<NotificationGroup*> m_groups;
QList<NotificationGroup*> m_popupGroups;
QList<NotificationRow*> m_rows;
QHash<QObject*, NotificationRow*> m_liveRows;
QHash<QString, NotificationGroup*> m_groupsByName;
QHash<QString, NotificationGroup*> m_popupGroupsByName;
QSet<NotificationRow*> m_dirty;
NotifImageProvider* m_provider = nullptr;
QTimer* m_flush = nullptr;
QTimer* m_tick = nullptr;
};
} // namespace ZShell::Services