|
|
|
@@ -38,21 +38,14 @@ namespace {
|
|
|
|
|
constexpr int MAX_ROWS = 500;
|
|
|
|
|
constexpr int MAX_PER_GROUP = 50;
|
|
|
|
|
constexpr qint64 MAX_AGE_MS = 30LL * 24 * 60 * 60 * 1000;
|
|
|
|
|
constexpr int TICK_MS = 250;
|
|
|
|
|
constexpr int TICK_MS = 100;
|
|
|
|
|
constexpr int MAX_IMAGE_DIM = 512;
|
|
|
|
|
// Memoized icon theme file lookups (hits and misses) per theme configuration.
|
|
|
|
|
constexpr int MAX_ICON_PATHS = 256;
|
|
|
|
|
constexpr qint64 PRUNE_INTERVAL_MS = 4LL * 60 * 60 * 1000;
|
|
|
|
|
constexpr qint64 TIME_STR_REFRESH_MS = 2000;
|
|
|
|
|
// Coalesces content updates (live replaces_id edits, popup state) into one
|
|
|
|
|
// transaction instead of one UPDATE per changed field.
|
|
|
|
|
constexpr qint64 MAX_EXPIRE_MS = 5LL * 60 * 1000;
|
|
|
|
|
constexpr int FLUSH_MS = 300;
|
|
|
|
|
|
|
|
|
|
// Quickshell serves inline notification pictures through image://qsimage/
|
|
|
|
|
// (and qspixmap) handles that belong to the live notification, not to the
|
|
|
|
|
// shell: once that notification is replaced or dismissed, the handle is gone
|
|
|
|
|
// and the url fails to resolve. Such a source is only ever useful as the raw
|
|
|
|
|
// url long enough to read the bytes, so the UI must never see it.
|
|
|
|
|
bool transientImageSource(const QString& source) {
|
|
|
|
|
if (source.isEmpty()) {
|
|
|
|
|
return false;
|
|
|
|
@@ -64,8 +57,6 @@ bool transientImageSource(const QString& source) {
|
|
|
|
|
url.host() == QLatin1String("qspixmap"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The url the UI should use: the cached blob when there is one, and the raw
|
|
|
|
|
// source only while that source keeps resolving.
|
|
|
|
|
QString displayImage(const QString& source, const QString& digest) {
|
|
|
|
|
if (!digest.isEmpty()) {
|
|
|
|
|
return QStringLiteral("image://notifimg/") + digest;
|
|
|
|
@@ -74,14 +65,9 @@ QString displayImage(const QString& source, const QString& digest) {
|
|
|
|
|
return transientImageSource(source) ? QString() : source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Icon theme state, captured on the GUI thread. `QIcon::themeName()`,
|
|
|
|
|
// `fallbackThemeName()` and `themeSearchPaths()` touch GUI-thread only state,
|
|
|
|
|
// but the strings they return are safe to hand to a worker.
|
|
|
|
|
struct IconTheme {
|
|
|
|
|
QStringList searchPaths;
|
|
|
|
|
QStringList themes;
|
|
|
|
|
// Changes when the theme or the search paths change, so the path cache
|
|
|
|
|
// below can be dropped after a theme switch.
|
|
|
|
|
QString signature;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -104,21 +90,11 @@ IconTheme iconTheme() {
|
|
|
|
|
return theme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve an icon name to a file the theme actually ships, so notification
|
|
|
|
|
// images that only name an icon can still be cached. KF6's KIconLoader is not
|
|
|
|
|
// thread-safe (it crashed the shell when the icon provider was called from a
|
|
|
|
|
// worker) and QIcon painting is GUI-thread only, so this only builds candidate
|
|
|
|
|
// paths and stats them - which makes it worker-thread safe when the theme
|
|
|
|
|
// strings come from iconTheme(). Hits and misses are both memoized: a sender
|
|
|
|
|
// repeats the same icon name, and a name the theme lacks costs thousands of
|
|
|
|
|
// stat calls to discover.
|
|
|
|
|
QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|
|
|
|
if (name.isEmpty()) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stat results only change when a theme is installed or removed, which the
|
|
|
|
|
// signature covers; the cap just keeps a stream of odd names bounded.
|
|
|
|
|
static std::mutex cacheMutex;
|
|
|
|
|
static QHash<QString, QString> pathCache;
|
|
|
|
|
static QString cacheSignature;
|
|
|
|
@@ -171,7 +147,6 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|
|
|
|
const QString suffix =
|
|
|
|
|
candidateName + "." + extension;
|
|
|
|
|
|
|
|
|
|
// hicolor: <size>/<context>/<ext>/<name>.<ext>
|
|
|
|
|
const QString canonical = themeDir.filePath(
|
|
|
|
|
size + "/" + context + "/" + extension +
|
|
|
|
|
"/" + suffix);
|
|
|
|
@@ -179,15 +154,12 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|
|
|
|
return canonical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hicolor and Papirus both also ship
|
|
|
|
|
// <size>/<context>/<name>.<ext>
|
|
|
|
|
const QString sized = themeDir.filePath(
|
|
|
|
|
size + "/" + context + "/" + suffix);
|
|
|
|
|
if (QFileInfo::exists(sized)) {
|
|
|
|
|
return sized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// themes ordering size under context
|
|
|
|
|
const QString nested = themeDir.filePath(
|
|
|
|
|
context + "/" + size + "/" + suffix);
|
|
|
|
|
if (QFileInfo::exists(nested)) {
|
|
|
|
@@ -197,7 +169,6 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// theme without size directories: <context>/<name>.<ext>
|
|
|
|
|
for (const auto& context : contexts) {
|
|
|
|
|
for (const auto& extension : extensions) {
|
|
|
|
|
const QString plain = themeDir.filePath(
|
|
|
|
@@ -420,10 +391,6 @@ void NotificationRow::setPopup(bool popup) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_popup = popup;
|
|
|
|
|
m_popupPaused = false;
|
|
|
|
|
m_deadline =
|
|
|
|
|
popup ? QDateTime::currentMSecsSinceEpoch() + m_store->popupTimeout()
|
|
|
|
|
: 0;
|
|
|
|
|
emit popupChanged();
|
|
|
|
|
|
|
|
|
|
if (m_store != nullptr) {
|
|
|
|
@@ -473,9 +440,6 @@ void NotificationRow::lock(QObject* item) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_locks.insert(item);
|
|
|
|
|
// Goes through unlock() so releasing the last lock on an already-closed
|
|
|
|
|
// row finishes the removal the close started. The destroyed signal
|
|
|
|
|
// carries the dying object, which is the lock holder.
|
|
|
|
|
QObject::connect(item, &QObject::destroyed, this, [this](QObject* obj) {
|
|
|
|
|
unlock(obj);
|
|
|
|
|
});
|
|
|
|
@@ -500,15 +464,17 @@ void NotificationRow::unlock(QObject* item) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationRow::pausePopup() {
|
|
|
|
|
m_popupPaused = true;
|
|
|
|
|
if (m_popupGroup != nullptr) {
|
|
|
|
|
m_popupGroup->holdExpiry();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationRow::resumePopup() {
|
|
|
|
|
m_popupPaused = false;
|
|
|
|
|
if (m_popupGroup != nullptr) {
|
|
|
|
|
m_popupGroup->releaseExpiry();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The v2 notification UI uses caelestia's naming; these are thin aliases of the
|
|
|
|
|
// existing behaviour so both spellings work.
|
|
|
|
|
void NotificationRow::dismiss() {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
@@ -535,8 +501,6 @@ void NotificationRow::unref(QObject* item) {
|
|
|
|
|
unlock(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// True when path is this row's image, in either its raw or cached form, so the
|
|
|
|
|
// UI can tell "the same picture changed url" from "a different picture".
|
|
|
|
|
bool NotificationRow::isSameImage(const QString& path) const {
|
|
|
|
|
return path == m_image || path == m_imageSource;
|
|
|
|
|
}
|
|
|
|
@@ -545,11 +509,6 @@ bool NotificationRow::isSameAppIcon(const QString& path) const {
|
|
|
|
|
return path == m_appIcon;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationRow::expired(qint64 now) const {
|
|
|
|
|
return m_popup && !m_popupPaused && !m_closed && m_deadline > 0 &&
|
|
|
|
|
now >= m_deadline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationRow::refreshTimeStr() {
|
|
|
|
|
const qint64 diff = QDateTime::currentMSecsSinceEpoch() - m_timeMs;
|
|
|
|
|
const qint64 m = diff / 60000;
|
|
|
|
@@ -595,6 +554,26 @@ int NotificationGroup::count() const {
|
|
|
|
|
return m_count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationGroup::expireActive() const {
|
|
|
|
|
return m_expireActive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationGroup::expirePaused() const {
|
|
|
|
|
return m_expirePaused;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qreal NotificationGroup::expireProgress() const {
|
|
|
|
|
return m_expireProgress;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int NotificationGroup::expireRemaining() const {
|
|
|
|
|
return m_expireRemaining;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int NotificationGroup::expireTimeout() const {
|
|
|
|
|
return m_expireTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString NotificationGroup::firstSummary() const {
|
|
|
|
|
return m_firstSummary;
|
|
|
|
|
}
|
|
|
|
@@ -636,11 +615,6 @@ void NotificationGroup::removeRow(NotificationRow* row) {
|
|
|
|
|
|
|
|
|
|
QObject::disconnect(row, nullptr, this, nullptr);
|
|
|
|
|
emit notifsChanged();
|
|
|
|
|
// Must not drop the row from m_active here: refreshActive() only emits
|
|
|
|
|
// activeNotifsChanged when the list it rebuilds differs from the stored
|
|
|
|
|
// one, and a silently pre-edited m_active is equal to the rebuild. The
|
|
|
|
|
// popup stack reads activeNotifs, so skipping the emit left a closed row
|
|
|
|
|
// occupying its slot - an invisible gap other entries did not move into.
|
|
|
|
|
refreshActive();
|
|
|
|
|
sync();
|
|
|
|
|
}
|
|
|
|
@@ -708,8 +682,109 @@ void NotificationGroup::sync() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delegates compare their own shown url against the group's current source row,
|
|
|
|
|
// so a row swapping raw source <-> cached blob does not re-trigger the fade.
|
|
|
|
|
void NotificationGroup::resetExpiry(int timeout) {
|
|
|
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
|
|
|
|
|
|
if (timeout != m_expireTimeout) {
|
|
|
|
|
m_expireTimeout = timeout;
|
|
|
|
|
emit expireTimeoutChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (timeout <= 0) {
|
|
|
|
|
m_expireDeadline = 0;
|
|
|
|
|
m_expireHeld = 0;
|
|
|
|
|
|
|
|
|
|
if (m_expirePaused) {
|
|
|
|
|
m_expirePaused = false;
|
|
|
|
|
emit expirePausedChanged();
|
|
|
|
|
}
|
|
|
|
|
} else if (m_expirePaused) {
|
|
|
|
|
m_expireHeld = timeout;
|
|
|
|
|
} else {
|
|
|
|
|
m_expireDeadline = now + timeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshExpiry(now);
|
|
|
|
|
emit expireTimerRestarted();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationGroup::holdExpiry() {
|
|
|
|
|
if (m_expirePaused || m_expireDeadline <= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
|
m_expireHeld = (std::max)(qint64(0), m_expireDeadline - now);
|
|
|
|
|
m_expireDeadline = 0;
|
|
|
|
|
m_expirePaused = true;
|
|
|
|
|
|
|
|
|
|
emit expirePausedChanged();
|
|
|
|
|
refreshExpiry(now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationGroup::releaseExpiry() {
|
|
|
|
|
if (!m_expirePaused) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
|
m_expireDeadline = now + m_expireHeld;
|
|
|
|
|
m_expireHeld = 0;
|
|
|
|
|
m_expirePaused = false;
|
|
|
|
|
|
|
|
|
|
emit expirePausedChanged();
|
|
|
|
|
refreshExpiry(now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationGroup::clearExpiry() {
|
|
|
|
|
m_expireDeadline = 0;
|
|
|
|
|
m_expireHeld = 0;
|
|
|
|
|
|
|
|
|
|
if (m_expirePaused) {
|
|
|
|
|
m_expirePaused = false;
|
|
|
|
|
emit expirePausedChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshExpiry(QDateTime::currentMSecsSinceEpoch());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationGroup::expired(qint64 now) const {
|
|
|
|
|
return m_expireDeadline > 0 && !m_expirePaused && now >= m_expireDeadline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationGroup::refreshExpiry(qint64 now) {
|
|
|
|
|
qint64 remaining = 0;
|
|
|
|
|
if (m_expirePaused) {
|
|
|
|
|
remaining = m_expireHeld;
|
|
|
|
|
} else if (m_expireDeadline > 0) {
|
|
|
|
|
remaining = (std::max)(qint64(0), m_expireDeadline - now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (remaining != m_expireRemaining) {
|
|
|
|
|
m_expireRemaining = static_cast<int>(remaining);
|
|
|
|
|
emit expireRemainingChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qreal progress =
|
|
|
|
|
m_expireTimeout > 0
|
|
|
|
|
? (std::min)(1.0, static_cast<qreal>(remaining) / m_expireTimeout)
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
if (!qFuzzyCompare(progress, m_expireProgress)) {
|
|
|
|
|
m_expireProgress = progress;
|
|
|
|
|
emit expireProgressChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setExpireActive(m_expireDeadline > 0 || m_expirePaused);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationGroup::setExpireActive(bool active) {
|
|
|
|
|
if (active != m_expireActive) {
|
|
|
|
|
m_expireActive = active;
|
|
|
|
|
emit expireActiveChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationGroup::isSameImage(const QString& path) const {
|
|
|
|
|
return m_imageSource != nullptr && m_imageSource->isSameImage(path);
|
|
|
|
|
}
|
|
|
|
@@ -721,8 +796,6 @@ bool NotificationGroup::isSameAppIcon(const QString& path) const {
|
|
|
|
|
void NotificationGroup::lock(QObject* item) {
|
|
|
|
|
if (item != nullptr && !m_locks.contains(item)) {
|
|
|
|
|
m_locks.insert(item);
|
|
|
|
|
// No Qt::UniqueConnection here: it requires a member-function pointer,
|
|
|
|
|
// and the contains() check above already keeps this one-shot per item.
|
|
|
|
|
QObject::connect(
|
|
|
|
|
item, &QObject::destroyed, this, [this, item](QObject*) {
|
|
|
|
|
unlock(item);
|
|
|
|
@@ -737,8 +810,6 @@ void NotificationGroup::unlock(QObject* item) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Called by the store once the group has no rows left. A delegate may still be
|
|
|
|
|
// animating it out, so the delete waits for its last unlock.
|
|
|
|
|
void NotificationGroup::release() {
|
|
|
|
|
m_releasing = true;
|
|
|
|
|
if (m_locks.isEmpty()) {
|
|
|
|
@@ -758,9 +829,6 @@ NotificationStore::NotificationStore(QObject* parent)
|
|
|
|
|
m_flush->setInterval(FLUSH_MS);
|
|
|
|
|
QObject::connect(m_flush, &QTimer::timeout, this, &NotificationStore::flush);
|
|
|
|
|
|
|
|
|
|
// Config-backed settings, same pattern as TickingService: read the
|
|
|
|
|
// config node directly and update live on its change signals. QML may
|
|
|
|
|
// still write these properties; the next config change re-applies.
|
|
|
|
|
auto* notifs = config::ConfigSingleton::instance()->notifs();
|
|
|
|
|
applyNotifConfig(notifs);
|
|
|
|
|
QObject::connect(
|
|
|
|
@@ -818,6 +886,12 @@ void NotificationStore::setPopupTimeout(int timeout) {
|
|
|
|
|
|
|
|
|
|
m_popupTimeout = timeout;
|
|
|
|
|
emit popupTimeoutChanged();
|
|
|
|
|
|
|
|
|
|
if (m_autoExpire) {
|
|
|
|
|
for (auto* group : std::as_const(m_popupGroups)) {
|
|
|
|
|
group->resetExpiry(groupExpireTimeoutMs(group));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool NotificationStore::autoExpire() const {
|
|
|
|
@@ -831,6 +905,14 @@ void NotificationStore::setAutoExpire(bool autoExpire) {
|
|
|
|
|
|
|
|
|
|
m_autoExpire = autoExpire;
|
|
|
|
|
emit autoExpireChanged();
|
|
|
|
|
|
|
|
|
|
for (auto* group : std::as_const(m_popupGroups)) {
|
|
|
|
|
if (autoExpire) {
|
|
|
|
|
group->resetExpiry(groupExpireTimeoutMs(group));
|
|
|
|
|
} else {
|
|
|
|
|
group->clearExpiry();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::applyNotifConfig(const config::Notifs* notifs) {
|
|
|
|
@@ -881,9 +963,6 @@ NotificationRow* NotificationStore::insert(
|
|
|
|
|
attachNotification(row, wrapped, notification);
|
|
|
|
|
|
|
|
|
|
row->m_popup = popup;
|
|
|
|
|
if (popup) {
|
|
|
|
|
row->m_deadline = QDateTime::currentMSecsSinceEpoch() + m_popupTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
insertDbRow(row);
|
|
|
|
|
startImageCache(row);
|
|
|
|
@@ -929,8 +1008,6 @@ void NotificationStore::clear() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The bulk delete above already removed every DB row; the per-row
|
|
|
|
|
// cleanup would just re-delete no-ops, so batch it instead.
|
|
|
|
|
m_clearing = true;
|
|
|
|
|
const auto rows = m_rows;
|
|
|
|
|
for (auto* row : rows) {
|
|
|
|
@@ -953,6 +1030,11 @@ void NotificationStore::copyFields(NotificationRow* row, QObject* notification)
|
|
|
|
|
row->m_resident = notification->property("resident").toBool();
|
|
|
|
|
row->m_hasActionIcons = notification->property("hasActionIcons").toBool();
|
|
|
|
|
row->m_urgency = notification->property("urgency").toInt();
|
|
|
|
|
|
|
|
|
|
const auto expireTimeout = notification->property("expireTimeout");
|
|
|
|
|
row->m_expireTimeoutMs = expireTimeout.isValid()
|
|
|
|
|
? static_cast<qint64>(expireTimeout.toDouble())
|
|
|
|
|
: -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::attachNotification(
|
|
|
|
@@ -980,10 +1062,6 @@ void NotificationStore::attachNotification(
|
|
|
|
|
rowGuard->m_store->handleClosed(rowGuard);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Quickshell applies replaces_id updates to the object we already
|
|
|
|
|
// hold; mirror those changes onto the row (in UI and in the DB).
|
|
|
|
|
// Connected straight to the slot (not through an intermediate
|
|
|
|
|
// signal) so sender() in handleLiveUpdate is the Notification.
|
|
|
|
|
static const QMetaMethod slot = [] {
|
|
|
|
|
const QMetaObject& storeMeta = NotificationStore::staticMetaObject;
|
|
|
|
|
return storeMeta.method(
|
|
|
|
@@ -1000,6 +1078,7 @@ void NotificationStore::attachNotification(
|
|
|
|
|
"hasActionIconsChanged",
|
|
|
|
|
"actionsChanged",
|
|
|
|
|
"hintsChanged",
|
|
|
|
|
"expireTimeoutChanged",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const auto* meta = notification->metaObject();
|
|
|
|
@@ -1040,6 +1119,11 @@ void NotificationStore::handleLiveUpdate() {
|
|
|
|
|
const bool hasActionIcons = it.key()->property("hasActionIcons").toBool();
|
|
|
|
|
const int urgency = it.key()->property("urgency").toInt();
|
|
|
|
|
|
|
|
|
|
const auto expireTimeout = it.key()->property("expireTimeout");
|
|
|
|
|
row->m_expireTimeoutMs = expireTimeout.isValid()
|
|
|
|
|
? static_cast<qint64>(expireTimeout.toDouble())
|
|
|
|
|
: -1;
|
|
|
|
|
|
|
|
|
|
const bool imageChanged = imageSource != row->m_imageSource;
|
|
|
|
|
|
|
|
|
|
if (summary != row->m_summary) {
|
|
|
|
@@ -1095,12 +1179,6 @@ void NotificationStore::handleLiveUpdate() {
|
|
|
|
|
startImageCache(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New content deserves a full look: restart the popup countdown.
|
|
|
|
|
if (row->m_popup) {
|
|
|
|
|
row->m_deadline = QDateTime::currentMSecsSinceEpoch() + m_popupTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The row may have moved to another app name (replaces_id can change it).
|
|
|
|
|
if (row->m_group != nullptr && row->m_group->m_appName != row->m_appName) {
|
|
|
|
|
removeGroupRow(row);
|
|
|
|
|
addGroup(row);
|
|
|
|
@@ -1111,6 +1189,10 @@ void NotificationStore::handleLiveUpdate() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row->m_popupGroup != nullptr) {
|
|
|
|
|
touchExpiry(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
markDirty(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1199,7 +1281,6 @@ NotificationRow* NotificationStore::rowForNotificationId(int id) const {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::handleClosed(NotificationRow* row) {
|
|
|
|
|
// During clear() the bulk DELETE already removed every row.
|
|
|
|
|
if (m_clearing) {
|
|
|
|
|
removeRow(row);
|
|
|
|
|
return;
|
|
|
|
@@ -1221,9 +1302,6 @@ void NotificationStore::handleClosed(NotificationRow* row) {
|
|
|
|
|
row->m_group->sync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A closed row leaves the store immediately, so its group and the sidebar
|
|
|
|
|
// order update at once; a delegate that still holds it only delays the
|
|
|
|
|
// delete, never the removal.
|
|
|
|
|
removeRow(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1236,14 +1314,10 @@ void NotificationStore::removeRow(NotificationRow* row) {
|
|
|
|
|
removeGroupRow(row);
|
|
|
|
|
m_dirty.remove(row);
|
|
|
|
|
|
|
|
|
|
// The row is gone; drop any live-notification mapping to it so
|
|
|
|
|
// handleLiveUpdate can never resolve a freed pointer.
|
|
|
|
|
erase_if(m_liveRows, [row](std::pair<const QObject*, NotificationRow*> e) {
|
|
|
|
|
return e.second == row;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// A delegate animating this row out still holds it; unlock() deletes once
|
|
|
|
|
// the last lock is gone.
|
|
|
|
|
if (row->m_locks.isEmpty()) {
|
|
|
|
|
row->deleteLater();
|
|
|
|
|
} else {
|
|
|
|
@@ -1302,7 +1376,6 @@ void NotificationStore::addGroup(NotificationRow* row) {
|
|
|
|
|
m_groups.prepend(group);
|
|
|
|
|
emit groupsChanged();
|
|
|
|
|
} else {
|
|
|
|
|
// Most-recently-active group first, matching the sidebar order.
|
|
|
|
|
const qsizetype index = m_groups.indexOf(group);
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
m_groups.move(index, 0);
|
|
|
|
@@ -1333,8 +1406,6 @@ void NotificationStore::removeGroupRow(NotificationRow* row) {
|
|
|
|
|
emit groupsChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Popup groups mirror m_groups over the rows currently showing as popups, so
|
|
|
|
|
// the popup stack can render one card per app instead of one per notification.
|
|
|
|
|
void NotificationStore::addPopupGroup(NotificationRow* row) {
|
|
|
|
|
if (row->m_popupGroup != nullptr) {
|
|
|
|
|
return;
|
|
|
|
@@ -1356,9 +1427,19 @@ void NotificationStore::addPopupGroup(NotificationRow* row) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row->m_popupGroup = group;
|
|
|
|
|
|
|
|
|
|
touchExpiry(row);
|
|
|
|
|
|
|
|
|
|
group->addRow(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::expireGroup(NotificationGroup* group) {
|
|
|
|
|
const auto rows = group->m_notifs; // setPopup() mutates the group's list
|
|
|
|
|
for (auto* row : rows) {
|
|
|
|
|
row->setPopup(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::removePopupGroupRow(NotificationRow* row) {
|
|
|
|
|
auto* group = row->m_popupGroup;
|
|
|
|
|
if (group == nullptr) {
|
|
|
|
@@ -1378,6 +1459,32 @@ void NotificationStore::removePopupGroupRow(NotificationRow* row) {
|
|
|
|
|
emit popupGroupsChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int NotificationStore::expireTimeoutMs(const NotificationRow* row) const {
|
|
|
|
|
if (row->m_expireTimeoutMs == 0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row->m_expireTimeoutMs < 0) {
|
|
|
|
|
return m_popupTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return static_cast<int>((std::min)(row->m_expireTimeoutMs, MAX_EXPIRE_MS));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int NotificationStore::groupExpireTimeoutMs(
|
|
|
|
|
const NotificationGroup* group) const {
|
|
|
|
|
const auto* newest = group->m_notifs.value(0);
|
|
|
|
|
return newest != nullptr ? expireTimeoutMs(newest) : m_popupTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::touchExpiry(NotificationRow* row) {
|
|
|
|
|
if (!m_autoExpire || row->m_popupGroup == nullptr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row->m_popupGroup->resetExpiry(expireTimeoutMs(row));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotificationStore::updatePopupGroup(NotificationRow* row) {
|
|
|
|
|
if (row->m_popup && !row->m_closed) {
|
|
|
|
|
addPopupGroup(row);
|
|
|
|
@@ -1450,10 +1557,6 @@ bool NotificationStore::openDb() {
|
|
|
|
|
|
|
|
|
|
QSqlQuery query(db);
|
|
|
|
|
const QStringList statements = {
|
|
|
|
|
// WAL + NORMAL: commits don't fsync per insert (the fsync is what
|
|
|
|
|
// made every notification insert 5-8ms). Safe with WAL: an
|
|
|
|
|
// application crash still recovers the WAL; only an OS/power
|
|
|
|
|
// failure can lose the most recent commits.
|
|
|
|
|
"PRAGMA journal_mode=WAL",
|
|
|
|
|
"PRAGMA synchronous=NORMAL",
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS notifications("
|
|
|
|
@@ -1479,8 +1582,6 @@ bool NotificationStore::openDb() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Databases created before progress was stored lack the column; ALTER adds
|
|
|
|
|
// it instead of dropping history (caelestia drops and recreates).
|
|
|
|
|
if (!db.record("notifications").contains("progress")) {
|
|
|
|
|
QSqlQuery alter(db);
|
|
|
|
|
if (!alter.exec(
|
|
|
|
@@ -1574,10 +1675,6 @@ void NotificationStore::loadRows() {
|
|
|
|
|
row->m_urgency = query.value(7).toInt();
|
|
|
|
|
row->m_resident = query.value(8).toInt() != 0;
|
|
|
|
|
row->m_hasActionIcons = query.value(9).toInt() != 0;
|
|
|
|
|
// Popup state is deliberately not restored: showing stored
|
|
|
|
|
// notifications as toasts again after a shell restart would
|
|
|
|
|
// replay an old burst, and with autoExpire off the flag never
|
|
|
|
|
// clears itself. The rows stay in the sidebar instead.
|
|
|
|
|
row->m_popup = false;
|
|
|
|
|
row->m_imageSource = query.value(12).toString();
|
|
|
|
|
row->m_digest = query.value(13).toString();
|
|
|
|
@@ -1649,7 +1746,6 @@ void NotificationStore::migrateLegacyJson() {
|
|
|
|
|
row->m_urgency = object.value("urgency").toInt(1);
|
|
|
|
|
row->m_resident = object.value("resident").toBool();
|
|
|
|
|
row->m_hasActionIcons = object.value("hasActionIcons").toBool();
|
|
|
|
|
// Same reasoning as loadRows(): never re-toast migrated rows.
|
|
|
|
|
row->m_popup = false;
|
|
|
|
|
|
|
|
|
|
const auto time = object.value("time");
|
|
|
|
@@ -1708,8 +1804,6 @@ void NotificationStore::prune() {
|
|
|
|
|
<< query.lastError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Total cap, preferring higher urgency so a chatty app cannot push out
|
|
|
|
|
// an important notification.
|
|
|
|
|
if (!query.exec(
|
|
|
|
|
"DELETE FROM notifications WHERE id NOT IN"
|
|
|
|
|
" (SELECT id FROM notifications ORDER BY urgency DESC, time DESC"
|
|
|
|
@@ -1720,7 +1814,6 @@ void NotificationStore::prune() {
|
|
|
|
|
<< query.lastError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Per-app cap: one app flooding must not crowd out the other groups.
|
|
|
|
|
QStringList appNames;
|
|
|
|
|
if (query.exec("SELECT DISTINCT app_name FROM notifications")) {
|
|
|
|
|
while (query.next()) {
|
|
|
|
@@ -1753,8 +1846,6 @@ void NotificationStore::trimRows() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only closed, unlocked rows are droppable (open rows may still be
|
|
|
|
|
// acted on by the sender). Weakest first: lowest urgency, then oldest.
|
|
|
|
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
|
|
|
|
|
|
|
|
|
while (m_rows.size() > MAX_ROWS) {
|
|
|
|
@@ -1802,11 +1893,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|
|
|
|
|
|
|
|
|
const QUrl url(row->m_imageSource);
|
|
|
|
|
|
|
|
|
|
// Resolve *what* to read on the main thread (engine provider lookups and
|
|
|
|
|
// QIcon's theme state belong here), but do no file scanning and no
|
|
|
|
|
// decoding here: providers are designed to serve requestImage from non-GUI
|
|
|
|
|
// threads, and SVG rasterization was blocking the shell 3-9ms per
|
|
|
|
|
// notification.
|
|
|
|
|
QString localPath;
|
|
|
|
|
QQuickImageProvider* provider = nullptr;
|
|
|
|
|
QString providerId;
|
|
|
|
@@ -1825,20 +1911,9 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|
|
|
|
|
|
|
|
|
if (host != QLatin1String("qsimage") &&
|
|
|
|
|
host != QLatin1String("qspixmap")) {
|
|
|
|
|
// Provider urls such as image://icon/... are served by KF6's
|
|
|
|
|
// KIconLoader, which is not thread-safe (calling it from a
|
|
|
|
|
// worker crashed the shell). Only QIcon's theme state is read
|
|
|
|
|
// here, because the GUI thread owns it; the candidate scanning it
|
|
|
|
|
// feeds is pure filesystem work and runs in the worker below. A
|
|
|
|
|
// name the theme does not have means thousands of stat calls, and
|
|
|
|
|
// doing those here stalled inserts for milliseconds. If nothing
|
|
|
|
|
// matches, the row keeps rendering the url directly.
|
|
|
|
|
iconName = id;
|
|
|
|
|
theme = iconTheme();
|
|
|
|
|
} else {
|
|
|
|
|
// Transient inline notification images (image://qsimage/...).
|
|
|
|
|
// Provider objects are engine-owned and outlive this task;
|
|
|
|
|
// ids owned by a destroyed notification resolve to nothing.
|
|
|
|
|
auto* engine = qmlEngine(this);
|
|
|
|
|
if (engine == nullptr) {
|
|
|
|
|
return;
|
|
|
|
@@ -1864,10 +1939,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy the pixels here instead of in the worker: the handle
|
|
|
|
|
// belongs to the sender's notification, and it can be gone by the
|
|
|
|
|
// time the task runs, which silently dropped the picture. This is
|
|
|
|
|
// a memory copy of an already decoded image, not a rasterization.
|
|
|
|
|
inlineImage =
|
|
|
|
|
usePixmap
|
|
|
|
|
? provider->requestPixmap(providerId, nullptr, QSize())
|
|
|
|
@@ -1938,8 +2009,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|
|
|
|
|
|
|
|
|
normalized = normalized.convertToFormat(QImage::Format_RGBA8888);
|
|
|
|
|
|
|
|
|
|
// Cache the avatar content already cut to a circle, so the view
|
|
|
|
|
// does not have to mask it with a layer effect every frame.
|
|
|
|
|
const int side =
|
|
|
|
|
(std::min)(normalized.width(), normalized.height());
|
|
|
|
|
const QRect square(
|
|
|
|
@@ -2020,8 +2089,6 @@ void NotificationStore::storeImageBlob(
|
|
|
|
|
|
|
|
|
|
row->m_digest = digest;
|
|
|
|
|
row->m_image = QStringLiteral("image://notifimg/") + digest;
|
|
|
|
|
// The group reads its image through the source row and listens to
|
|
|
|
|
// imageChanged, so this emit is enough to keep the card in sync.
|
|
|
|
|
emit row->imageChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -2029,9 +2096,15 @@ void NotificationStore::storeImageBlob(
|
|
|
|
|
void NotificationStore::tick() {
|
|
|
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
|
|
|
|
|
|
for (auto* row : std::as_const(m_rows)) {
|
|
|
|
|
if (m_autoExpire && row->expired(now)) {
|
|
|
|
|
row->setPopup(false);
|
|
|
|
|
if (m_autoExpire) {
|
|
|
|
|
const auto groups = m_popupGroups;
|
|
|
|
|
|
|
|
|
|
for (auto* group : groups) {
|
|
|
|
|
if (group->expired(now)) {
|
|
|
|
|
expireGroup(group);
|
|
|
|
|
} else {
|
|
|
|
|
group->refreshExpiry(now);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|