import QtQuick import Quickshell.Services.Notifications QtObject { id: root property ListModel notifications: ListModel {} property var groupedNotifications: [] property var groupExpansion: ({}) property int versionCounter: 0 function nextVersion() { versionCounter += 1; return versionCounter; } function indexOfId(notificationId) { for (let i = 0; i < notifications.count; i++) { const row = notifications.get(i); if (row.rowNotificationId === notificationId) return i; } return -1; } function normalizedString(value) { if (value === undefined || value === null) return ""; return String(value); } function hintString(notificationObject, name) { if (!notificationObject || !notificationObject.hints) return ""; const value = notificationObject.hints[name]; return value === undefined || value === null ? "" : String(value); } function groupKeyFor(notificationObject) { const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase(); if (desktopEntry.length > 0) return "desktop:" + desktopEntry; const appName = normalizedString(notificationObject ? notificationObject.appName : "").trim().toLowerCase(); if (appName.length > 0) return "app:" + appName; const appIcon = normalizedString(notificationObject ? notificationObject.appIcon : "").trim().toLowerCase(); if (appIcon.length > 0) return "icon:" + appIcon; const image = normalizedString(notificationObject ? notificationObject.image : "").trim().toLowerCase(); if (image.length > 0) return "image:" + image; return "id:" + String(notificationObject ? notificationObject.id : Date.now()); } function sortRowsNewestFirst(left, right) { return (right.createdAt || 0) - (left.createdAt || 0); } function groupSortNewestFirst(left, right) { return (right.newestAt || 0) - (left.newestAt || 0); } function rebuildGroups() { const grouped = ({}); for (let i = 0; i < notifications.count; i++) { const row = notifications.get(i); const key = row.rowGroupKey; if (!grouped[key]) { grouped[key] = { groupKey: key, appName: row.rowAppName, appIcon: row.rowAppIcon, desktopEntry: row.rowDesktopEntry, newestAt: row.rowCreatedAt, notifications: [] }; } grouped[key].notifications.push({ notificationId: row.rowNotificationId, notifObject: row.rowObject, notifVersion: row.rowVersion, createdAt: row.rowCreatedAt, appName: row.rowAppName, appIcon: row.rowAppIcon, image: row.rowImage }); if (row.rowCreatedAt > grouped[key].newestAt) grouped[key].newestAt = row.rowCreatedAt; } const groups = []; for (const groupKey in grouped) { if (!grouped.hasOwnProperty(groupKey)) continue; const group = grouped[groupKey]; group.notifications.sort(sortRowsNewestFirst); groups.push(group); } groups.sort(groupSortNewestFirst); groupedNotifications = groups; } function groupIsExpanded(groupKey) { return Boolean(groupExpansion[groupKey]); } function setGroupExpanded(groupKey, expanded) { const next = Object.assign({}, groupExpansion); next[groupKey] = Boolean(expanded); groupExpansion = next; } function toggleGroupExpanded(groupKey) { setGroupExpanded(groupKey, !groupIsExpanded(groupKey)); } function addNotification(notificationObject) { if (notificationObject.lastGeneration) { notificationObject.dismiss(); return; } notificationObject.tracked = true; const idCopy = notificationObject.id; const objectCopy = notificationObject; notificationObject.closed.connect(function () { const rowIndex = indexOfId(idCopy); if (rowIndex >= 0 && notifications.get(rowIndex).rowObject === objectCopy) { notifications.remove(rowIndex); rebuildGroups(); } }); const nowMs = Date.now(); const groupKey = groupKeyFor(notificationObject); const existingIndex = indexOfId(notificationObject.id); if (existingIndex >= 0) { notifications.set(existingIndex, { rowNotificationId: notificationObject.id, rowObject: notificationObject, rowVersion: nextVersion(), rowCreatedAt: nowMs, rowGroupKey: groupKey, rowAppName: normalizedString(notificationObject.appName), rowAppIcon: normalizedString(notificationObject.appIcon), rowImage: normalizedString(notificationObject.image), rowDesktopEntry: hintString(notificationObject, "desktop-entry") }); rebuildGroups(); return; } notifications.append({ rowNotificationId: notificationObject.id, rowObject: notificationObject, rowVersion: nextVersion(), rowCreatedAt: nowMs, rowGroupKey: groupKey, rowAppName: normalizedString(notificationObject.appName), rowAppIcon: normalizedString(notificationObject.appIcon), rowImage: normalizedString(notificationObject.image), rowDesktopEntry: hintString(notificationObject, "desktop-entry") }); rebuildGroups(); } function closeById(notificationId, reason) { const rowIndex = indexOfId(notificationId); if (rowIndex < 0) return; const row = notifications.get(rowIndex); const notifObject = row.rowObject; if (notifObject) { if (reason === "timeout") { notifObject.expire(); } else if (reason === "click" || reason === "dismiss" || reason === "drag") { notifObject.dismiss(); } } notifications.remove(rowIndex); rebuildGroups(); } property NotificationServer server: NotificationServer { keepOnReload: false bodySupported: true bodyMarkupSupported: false bodyHyperlinksSupported: false actionsSupported: true // NotificationServer is Quickshell's DBus implementation for // org.freedesktop.Notifications, so this is the notification source. onNotification: function (notificationObject) { root.addNotification(notificationObject); } } }