1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

refactor(Notifications): further support for duplicate notification logic

- New setting to stack or suppress identical alerts (on by default)
Closes #2334
This commit is contained in:
purian23
2026-05-24 22:22:34 -04:00
parent 6093c37b41
commit d9525908f1
6 changed files with 122 additions and 38 deletions
+1
View File
@@ -688,6 +688,7 @@ Singleton {
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false property bool notificationCompactMode: false
property bool notificationDedupeEnabled: true
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400 property int notificationCustomAnimationDuration: 400
@@ -399,6 +399,7 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false }, notificationCompactMode: { def: false },
notificationDedupeEnabled: { def: true },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 }, notificationAnimationSpeed: { def: 1 },
notificationCustomAnimationDuration: { def: 400 }, notificationCustomAnimationDuration: { def: 400 },
@@ -182,26 +182,30 @@ Rectangle {
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing
StyledText { Item {
id: historyTitleText width: Math.max(0, parent.width - historySeparator.implicitWidth - Math.max(historyTimeText.implicitWidth, 72) - parent.spacing * 2)
width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) height: historyTitleText.implicitHeight
text: { visible: historyTitleText.text.length > 0
let title = historyItem.summary || "";
const appName = historyItem.appName || ""; StyledText {
const prefix = appName + " • "; id: historyTitleText
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { anchors.fill: parent
title = title.substring(prefix.length); text: {
let title = historyItem.summary || "";
const appName = historyItem.appName || "";
const prefix = appName + " • ";
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
title = title.substring(prefix.length);
}
return title;
} }
return title; color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
} }
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
} }
StyledText { StyledText {
id: historySeparator id: historySeparator
@@ -273,6 +273,17 @@ Item {
onToggled: checked => SettingsData.set("notificationCompactMode", checked) onToggled: checked => SettingsData.set("notificationCompactMode", checked)
} }
SettingsToggleRow {
settingKey: "notificationDedupeEnabled"
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
text: I18n.tr("Suppress Duplicate Notifications")
description: SettingsData.notificationDedupeEnabled
? I18n.tr("Identical alerts show as one popup instead of stacking")
: I18n.tr("Identical alerts stack as separate notification cards")
checked: SettingsData.notificationDedupeEnabled
onToggled: checked => SettingsData.set("notificationDedupeEnabled", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "notificationPopupShadowEnabled" settingKey: "notificationPopupShadowEnabled"
tags: ["notification", "popup", "shadow", "radius", "rounded"] tags: ["notification", "popup", "shadow", "radius", "rounded"]
+66 -21
View File
@@ -35,6 +35,8 @@ Singleton {
property int maxIngressPerSecond: 20 property int maxIngressPerSecond: 20
property double _lastIngressSec: 0 property double _lastIngressSec: 0
property int _ingressCountThisSec: 0 property int _ingressCountThisSec: 0
readonly property int notificationDedupBurstMs: 5000
property var _recentDedupKeys: []
property var _dismissQueue: [] property var _dismissQueue: []
property int _dismissBatchSize: 8 property int _dismissBatchSize: 8
@@ -291,18 +293,58 @@ Singleton {
return Date.now() / 1000.0; return Date.now() / 1000.0;
} }
function _normalizeDedupText(text) {
if (!text)
return "";
let normalized = text.toString();
normalized = normalized.replace(/<img\b[^>]*>/gi, "");
normalized = normalized.replace(/<[^>]+>/g, "");
normalized = normalized.replace(/\s+/g, " ").trim();
return normalized.toLowerCase();
}
function _dedupAppId(source) {
if (!source)
return "";
const desktopEntry = (source.desktopEntry || "").toString().trim().toLowerCase();
if (desktopEntry)
return desktopEntry;
return (source.appName || "").toString().trim().toLowerCase();
}
function _notificationDedupKey(source) { function _notificationDedupKey(source) {
if (!source) if (!source)
return ""; return "";
const app = (source.appName || source.desktopEntry || "").toString(); const app = _dedupAppId(source);
const summary = (source.summary || "").toString(); const summary = _normalizeDedupText(source.summary);
const body = (source.body || "").toString(); const body = _normalizeDedupText(source.body);
const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal; const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal;
const icon = (source.appIcon || "").toString();
if (!app && !summary && !body) if (!app && !summary && !body)
return ""; return "";
const sep = ""; const sep = "";
return app + sep + summary + sep + body + sep + urgency + sep + icon; return app + sep + summary + sep + body + sep + urgency;
}
function _pruneRecentDedupKeys() {
const cutoff = Date.now() - notificationDedupBurstMs;
_recentDedupKeys = _recentDedupKeys.filter(entry => entry && entry.atMs >= cutoff);
}
function _hasRecentDuplicate(key) {
if (!key)
return false;
_pruneRecentDedupKeys();
return _recentDedupKeys.some(entry => entry && entry.key === key);
}
function _recordDedupKey(key) {
if (!key)
return;
_pruneRecentDedupKeys();
_recentDedupKeys.push({
"key": key,
"atMs": Date.now()
});
} }
function _findActiveDuplicate(notif) { function _findActiveDuplicate(notif) {
@@ -310,17 +352,14 @@ Singleton {
if (!key) if (!key)
return null; return null;
for (const w of visibleNotifications) { for (const w of allWrappers) {
if (!w || !w.notification || !w.popup) if (!w || !w.notification || !w.popup)
continue; continue;
if (_notificationDedupKey(w.notification) === key) if (_notificationDedupKey(w.notification) !== key)
return w;
}
for (const w of notificationQueue) {
if (!w || !w.notification)
continue; continue;
if (_notificationDedupKey(w.notification) === key) if (visibleNotifications.indexOf(w) !== -1 || notificationQueue.indexOf(w) !== -1)
return w;
if (w.timer && w.timer.running)
return w; return w;
} }
@@ -637,14 +676,17 @@ Singleton {
return; return;
} }
const duplicate = _findActiveDuplicate(notif); if (SettingsData.notificationDedupeEnabled) {
if (duplicate) { const dedupKey = _notificationDedupKey(notif);
if (duplicate.timer && duplicate.timer.running) const duplicate = _findActiveDuplicate(notif);
duplicate.timer.restart(); if (duplicate || _hasRecentDuplicate(dedupKey)) {
try { if (duplicate && duplicate.timer && duplicate.timer.running)
notif.dismiss(); duplicate.timer.restart();
} catch (e) {} try {
return; notif.dismiss();
} catch (e) {}
return;
}
} }
if (!_ingressAllowed(policy.urgency)) { if (!_ingressAllowed(policy.urgency)) {
@@ -686,6 +728,9 @@ Singleton {
}); });
if (wrapper) { if (wrapper) {
if (SettingsData.notificationDedupeEnabled)
_recordDedupKey(_notificationDedupKey(notif));
root.allWrappers.push(wrapper); root.allWrappers.push(wrapper);
if (shouldKeepInCenter) { if (shouldKeepInCenter) {
root.notifications.push(wrapper); root.notifications.push(wrapper);
@@ -5807,6 +5807,28 @@
], ],
"description": "Use smaller notification cards" "description": "Use smaller notification cards"
}, },
{
"section": "notificationDedupeEnabled",
"label": "Suppress Duplicate Notifications",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"coalesce",
"dedupe",
"duplicate",
"duplicates",
"messages",
"notif",
"notification",
"notifications",
"repeat",
"stack",
"toast"
],
"description": "Control whether identical alerts stack or show as a single popup"
},
{ {
"section": "notificationHistorySaveCritical", "section": "notificationHistorySaveCritical",
"label": "Critical Priority", "label": "Critical Priority",