1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05:38 -05:00

Initial implementation of grouped notifications

This commit is contained in:
purian23
2025-07-14 10:50:48 -04:00
parent 68bcc2b049
commit cf5c26522b
4 changed files with 654 additions and 216 deletions

View File

@@ -0,0 +1,270 @@
import QtQuick
import Quickshell
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
// Grouped notifications model
property var groupedNotifications: ListModel {}
// Total count of all notifications across all groups
property int totalCount: 0
// Map to track group indices by app name for efficient lookups
property var appGroupMap: ({})
// Configuration
property int maxNotificationsPerGroup: 10
property int maxGroups: 20
Component.onCompleted: {
groupedNotifications = Qt.createQmlObject(`
import QtQuick
ListModel {}
`, root)
}
// Add a new notification to the appropriate group
function addNotification(notificationObj) {
if (!notificationObj || !notificationObj.appName) {
console.warn("Invalid notification object:", notificationObj)
return
}
const appName = notificationObj.appName
let groupIndex = appGroupMap[appName]
if (groupIndex === undefined) {
// Create new group
groupIndex = createNewGroup(appName, notificationObj)
} else {
// Add to existing group
addToExistingGroup(groupIndex, notificationObj)
}
updateTotalCount()
}
// Create a new notification group
function createNewGroup(appName, notificationObj) {
// Check if we need to remove oldest group
if (groupedNotifications.count >= maxGroups) {
removeOldestGroup()
}
const groupIndex = groupedNotifications.count
const notificationsList = Qt.createQmlObject(`
import QtQuick
ListModel {}
`, root)
notificationsList.append(notificationObj)
groupedNotifications.append({
"appName": appName,
"appIcon": notificationObj.appIcon || "",
"notifications": notificationsList,
"totalCount": 1,
"latestNotification": notificationObj,
"expanded": false,
"timestamp": notificationObj.timestamp
})
appGroupMap[appName] = groupIndex
updateGroupMap()
return groupIndex
}
// Add notification to existing group
function addToExistingGroup(groupIndex, notificationObj) {
if (groupIndex >= groupedNotifications.count) {
console.warn("Invalid group index:", groupIndex)
return
}
const group = groupedNotifications.get(groupIndex)
if (!group) return
// Add to front of group (newest first)
group.notifications.insert(0, notificationObj)
// Update group metadata
groupedNotifications.setProperty(groupIndex, "totalCount", group.totalCount + 1)
groupedNotifications.setProperty(groupIndex, "latestNotification", notificationObj)
groupedNotifications.setProperty(groupIndex, "timestamp", notificationObj.timestamp)
// Keep only max notifications per group
while (group.notifications.count > maxNotificationsPerGroup) {
group.notifications.remove(group.notifications.count - 1)
}
// Move group to front (most recent activity)
moveGroupToFront(groupIndex)
}
// Move a group to the front of the list
function moveGroupToFront(groupIndex) {
if (groupIndex === 0) return // Already at front
const group = groupedNotifications.get(groupIndex)
if (!group) return
// Remove from current position
groupedNotifications.remove(groupIndex)
// Insert at front
groupedNotifications.insert(0, group)
// Update group map
updateGroupMap()
}
// Remove the oldest group (least recent activity)
function removeOldestGroup() {
if (groupedNotifications.count === 0) return
const lastIndex = groupedNotifications.count - 1
const group = groupedNotifications.get(lastIndex)
if (group) {
delete appGroupMap[group.appName]
groupedNotifications.remove(lastIndex)
updateGroupMap()
}
}
// Update the app group map after structural changes
function updateGroupMap() {
appGroupMap = {}
for (let i = 0; i < groupedNotifications.count; i++) {
const group = groupedNotifications.get(i)
if (group) {
appGroupMap[group.appName] = i
}
}
}
// Toggle group expansion state
function toggleGroupExpansion(groupIndex) {
if (groupIndex >= groupedNotifications.count) return
const group = groupedNotifications.get(groupIndex)
if (group) {
groupedNotifications.setProperty(groupIndex, "expanded", !group.expanded)
}
}
// Remove a specific notification from a group
function removeNotification(groupIndex, notificationIndex) {
if (groupIndex >= groupedNotifications.count) return
const group = groupedNotifications.get(groupIndex)
if (!group || notificationIndex >= group.notifications.count) return
group.notifications.remove(notificationIndex)
// Update group count
const newCount = group.totalCount - 1
groupedNotifications.setProperty(groupIndex, "totalCount", newCount)
// If group is empty, remove it
if (newCount === 0) {
removeGroup(groupIndex)
} else {
// Update latest notification if we removed the latest one
if (notificationIndex === 0 && group.notifications.count > 0) {
const newLatest = group.notifications.get(0)
groupedNotifications.setProperty(groupIndex, "latestNotification", newLatest)
}
}
updateTotalCount()
}
// Remove an entire group
function removeGroup(groupIndex) {
if (groupIndex >= groupedNotifications.count) return
const group = groupedNotifications.get(groupIndex)
if (group) {
delete appGroupMap[group.appName]
groupedNotifications.remove(groupIndex)
updateGroupMap()
updateTotalCount()
}
}
// Clear all notifications
function clearAllNotifications() {
groupedNotifications.clear()
appGroupMap = {}
totalCount = 0
}
// Update total count across all groups
function updateTotalCount() {
let count = 0
for (let i = 0; i < groupedNotifications.count; i++) {
const group = groupedNotifications.get(i)
if (group) {
count += group.totalCount
}
}
totalCount = count
}
// Get notification by ID across all groups
function getNotificationById(notificationId) {
for (let i = 0; i < groupedNotifications.count; i++) {
const group = groupedNotifications.get(i)
if (!group) continue
for (let j = 0; j < group.notifications.count; j++) {
const notification = group.notifications.get(j)
if (notification && notification.id === notificationId) {
return {
groupIndex: i,
notificationIndex: j,
notification: notification
}
}
}
}
return null
}
// Get group by app name
function getGroupByAppName(appName) {
const groupIndex = appGroupMap[appName]
if (groupIndex !== undefined) {
return {
groupIndex: groupIndex,
group: groupedNotifications.get(groupIndex)
}
}
return null
}
// Get visible notifications for a group (considering expansion state)
function getVisibleNotifications(groupIndex, maxVisible = 3) {
if (groupIndex >= groupedNotifications.count) return []
const group = groupedNotifications.get(groupIndex)
if (!group) return []
if (group.expanded) {
// Show all notifications when expanded
return group.notifications
} else {
// Show only the latest notification(s) when collapsed
const visibleCount = Math.min(maxVisible, group.notifications.count)
const visible = []
for (let i = 0; i < visibleCount; i++) {
visible.push(group.notifications.get(i))
}
return visible
}
}
}

View File

@@ -14,4 +14,5 @@ singleton LauncherService 1.0 LauncherService.qml
singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml
singleton CalendarService 1.0 CalendarService.qml singleton CalendarService 1.0 CalendarService.qml
singleton UserInfoService 1.0 UserInfoService.qml singleton UserInfoService 1.0 UserInfoService.qml
singleton FocusedWindowService 1.0 FocusedWindowService.qml singleton FocusedWindowService 1.0 FocusedWindowService.qml
singleton NotificationGroupingService 1.0 NotificationGroupingService.qml

View File

@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import "../Common" import "../Common"
import "../Services"
PanelWindow { PanelWindow {
id: notificationHistoryPopup id: notificationHistoryPopup
@@ -89,7 +90,7 @@ PanelWindow {
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: notificationHistory.count > 0 visible: NotificationGroupingService.totalCount > 0
color: clearArea.containsMouse ? color: clearArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
@@ -128,6 +129,7 @@ PanelWindow {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
NotificationGroupingService.clearAllNotifications()
notificationHistory.clear() notificationHistory.clear()
} }
} }
@@ -149,48 +151,193 @@ PanelWindow {
} }
} }
// Notification List // Grouped Notification List
ScrollView { ScrollView {
width: parent.width width: parent.width
height: parent.height - 120 height: parent.height - 120
clip: true clip: true
ListView { ListView {
id: notificationListView id: groupedNotificationListView
model: notificationHistory model: NotificationGroupingService.groupedNotifications
spacing: Theme.spacingS spacing: Theme.spacingM
delegate: Rectangle { delegate: Column {
width: notificationListView.width width: groupedNotificationListView.width
height: 80 spacing: Theme.spacingXS
radius: Theme.cornerRadius
color: notifArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
// Close button for individual notification property var groupData: model
property bool isExpanded: model.expanded || false
// Group Header
Rectangle { Rectangle {
width: 24 width: parent.width
height: 24 height: 56
radius: 12 radius: Theme.cornerRadius
anchors.right: parent.right color: groupHeaderArea.containsMouse ?
anchors.top: parent.top Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
anchors.margins: 8 Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
color: closeNotifArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text { Row {
anchors.centerIn: parent anchors.fill: parent
text: "close" anchors.margins: Theme.spacingM
font.family: Theme.iconFont spacing: Theme.spacingM
font.pixelSize: 14
color: closeNotifArea.containsMouse ? Theme.primary : Theme.surfaceText // App Icon
Rectangle {
width: 32
height: 32
radius: width / 2
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
// Material icon fallback
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "apps"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
}
// App icon
Loader {
active: model.appIcon && model.appIcon !== ""
anchors.centerIn: parent
sourceComponent: IconImage {
width: 24
height: 24
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
}
// App Name and Summary
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
spacing: 2
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: model.appName || "App"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
// Notification count badge
Rectangle {
width: Math.max(countText.width + 8, 20)
height: 20
radius: 10
color: Theme.primary
visible: model.totalCount > 1
anchors.verticalCenter: parent.verticalCenter
Text {
id: countText
anchors.centerIn: parent
text: model.totalCount.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.primaryText
font.weight: Font.Medium
}
}
}
Text {
text: model.latestNotification ?
(model.latestNotification.summary || model.latestNotification.body || "") : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
// Expand/Collapse Icon
Rectangle {
width: 32
height: 32
radius: 16
anchors.verticalCenter: parent.verticalCenter
color: groupHeaderArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
Text {
anchors.centerIn: parent
text: isExpanded ? "expand_less" : "expand_more"
font.family: Theme.iconFont
font.pixelSize: 20
color: groupHeaderArea.containsMouse ? Theme.primary : Theme.surfaceText
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Close group button
Rectangle {
width: 24
height: 24
radius: 12
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 6
color: closeGroupArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
Text {
anchors.centerIn: parent
text: "close"
font.family: Theme.iconFont
font.pixelSize: 14
color: closeGroupArea.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: closeGroupArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationGroupingService.removeGroup(index)
}
}
} }
MouseArea { MouseArea {
id: closeNotifArea id: groupHeaderArea
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: 32
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
notificationHistory.remove(index) NotificationGroupingService.toggleGroupExpansion(index)
} }
} }
@@ -202,202 +349,219 @@ PanelWindow {
} }
} }
Row { // Expanded Notifications List
anchors.fill: parent Loader {
anchors.margins: Theme.spacingM width: parent.width
anchors.rightMargin: 36 // Don't overlap with close button active: isExpanded
spacing: Theme.spacingM
// Notification icon based on EXAMPLE NotificationAppIcon pattern sourceComponent: Column {
Rectangle { width: parent.width
width: 48
height: 48
radius: width / 2 // Fully rounded like EXAMPLE
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
// Material icon fallback (when no app icon)
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.primaryText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon (when no notification image)
Loader {
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
anchors.centerIn: parent
sourceComponent: IconImage {
width: 32
height: 32
asynchronous: true
source: {
if (!model.appIcon) return ""
// Handle file:// URLs directly
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
// Otherwise treat as icon name
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
// Notification image (like Discord user avatar) - PRIORITY
Loader {
active: model.image && model.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: historyNotifImage
anchors.fill: parent
source: model.image || ""
fillMode: Image.PreserveAspectCrop
cache: true // Enable caching for history
antialiasing: true
asynchronous: true
smooth: true
// Use the parent size for optimization
sourceSize.width: parent.width
sourceSize.height: parent.height
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: Rectangle {
width: 48
height: 48
radius: 24 // Fully rounded
}
}
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load notification history image:", source)
} else if (status === Image.Ready) {
console.log("Notification history image loaded:", source, "size:", sourceSize.width + "x" + sourceSize.height)
}
}
}
// Fallback to app icon when primary image fails
Loader {
active: model.appIcon && model.appIcon !== "" && historyNotifImage.status === Image.Error
anchors.centerIn: parent
sourceComponent: IconImage {
width: 32
height: 32
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: model.appIcon && model.appIcon !== "" && historyNotifImage.status === Image.Ready
anchors.bottom: parent.bottom
anchors.right: parent.right
sourceComponent: IconImage {
width: 16
height: 16
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
}
}
}
// Content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Repeater {
text: model.appName || "App" model: groupData.notifications
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary delegate: Rectangle {
font.weight: Font.Medium width: parent.width
height: 80
radius: Theme.cornerRadius
color: notifArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
// Individual notification close button
Rectangle {
width: 24
height: 24
radius: 12
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
color: closeNotifArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
Text {
anchors.centerIn: parent
text: "close"
font.family: Theme.iconFont
font.pixelSize: 14
color: closeNotifArea.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: closeNotifArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Use the parent ListView's index to get the group index
let groupIndex = parent.parent.parent.parent.parent.index
NotificationGroupingService.removeNotification(groupIndex, model.index)
}
}
}
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: 36
spacing: Theme.spacingM
// Notification icon
Rectangle {
width: 48
height: 48
radius: width / 2
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
// Material icon fallback
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.primaryText
}
}
// App icon (when no notification image)
Loader {
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
anchors.centerIn: parent
sourceComponent: IconImage {
width: 32
height: 32
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
// Notification image (priority)
Loader {
active: model.image && model.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: notifImage
anchors.fill: parent
source: model.image || ""
fillMode: Image.PreserveAspectCrop
cache: true
antialiasing: true
asynchronous: true
smooth: true
sourceSize.width: parent.width
sourceSize.height: parent.height
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: Rectangle {
width: 48
height: 48
radius: 24
}
}
}
// Small app icon overlay
Loader {
active: model.appIcon && model.appIcon !== "" && notifImage.status === Image.Ready
anchors.bottom: parent.bottom
anchors.right: parent.right
sourceComponent: IconImage {
width: 16
height: 16
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
}
}
}
// Notification content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
spacing: Theme.spacingXS
Text {
text: model.summary || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: model.body || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
MouseArea {
id: notifArea
anchors.fill: parent
anchors.rightMargin: 32
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (model && root.handleNotificationClick) {
root.handleNotificationClick(model)
}
// Use the parent ListView's index to get the group index
let groupIndex = parent.parent.parent.parent.parent.index
NotificationGroupingService.removeNotification(groupIndex, model.index)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
Text {
text: model.summary || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: model.body || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
MouseArea {
id: notifArea
anchors.fill: parent
anchors.rightMargin: 32 // Don't overlap with close button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Try to handle notification click if it has actions
if (model && root.handleNotificationClick) {
root.handleNotificationClick(model)
}
// Remove from history after handling
notificationHistory.remove(index)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
} }
} }
} }
} }
// Empty state - properly centered // Empty state
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: notificationHistory.count === 0 visible: NotificationGroupingService.totalCount === 0
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -424,7 +588,7 @@ PanelWindow {
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: "Notifications will appear here" text: "Notifications will appear here grouped by app"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter

View File

@@ -245,10 +245,13 @@ ShellRoot {
"notification": notification // Keep reference for action handling "notification": notification // Keep reference for action handling
} }
// Add to history (prepend to show newest first) // Add to grouped notifications
NotificationGroupingService.addNotification(notifObj)
// Also add to legacy flat history for backwards compatibility
notificationHistory.insert(0, notifObj) notificationHistory.insert(0, notifObj)
// Keep only last 50 notifications // Keep only last 50 notifications in flat history
while (notificationHistory.count > 50) { while (notificationHistory.count > 50) {
notificationHistory.remove(notificationHistory.count - 1) notificationHistory.remove(notificationHistory.count - 1)
} }
@@ -303,7 +306,7 @@ ShellRoot {
bluetoothAvailable: root.bluetoothAvailable bluetoothAvailable: root.bluetoothAvailable
bluetoothEnabled: root.bluetoothEnabled bluetoothEnabled: root.bluetoothEnabled
shellRoot: root shellRoot: root
notificationCount: notificationHistory.count notificationCount: NotificationGroupingService.totalCount
processDropdown: processListDropdown processDropdown: processListDropdown
// Connect tray menu properties // Connect tray menu properties