diff --git a/Modules/Notifications/NotificationCard.qml b/Modules/Notifications/NotificationCard.qml
index 321a8caf..33eebc07 100644
--- a/Modules/Notifications/NotificationCard.qml
+++ b/Modules/Notifications/NotificationCard.qml
@@ -11,48 +11,62 @@ Rectangle {
property var notificationGroup
property bool expanded: NotificationService.expandedGroups[notificationGroup?.key] || false
+ property bool descriptionExpanded: false
width: parent.width
height: {
- if (expanded && notificationGroup && notificationGroup.count >= 1) {
- const baseHeight = (116 * notificationGroup.count) + (12 * (notificationGroup.count - 1));
- const bottomMargin = notificationGroup.count === 1 ? 65 : (notificationGroup.count <= 3 ? 30 : -30);
- return baseHeight + bottomMargin;
+ if (expanded) {
+ return expandedContent.height + 28;
}
- return 116;
+ const baseHeight = 116;
+ if (descriptionExpanded && descriptionText.hasMoreText) {
+ const twoLineHeight = descriptionText.font.pixelSize * 1.2 * 2;
+ const extraHeight = descriptionText.contentHeight - twoLineHeight;
+ return baseHeight + extraHeight;
+ }
+ return baseHeight;
}
radius: Theme.cornerRadiusLarge
- color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
+ color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: notificationGroup?.latestNotification?.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: notificationGroup?.latestNotification?.urgency === 2 ? 2 : 1
clip: true
Rectangle {
- width: 4
- height: parent.height - 16
- anchors.left: parent.left
- anchors.leftMargin: 2
- anchors.verticalCenter: parent.verticalCenter
- radius: 2
- color: Theme.primary
+ anchors.fill: parent
+ radius: parent.radius
visible: notificationGroup?.latestNotification?.urgency === 2
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+ GradientStop {
+ position: 0.0
+ color: Theme.primary
+ }
+ GradientStop {
+ position: 0.02
+ color: Theme.primary
+ }
+ GradientStop {
+ position: 0.021
+ color: "transparent"
+ }
+ }
+ opacity: 1.0
}
Item {
id: collapsedContent
-
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
- anchors.rightMargin: 16
+ anchors.rightMargin: 56
height: 92
visible: !expanded
Rectangle {
id: iconContainer
-
readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== ""
width: 55
@@ -70,12 +84,10 @@ Rectangle {
source: {
if (parent.hasNotificationImage)
return notificationGroup.latestNotification.cleanImage;
-
if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon;
-
return Quickshell.iconPath(appIcon, "");
}
return "";
@@ -121,7 +133,7 @@ Rectangle {
anchors.left: iconContainer.right
anchors.leftMargin: 12
- anchors.right: controlsContainer.left
+ anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
@@ -132,7 +144,7 @@ Rectangle {
width: parent.width
height: parent.height
anchors.top: parent.top
- anchors.topMargin: 2
+ anchors.topMargin: -4
Column {
width: parent.width
@@ -166,95 +178,42 @@ Rectangle {
}
Text {
- text: {
- let bodyText = notificationGroup?.latestNotification?.body || "";
- const urlRegex = /(https?:\/\/[^\s]+)/g;
- return bodyText.replace(urlRegex, '$1');
- }
+ id: descriptionText
+ property string fullText: notificationGroup?.latestNotification?.body || ""
+ property bool hasMoreText: false
+
+ text: fullText
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
- maximumLineCount: (notificationGroup?.count || 0) > 1 ? 2 : 3
+ maximumLineCount: descriptionExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
- textFormat: Text.RichText
- onLinkActivated: function(link) {
- Qt.openUrlExternally(link);
+ textFormat: Text.PlainText
+
+ onContentHeightChanged: {
+ const singleLineHeight = font.pixelSize * 1.2;
+ const twoLineHeight = singleLineHeight * 2;
+ hasMoreText = descriptionText.contentHeight > twoLineHeight;
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: parent.hasMoreText ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: parent.hasMoreText
+ onClicked: {
+ descriptionExpanded = !descriptionExpanded;
+ }
}
}
}
}
}
-
- Item {
- id: controlsContainer
-
- anchors.right: parent.right
- anchors.rightMargin: 0
- anchors.top: parent.top
- anchors.topMargin: 0
- width: (notificationGroup?.count || 0) > 1 ? 40 : 20
- height: 24
-
- Rectangle {
- anchors.left: parent.left
- anchors.top: parent.top
- width: 20
- height: 20
- radius: 10
- color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
- visible: (notificationGroup?.count || 0) > 1
-
- DankIcon {
- anchors.centerIn: parent
- name: expanded ? "expand_less" : "expand_more"
- size: 14
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: expandArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
- }
- }
-
- Rectangle {
- property bool isHovered: false
-
- anchors.right: parent.right
- anchors.top: parent.top
- width: 20
- height: 20
- radius: 10
- color: isHovered ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- name: "close"
- size: 14
- color: parent.isHovered ? Theme.primary : Theme.surfaceText
- anchors.centerIn: parent
- }
-
- MouseArea {
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onEntered: parent.isHovered = true
- onExited: parent.isHovered = false
- onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
- }
- }
- }
}
Column {
id: expandedContent
-
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -271,6 +230,8 @@ Rectangle {
Row {
anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.rightMargin: 56
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
@@ -280,6 +241,8 @@ Rectangle {
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ maximumLineCount: 1
}
Rectangle {
@@ -300,61 +263,6 @@ Rectangle {
}
}
- Item {
- width: 48
- height: 24
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.topMargin: 2
-
- Rectangle {
- width: 20
- height: 20
- radius: 10
- anchors.left: parent.left
- color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- anchors.centerIn: parent
- name: "expand_less"
- size: 14
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: collapseArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
- }
- }
-
- Rectangle {
- width: 20
- height: 20
- radius: 10
- anchors.right: parent.right
- color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- anchors.centerIn: parent
- name: "close"
- size: 14
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: dismissAllArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
- }
- }
- }
}
Column {
@@ -369,15 +277,33 @@ Rectangle {
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false
width: parent.width
- height: messageExpanded ? Math.min(120, 50 + (bodyText.contentHeight || 0)) : 80
+ height: {
+ const baseHeight = 120;
+ if (messageExpanded) {
+ const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
+ if (bodyText.implicitHeight > twoLineHeight + 2) {
+ const extraHeight = bodyText.implicitHeight - twoLineHeight;
+ return baseHeight + extraHeight;
+ }
+ }
+ return baseHeight;
+ }
radius: Theme.cornerRadiusLarge
- color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
+ color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
Item {
anchors.fill: parent
anchors.margins: 12
+ anchors.bottomMargin: 18
Rectangle {
id: messageIcon
@@ -388,7 +314,7 @@ Rectangle {
height: 32
radius: 16
anchors.left: parent.left
- anchors.top: parent.top
+ anchors.verticalCenter: parent.verticalCenter
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
@@ -428,26 +354,21 @@ Rectangle {
Column {
anchors.left: messageIcon.right
anchors.leftMargin: 12
- anchors.right: messageControls.left
- anchors.rightMargin: 0
+ anchors.right: parent.right
+ anchors.rightMargin: 12
anchors.top: parent.top
- spacing: 4
+ anchors.topMargin: -2
+ spacing: 2
Text {
width: parent.width
- text: {
- const appName = modelData?.appName || "";
- const timeStr = modelData?.timeStr || "";
- if (timeStr.length > 0)
- return appName + " • " + timeStr;
- else
- return appName;
- }
+ text: modelData?.timeStr || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
+ visible: text.length > 0
}
Text {
@@ -463,85 +384,94 @@ Rectangle {
Text {
id: bodyText
-
- width: parent.width
- text: {
- let bodyText = modelData?.body || "";
- if (messageExpanded)
- bodyText = bodyText.length > 500 ? bodyText.substring(0, 497) + "..." : bodyText;
- else
- bodyText = bodyText.length > 80 ? bodyText.substring(0, 77) + "..." : bodyText;
- const urlRegex = /(https?:\/\/[^\s]+)/g;
- return bodyText.replace(urlRegex, '$1');
- }
+
+ text: modelData?.body || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
+ width: parent.width
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
maximumLineCount: messageExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
- textFormat: Text.RichText
- onLinkActivated: function(link) {
- Qt.openUrlExternally(link);
- }
- }
- }
-
- Row {
- id: messageControls
-
- anchors.right: parent.right
- anchors.rightMargin: -6
- anchors.top: parent.top
- spacing: 4
-
- Rectangle {
- width: 20
- height: 20
- radius: 10
- color: expandMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
- visible: (modelData?.body || "").length > 80
-
- DankIcon {
- anchors.centerIn: parent
- name: {
- const messageExpanded = NotificationService.expandedMessages[modelData?.notification?.id] || false;
- return messageExpanded ? "expand_less" : "expand_more";
+ textFormat: Text.PlainText
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: parent.truncated ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: parent.truncated || messageExpanded
+ onClicked: {
+ NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
}
- size: 12
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: expandMessageArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleMessageExpansion(modelData?.notification?.id || "")
}
}
- Rectangle {
- width: 20
- height: 20
- radius: 10
- color: closeMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
+ Row {
+ anchors.right: parent.right
+ spacing: 8
+ anchors.topMargin: 4
+ anchors.bottomMargin: 6
- DankIcon {
- anchors.centerIn: parent
- name: "close"
- size: 12
- color: closeMessageArea.containsMouse ? Theme.primary : Theme.surfaceText
+ Repeater {
+ model: modelData?.actions || []
+
+ Rectangle {
+ property bool isHovered: false
+
+ width: Math.max(actionText.implicitWidth + 12, 50)
+ height: 24
+ radius: 4
+ color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
+
+ Text {
+ id: actionText
+ text: modelData.text || ""
+ color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: parent.isHovered = true
+ onExited: parent.isHovered = false
+ onClicked: {
+ if (modelData && modelData.invoke) {
+ modelData.invoke();
+ }
+ }
+ }
+ }
}
- MouseArea {
- id: closeMessageArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.dismissNotification(modelData)
+ Rectangle {
+ property bool isHovered: false
+
+ width: Math.max(dismissText.implicitWidth + 12, 50)
+ height: 24
+ radius: 4
+ color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
+
+ Text {
+ id: dismissText
+ text: "Dismiss"
+ color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: parent.isHovered = true
+ onExited: parent.isHovered = false
+ onClicked: NotificationService.dismissNotification(modelData)
+ }
}
}
}
@@ -551,49 +481,46 @@ Rectangle {
}
}
- Rectangle {
- property bool isHovered: false
-
+ Row {
+ visible: !expanded
anchors.right: dismissButton.left
- anchors.rightMargin: 4
+ anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
- width: viewText.width + 16
- height: viewText.height + 8
- radius: 6
- color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
-
- Text {
- id: viewText
-
- text: "View"
- color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Font.Medium
- anchors.centerIn: parent
- }
-
- MouseArea {
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onEntered: parent.isHovered = true
- onExited: parent.isHovered = false
- onClicked: {
- if (notificationGroup?.latestNotification?.actions) {
- for (const action of notificationGroup.latestNotification.actions) {
- if (action.text && action.text.toLowerCase() === "view") {
- if (action.invoke) {
- action.invoke();
- return;
- }
+ spacing: 8
+
+ Repeater {
+ model: notificationGroup?.latestNotification?.actions || []
+
+ Rectangle {
+ property bool isHovered: false
+
+ width: Math.max(actionText.implicitWidth + 12, 50)
+ height: 24
+ radius: 4
+ color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
+
+ Text {
+ id: actionText
+ text: modelData.text || ""
+ color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: parent.isHovered = true
+ onExited: parent.isHovered = false
+ onClicked: {
+ if (modelData && modelData.invoke) {
+ modelData.invoke();
}
}
- if (notificationGroup.latestNotification.actions.length > 0) {
- const firstAction = notificationGroup.latestNotification.actions[0];
- if (firstAction.invoke)
- firstAction.invoke();
- }
}
}
}
@@ -604,6 +531,7 @@ Rectangle {
property bool isHovered: false
+ visible: !expanded
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
@@ -640,16 +568,39 @@ Rectangle {
z: -1
}
- Behavior on height {
- SequentialAnimation {
- PauseAnimation {
- duration: 25
- }
+ Item {
+ id: fixedControls
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: 12
+ anchors.rightMargin: 16
+ width: 40
+ height: 24
- NumberAnimation {
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
+ DankActionButton {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ visible: (notificationGroup?.count || 0) > 1
+ iconName: expanded ? "expand_less" : "expand_more"
+ iconSize: 14
+ buttonSize: 20
+ onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
+ }
+
+ DankActionButton {
+ anchors.right: parent.right
+ anchors.top: parent.top
+ iconName: "close"
+ iconSize: 14
+ buttonSize: 20
+ onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
+ }
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
}
}
}
\ No newline at end of file
diff --git a/Modules/Notifications/NotificationCenterPopout.qml b/Modules/Notifications/NotificationCenterPopout.qml
index f9aa155a..b740a3af 100644
--- a/Modules/Notifications/NotificationCenterPopout.qml
+++ b/Modules/Notifications/NotificationCenterPopout.qml
@@ -14,8 +14,11 @@ PanelWindow {
property bool notificationHistoryVisible: false
visible: notificationHistoryVisible
+ onNotificationHistoryVisibleChanged: {
+ NotificationService.disablePopups(notificationHistoryVisible);
+ }
implicitWidth: 400
- implicitHeight: Math.min(Screen.height * 0.6, Math.max(580, 720))
+ implicitHeight: Math.min(Screen.height * 0.8, 400)
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -36,8 +39,22 @@ PanelWindow {
}
Rectangle {
+ id: mainRect
+
+ function calculateHeight() {
+ let baseHeight = Theme.spacingL * 2;
+ baseHeight += notificationHeader.height;
+ baseHeight += Theme.spacingM;
+ let listHeight = notificationList.listContentHeight;
+ if (NotificationService.groupedNotifications.length === 0)
+ listHeight = 200;
+
+ baseHeight += Math.min(listHeight, 600);
+ return Math.max(300, baseHeight);
+ }
+
width: 400
- height: Math.min(Screen.height * 0.6, Math.max(580, 720))
+ height: calculateHeight()
x: Screen.width - width - Theme.spacingL
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
@@ -45,103 +62,7 @@ PanelWindow {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: notificationHistoryVisible ? 1 : 0
-
- Rectangle {
- anchors.fill: parent
- anchors.margins: -3
- color: "transparent"
- radius: parent.radius + 3
- border.color: Qt.rgba(0, 0, 0, 0.05)
- border.width: 1
- z: -3
- }
-
- Rectangle {
- anchors.fill: parent
- anchors.margins: -2
- color: "transparent"
- radius: parent.radius + 2
- border.color: Qt.rgba(0, 0, 0, 0.08)
- border.width: 1
- z: -2
- }
-
- Rectangle {
- anchors.fill: parent
- color: "transparent"
- border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
- border.width: 1
- radius: parent.radius
- z: -1
- }
-
- transform: [
- Scale {
- id: scaleTransform
-
- origin.x: 400
- origin.y: 0
- xScale: notificationHistoryVisible ? 1 : 0.95
- yScale: notificationHistoryVisible ? 1 : 0.8
- },
- Translate {
- id: translateTransform
-
- x: notificationHistoryVisible ? 0 : 15
- y: notificationHistoryVisible ? 0 : -30
- }
- ]
-
- states: [
- State {
- name: "visible"
- when: notificationHistoryVisible
-
- PropertyChanges {
- target: scaleTransform
- xScale: 1
- yScale: 1
- }
-
- PropertyChanges {
- target: translateTransform
- x: 0
- y: 0
- }
- },
- State {
- name: "hidden"
- when: !notificationHistoryVisible
-
- PropertyChanges {
- target: scaleTransform
- xScale: 0.95
- yScale: 0.8
- }
-
- PropertyChanges {
- target: translateTransform
- x: 15
- y: -30
- }
- }
- ]
-
- transitions: [
- Transition {
- from: "*"
- to: "*"
-
- ParallelAnimation {
- NumberAnimation {
- targets: [scaleTransform, translateTransform]
- properties: "xScale,yScale,x,y"
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
- }
- }
- ]
+ scale: notificationHistoryVisible ? 1 : 0.9
MouseArea {
anchors.fill: parent
@@ -150,27 +71,71 @@ PanelWindow {
}
Column {
+ id: contentColumn
+
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
- NotificationHeader {}
-
- NotificationList {}
+ NotificationHeader {
+ id: notificationHeader
+ }
+
+ NotificationList {
+ id: notificationList
+
+ width: parent.width
+ height: parent.height - notificationHeader.height - contentColumn.spacing
+ }
+
+ }
+
+ Connections {
+ function onNotificationsChanged() {
+ mainRect.height = mainRect.calculateHeight();
+ }
+
+ function onGroupedNotificationsChanged() {
+ mainRect.height = mainRect.calculateHeight();
+ }
+
+ function onExpandedGroupsChanged() {
+ mainRect.height = mainRect.calculateHeight();
+ }
+
+ function onExpandedMessagesChanged() {
+ mainRect.height = mainRect.calculateHeight();
+ }
+
+ target: NotificationService
}
Behavior on height {
NumberAnimation {
- duration: Theme.shortDuration
- easing.type: Theme.standardEasing
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
}
+
}
Behavior on opacity {
NumberAnimation {
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
+ duration: Anims.durMed
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasized
}
+
}
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Anims.durMed
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasized
+ }
+
+ }
+
}
-}
\ No newline at end of file
+
+}
diff --git a/Modules/Notifications/NotificationEmptyState.qml b/Modules/Notifications/NotificationEmptyState.qml
index 5061a63d..6fa5e36e 100644
--- a/Modules/Notifications/NotificationEmptyState.qml
+++ b/Modules/Notifications/NotificationEmptyState.qml
@@ -5,14 +5,14 @@ import qs.Widgets
Item {
id: root
-
+
width: parent.width
height: 200
visible: NotificationService.notifications.length === 0
Column {
anchors.centerIn: parent
- spacing: Theme.spacingM
+ spacing: Theme.spacingXS
width: parent.width * 0.8
DankIcon {
@@ -24,21 +24,13 @@ Item {
Text {
anchors.horizontalCenter: parent.horizontalCenter
- text: "No notifications"
+ text: "Nothing to see here"
font.pixelSize: Theme.fontSizeLarge
- color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- text: "Notifications will appear here"
- font.pixelSize: Theme.fontSizeMedium
- color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
- horizontalAlignment: Text.AlignHCenter
- wrapMode: Text.WordWrap
- width: parent.width
- }
}
-}
\ No newline at end of file
+
+}
diff --git a/Modules/Notifications/NotificationHeader.qml b/Modules/Notifications/NotificationHeader.qml
index 43be85f4..94f57763 100644
--- a/Modules/Notifications/NotificationHeader.qml
+++ b/Modules/Notifications/NotificationHeader.qml
@@ -6,7 +6,7 @@ import qs.Widgets
Item {
id: root
-
+
width: parent.width
height: 32
@@ -48,11 +48,12 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
+
}
MouseArea {
id: clearArea
-
+
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -64,6 +65,7 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
+
}
Behavior on border.color {
@@ -71,6 +73,9 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
+
}
+
}
-}
\ No newline at end of file
+
+}
diff --git a/Modules/Notifications/NotificationList.qml b/Modules/Notifications/NotificationList.qml
index 63c5b029..86010c72 100644
--- a/Modules/Notifications/NotificationList.qml
+++ b/Modules/Notifications/NotificationList.qml
@@ -3,89 +3,116 @@ import QtQuick.Controls
import qs.Common
import qs.Services
-ScrollView {
+ListView {
id: root
-
+
+ property alias count: root.count
+ readonly property real listContentHeight: root.contentHeight
+ readonly property bool atYBeginning: root.contentY === 0
+ property real stableY: 0
+ property bool isUserScrolling: false
+
width: parent.width
- height: parent.height - 140
+ height: parent.height
clip: true
- contentWidth: -1
- ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
- ScrollBar.vertical.policy: ScrollBar.AsNeeded
+ model: NotificationService.groupedNotifications
+ spacing: Theme.spacingL
+ interactive: true
+ boundsBehavior: Flickable.StopAtBounds
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ cacheBuffer: 1000
+ onMovementStarted: isUserScrolling = true
+ onMovementEnded: {
+ isUserScrolling = false;
+ if (contentY > 40)
+ stableY = contentY;
- ListView {
- id: notificationsList
+ }
+ onContentYChanged: {
+ if (!isUserScrolling && visible && parent.visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
+ contentY = stableY;
- model: NotificationService.groupedNotifications
- spacing: Theme.spacingL
- interactive: true
- boundsBehavior: Flickable.StopAtBounds
- flickDeceleration: 1500
- maximumFlickVelocity: 2000
+ }
+
+ NotificationEmptyState {
+ visible: root.count === 0
+ anchors.centerIn: parent
+ }
+
+ add: Transition {
+ enabled: !root.isUserScrolling
+
+ ParallelAnimation {
+ NumberAnimation {
+ properties: "opacity"
+ from: 0
+ to: 1
+ duration: root.isUserScrolling ? 0 : Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+
+ NumberAnimation {
+ properties: "height"
+ from: 0
+ duration: root.isUserScrolling ? 0 : Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+
+ }
+
+ }
+
+ remove: Transition {
+ SequentialAnimation {
+ PauseAnimation {
+ duration: 50
+ }
- add: Transition {
ParallelAnimation {
NumberAnimation {
properties: "opacity"
- from: 0
- to: 1
+ to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
- properties: "height"
- from: 0
+ properties: "height,anchors.topMargin,anchors.bottomMargin"
+ to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
+
}
+
}
- remove: Transition {
- SequentialAnimation {
- PauseAnimation {
- duration: 50
- }
-
- ParallelAnimation {
- NumberAnimation {
- properties: "opacity"
- to: 0
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
-
- NumberAnimation {
- properties: "height,anchors.topMargin,anchors.bottomMargin"
- to: 0
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
- }
- }
- }
-
- displaced: Transition {
- NumberAnimation {
- properties: "y"
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
- }
-
- move: Transition {
- NumberAnimation {
- properties: "y"
- duration: Theme.mediumDuration
- easing.type: Theme.emphasizedEasing
- }
- }
-
- delegate: NotificationCard {
- notificationGroup: modelData
- }
}
- NotificationEmptyState {}
-}
\ No newline at end of file
+ displaced: Transition {
+ enabled: !root.isUserScrolling
+
+ NumberAnimation {
+ properties: "y"
+ duration: 0
+ }
+
+ }
+
+ move: Transition {
+ enabled: !root.isUserScrolling
+
+ NumberAnimation {
+ properties: "y"
+ duration: (root.atYBeginning && !root.isUserScrolling) ? Theme.mediumDuration : 0
+ easing.type: Theme.emphasizedEasing
+ }
+
+ }
+
+ delegate: NotificationCard {
+ notificationGroup: modelData
+ }
+
+}
diff --git a/Modules/Notifications/NotificationPopup.qml b/Modules/Notifications/NotificationPopup.qml
index af17e72f..e6f54dc0 100644
--- a/Modules/Notifications/NotificationPopup.qml
+++ b/Modules/Notifications/NotificationPopup.qml
@@ -60,7 +60,6 @@ PanelWindow {
readonly property bool isPopup: modelData.latestNotification.popup
readonly property int expireTimeout: modelData.latestNotification.notification.expireTimeout
property string stableGroupKey: ""
- // Watch for changes to latest notification (new message joins group)
property var currentLatestNotification: modelData.latestNotification
Component.onCompleted: {
@@ -70,7 +69,6 @@ PanelWindow {
height: {
if (expanded && modelData.count >= 1) {
const baseHeight = (116 * modelData.count) + (12 * (modelData.count - 1));
- // Add extra bottom margin for View/Dismiss buttons when there are fewer than 3 messages
const bottomMargin = modelData.count === 1 ? 70 : (modelData.count < 3 ? 50 : -28);
return baseHeight + bottomMargin;
}
@@ -81,8 +79,12 @@ PanelWindow {
border.color: modelData.latestNotification.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: modelData.latestNotification.urgency === 2 ? 2 : 1
clip: true
-
- // Material 3 elevation with multiple layers
+ onCurrentLatestNotificationChanged: {
+ if (isPopup && !cardHoverArea.containsMouse)
+ dismissTimer.restart();
+
+ }
+
Rectangle {
anchors.fill: parent
anchors.margins: -3
@@ -92,7 +94,7 @@ PanelWindow {
border.width: 1
z: -3
}
-
+
Rectangle {
anchors.fill: parent
anchors.margins: -2
@@ -102,7 +104,7 @@ PanelWindow {
border.width: 1
z: -2
}
-
+
Rectangle {
anchors.fill: parent
color: "transparent"
@@ -111,24 +113,35 @@ PanelWindow {
radius: parent.radius
z: -1
}
- onCurrentLatestNotificationChanged: {
- if (isPopup && !cardHoverArea.containsMouse)
- dismissTimer.restart();
-
- }
Rectangle {
- width: 4
- height: parent.height - 16
- anchors.left: parent.left
- anchors.leftMargin: 2
- anchors.verticalCenter: parent.verticalCenter
- radius: 2
- color: Theme.primary
+ anchors.fill: parent
+ radius: parent.radius
visible: modelData.latestNotification.urgency === 2
+ opacity: 1
+
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+
+ GradientStop {
+ position: 0
+ color: Theme.primary
+ }
+
+ GradientStop {
+ position: 0.02
+ color: Theme.primary
+ }
+
+ GradientStop {
+ position: 0.021
+ color: "transparent"
+ }
+
+ }
+
}
- // Collapsed view - show only latest notification
Item {
id: collapsedContent
@@ -160,11 +173,9 @@ PanelWindow {
anchors.fill: parent
anchors.margins: 2
source: {
- // Priority 1: Use notification image if available
if (parent.hasNotificationImage)
return modelData.latestNotification.cleanImage;
- // Priority 2: Use appIcon - handle URLs directly, use iconPath for icon names
if (modelData.latestNotification.appIcon) {
const appIcon = modelData.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
@@ -272,9 +283,7 @@ PanelWindow {
}
text: {
- // Auto-detect and make URLs clickable, with truncation for popups
let bodyText = modelData.latestNotification.body;
- // Truncate to 108 characters max for popup notifications
if (bodyText.length > 105)
bodyText = bodyText.substring(0, 102) + "...";
@@ -307,89 +316,42 @@ PanelWindow {
anchors.rightMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
- width: modelData.count > 1 ? 40 : 20 // Dynamic width: 40px for expand+close, 20px for close only
+ width: modelData.count > 1 ? 40 : 20
height: 24
- // Expand button - always takes up space but only visible when needed
- Rectangle {
- id: collapsedExpandButton
-
+ DankActionButton {
anchors.left: parent.left
anchors.top: parent.top
- width: 20
- height: 20
- radius: 10
- color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
visible: modelData.count > 1
-
- DankIcon {
- anchors.centerIn: parent
- name: expanded ? "expand_less" : "expand_more"
- size: 14
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: expandArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleGroupExpansion(modelData.key)
- }
-
+ iconName: expanded ? "expand_less" : "expand_more"
+ iconSize: 14
+ buttonSize: 20
+ z: 15
+ onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
- // Close button - always positioned at the right edge
- Rectangle {
- id: closeButton
-
- property bool isHovered: false
-
+ DankActionButton {
anchors.right: parent.right
anchors.top: parent.top
- width: 20
- height: 20
- radius: 10
- color: isHovered ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
- z: 10
-
- DankIcon {
- id: closeIcon
-
- name: "close"
- size: 14
- color: closeButton.isHovered ? Theme.primary : Theme.surfaceText
- anchors.centerIn: parent
- }
-
- MouseArea {
- id: dismissArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- z: 11
- onEntered: {
- closeButton.isHovered = true;
- dismissTimer.stop();
+ iconName: "close"
+ iconSize: 14
+ buttonSize: 20
+ z: 15
+ onClicked: {
+ if (modelData.latestNotification.notification.transient) {
+ NotificationService.dismissGroup(modelData.key);
+ } else {
+ for (const notif of modelData.notifications) {
+ notif.popup = false;
+ }
}
- onExited: {
- closeButton.isHovered = false;
- if (modelData.latestNotification.popup && !cardHoverArea.containsMouse)
- dismissTimer.restart();
-
- }
- onClicked: NotificationService.dismissGroup(modelData.key)
}
-
}
}
}
- // Expanded view - show all notifications in group
Item {
anchors.fill: parent
anchors.margins: 16
@@ -401,7 +363,6 @@ PanelWindow {
width: parent.width
spacing: 10
- // Header with app name and count
Item {
width: parent.width
height: 32
@@ -449,52 +410,28 @@ PanelWindow {
anchors.fill: parent
spacing: 8
- Rectangle {
- width: 20
- height: 20
- radius: 10
- color: expandedExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- anchors.centerIn: parent
- name: "expand_less"
- size: 14
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: expandedExpandArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleGroupExpansion(modelData.key)
- }
-
+ DankActionButton {
+ iconName: "expand_less"
+ iconSize: 14
+ buttonSize: 20
+ z: 15
+ onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
- Rectangle {
- width: 20
- height: 20
- radius: 10
- color: expandedCloseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- anchors.centerIn: parent
- name: "close"
- size: 14
- color: expandedCloseArea.containsMouse ? Theme.primary : Theme.surfaceText
+ DankActionButton {
+ iconName: "close"
+ iconSize: 14
+ buttonSize: 20
+ z: 15
+ onClicked: {
+ if (modelData.latestNotification.notification.transient) {
+ NotificationService.dismissGroup(modelData.key);
+ } else {
+ for (const notif of modelData.notifications) {
+ notif.popup = false;
+ }
+ }
}
-
- MouseArea {
- id: expandedCloseArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.dismissGroup(modelData.key)
- }
-
}
}
@@ -503,7 +440,6 @@ PanelWindow {
}
- // Scrollable list of individual notifications
Rectangle {
width: parent.width
height: Math.min(400, modelData.notifications.length * 90) // Fixed height constraint for inner scroll
@@ -669,56 +605,22 @@ PanelWindow {
spacing: 4
// Expand/collapse button for individual message
- Rectangle {
- id: expandButton
-
- width: 20
- height: 20
- radius: 10
- color: expandMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
+ DankActionButton {
visible: (modelData.body || "").length > 80
-
- DankIcon {
- anchors.centerIn: parent
- name: messageExpanded ? "expand_less" : "expand_more"
- size: 12
- color: Theme.surfaceText
- }
-
- MouseArea {
- id: expandMessageArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id)
- }
-
+ iconName: messageExpanded ? "expand_less" : "expand_more"
+ iconSize: 12
+ buttonSize: 20
+ z: 15
+ onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id)
}
// Close button for individual message
- Rectangle {
- width: 20
- height: 20
- radius: 10
- color: closeMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
-
- DankIcon {
- anchors.centerIn: parent
- name: "close"
- size: 12
- color: closeMessageArea.containsMouse ? Theme.primary : Theme.surfaceText
- }
-
- MouseArea {
- id: closeMessageArea
-
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: NotificationService.dismissNotification(modelData)
- }
-
+ DankActionButton {
+ iconName: "close"
+ iconSize: 12
+ buttonSize: 20
+ z: 15
+ onClicked: NotificationService.dismissNotification(modelData)
}
}
@@ -747,13 +649,13 @@ PanelWindow {
}
- // Main hover area for persistence
+ // Main hover area for persistence and click handling
MouseArea {
id: cardHoverArea
anchors.fill: parent
hoverEnabled: true
- acceptedButtons: Qt.NoButton
+ acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
z: 0
onEntered: {
@@ -764,71 +666,72 @@ PanelWindow {
dismissTimer.restart();
}
+ onClicked: {
+ if (modelData.latestNotification.notification.transient) {
+ NotificationService.dismissGroup(modelData.key);
+ } else {
+ for (const notif of modelData.notifications) {
+ notif.popup = false;
+ }
+ }
+ }
}
- // View button positioned at bottom-right of notification card
- Rectangle {
- id: viewButton
-
- property bool isHovered: false
-
+ // Action buttons positioned at bottom-left of notification card
+ Row {
anchors.right: dismissButton.left
- anchors.rightMargin: 4
+ anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
- width: viewText.width + 16
- height: viewText.height + 8
- radius: 6
- color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
+ spacing: 4
z: 10
- Text {
- id: viewText
+ Repeater {
+ model: modelData.latestNotification.actions || []
- text: "View"
- color: viewButton.isHovered ? Theme.primary : Theme.surfaceVariantText
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Font.Medium
- anchors.centerIn: parent
- }
+ Rectangle {
+ property bool isHovered: false
- MouseArea {
- id: viewArea
+ width: Math.min(actionText.contentWidth + 12, 70)
+ height: 24
+ radius: 4
+ color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- z: 11
- onEntered: {
- viewButton.isHovered = true;
- dismissTimer.stop();
- }
- onExited: {
- viewButton.isHovered = false;
- if (modelData.latestNotification.popup && !cardHoverArea.containsMouse)
- dismissTimer.restart();
+ Text {
+ id: actionText
- }
- onClicked: {
- // Handle navigation to source message
- if (modelData.latestNotification.actions) {
- for (const action of modelData.latestNotification.actions) {
- if (action.text && action.text.toLowerCase() === "view") {
- if (action.invoke) {
- action.invoke();
- return ;
- }
- }
+ text: modelData.text || ""
+ color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ elide: Text.ElideRight
+ width: Math.min(contentWidth, parent.width - 8)
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ parent.isHovered = true;
+ dismissTimer.stop();
}
- // If no View action, try the first available action
- if (modelData.latestNotification.actions.length > 0) {
- const firstAction = modelData.latestNotification.actions[0];
- if (firstAction.invoke)
- firstAction.invoke();
+ onExited: {
+ parent.isHovered = false;
+ if (modelData.latestNotification.popup && !cardHoverArea.containsMouse)
+ dismissTimer.restart();
+
+ }
+ onClicked: {
+ if (modelData && modelData.invoke)
+ modelData.invoke();
}
}
+
}
+
}
}
@@ -877,12 +780,7 @@ PanelWindow {
}
onClicked: {
- // Move to notification center (don't close)
- const groupKey = stableGroupKey || modelData.key;
- console.log("Manually hiding notification group from popup:", groupKey);
- modelData.latestNotification.popup = false;
- // Clear expansion state when manually hiding from popup
- NotificationService.clearGroupExpansionState(groupKey);
+ NotificationService.dismissGroup(modelData.key);
}
}
@@ -896,7 +794,6 @@ PanelWindow {
onTriggered: {
// Move to notification center (don't dismiss completely)
const groupKey = stableGroupKey || modelData.key;
- console.log("Auto-hiding notification group from popup:", groupKey);
modelData.latestNotification.popup = false;
// Clear expansion state when hiding from popup
NotificationService.clearGroupExpansionState(groupKey);
diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml
index 8fcb41e1..2b6f8fb6 100644
--- a/Modules/TopBar/TopBar.qml
+++ b/Modules/TopBar/TopBar.qml
@@ -323,7 +323,6 @@ PanelWindow {
if (controlCenterPopout.controlCenterVisible) {
if (NetworkService.wifiEnabled)
NetworkService.scanWifi();
-
}
}
}
@@ -334,4 +333,4 @@ PanelWindow {
}
-}
+}
\ No newline at end of file
diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml
index 49f751bf..842003fd 100644
--- a/Services/NotificationService.qml
+++ b/Services/NotificationService.qml
@@ -16,8 +16,9 @@ Singleton {
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
- property var expandedGroups: ({}) // Track which groups are expanded
- property var expandedMessages: ({}) // Track which individual messages are expanded
+ property var expandedGroups: ({})
+ property var expandedMessages: ({})
+ property bool popupsDisabled: false
// Notification persistence settings
property int maxStoredNotifications: 100
@@ -29,6 +30,7 @@ Singleton {
keepOnReload: false
actionsSupported: true
+ actionIconsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
@@ -36,50 +38,23 @@ Singleton {
inlineReplySupported: true
onNotification: notif => {
- console.log("=== RAW NOTIFICATION DATA ===");
- console.log("appName:", notif.appName);
- console.log("summary:", notif.summary);
- console.log("body:", notif.body);
- console.log("appIcon:", notif.appIcon);
- console.log("image:", notif.image);
- console.log("urgency:", notif.urgency);
- console.log("hasInlineReply:", notif.hasInlineReply);
- console.log("=============================");
- // Check if notification should be shown based on settings
- if (!NotificationSettings.shouldShowNotification(notif)) {
- console.log("Notification blocked by settings for app:", notif.appName);
- return;
- }
notif.tracked = true;
const wrapper = notifComponent.createObject(root, {
- popup: true,
+ popup: !notif.transient, // Transient notifications show as popups but don't persist
notification: notif
});
if (wrapper) {
const groupKey = getGroupKey(wrapper);
- console.log("New notification added to group:", groupKey, "Expansion state:", expandedGroups[groupKey] || false);
- // Handle media notification replacement
- if (wrapper.isMedia) {
- handleMediaNotification(wrapper);
- } else {
- root.notifications.push(wrapper);
- }
- // Don't auto-expand groups - let user control expansion state
-
- // Add to persistent storage (only for non-transient notifications)
+ // Only add to notifications list if not transient
if (!notif.transient) {
+ root.notifications.push(wrapper);
addToPersistentStorage(wrapper);
}
-
- console.log("Notification added. Total notifications:", root.notifications.length);
- console.log("Grouped notifications:", root.groupedNotifications.length);
- } else {
- console.error("Failed to create notification wrapper");
}
}
}
@@ -87,7 +62,11 @@ Singleton {
component NotifWrapper: QtObject {
id: wrapper
- property bool popup: true
+ property bool popup: false
+
+ Component.onCompleted: {
+ popup = !root.popupsDisabled && !notification.transient;
+ }
readonly property date time: new Date()
readonly property string timeStr: {
const now = new Date();
@@ -114,6 +93,7 @@ Singleton {
return appIcon;
}
readonly property string appName: notification.appName
+ readonly property string desktopEntry: notification.desktopEntry
readonly property string image: notification.image
readonly property string cleanImage: {
if (!image) return "";
@@ -128,85 +108,6 @@ Singleton {
// Enhanced properties for better handling
readonly property bool hasImage: image && image.length > 0
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
- readonly property bool isConversation: notification.hasInlineReply
- readonly property bool isMedia: isMediaNotification()
- readonly property bool isSystem: isSystemNotification()
-
- function isMediaNotification() {
- const appNameLower = appName.toLowerCase();
- const summaryLower = summary.toLowerCase();
-
- // Check for media apps
- if (appNameLower.includes("spotify") ||
- appNameLower.includes("vlc") ||
- appNameLower.includes("mpv") ||
- appNameLower.includes("music") ||
- appNameLower.includes("player") ||
- appNameLower.includes("youtube") ||
- appNameLower.includes("media")) {
- return true;
- }
-
- // Check for media-related summary text
- if (summaryLower.includes("now playing") ||
- summaryLower.includes("playing") ||
- summaryLower.includes("paused") ||
- summaryLower.includes("track")) {
- return true;
- }
-
- // Check for media actions
- for (const action of actions) {
- const actionId = action.identifier.toLowerCase();
- if (actionId.includes("play") ||
- actionId.includes("pause") ||
- actionId.includes("next") ||
- actionId.includes("previous") ||
- actionId.includes("media")) {
- return true;
- }
- }
-
- return false;
- }
-
- function isSystemNotification() {
- const appNameLower = appName.toLowerCase();
- const summaryLower = summary.toLowerCase();
-
- // Check for system apps
- if (appNameLower.includes("system") ||
- appNameLower.includes("networkmanager") ||
- appNameLower.includes("upower") ||
- appNameLower.includes("notification-daemon") ||
- appNameLower.includes("systemd") ||
- appNameLower.includes("update") ||
- appNameLower.includes("battery") ||
- appNameLower.includes("network") ||
- appNameLower.includes("wifi") ||
- appNameLower.includes("bluetooth")) {
- return true;
- }
-
- // Check for system-related summary text
- if (summaryLower.includes("battery") ||
- summaryLower.includes("power") ||
- summaryLower.includes("update") ||
- summaryLower.includes("connected") ||
- summaryLower.includes("disconnected") ||
- summaryLower.includes("network") ||
- summaryLower.includes("wifi") ||
- summaryLower.includes("bluetooth")) {
- return true;
- }
-
- return false;
- }
-
-
-
-
-
readonly property Connections conn: Connections {
target: wrapper.notification.Retainable
@@ -218,11 +119,14 @@ Singleton {
const groupKey = getGroupKey(wrapper);
root.notifications.splice(index, 1);
- // Check if this group now has no notifications left
+ // Check if this group now has no notifications left or only 1 left
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey);
if (remainingInGroup.length === 0) {
// Immediately clear expansion state for empty group
clearGroupExpansionState(groupKey);
+ } else if (remainingInGroup.length === 1) {
+ // Collapse groups that only have 1 notification left
+ clearGroupExpansionState(groupKey);
}
// Clean up all expansion states
@@ -243,88 +147,43 @@ Singleton {
// Helper functions
function clearAllNotifications() {
- // Create a copy of the array to avoid modification during iteration
+ // Actually dismiss all notifications from center
const notificationsCopy = [...root.notifications];
- for (const notif of notificationsCopy) {
+ notificationsCopy.forEach(notif => {
notif.notification.dismiss();
- }
- // Note: Expansion states will be cleaned up by onDropped as notifications are removed
+ });
+ // Clear all expansion states
+ expandedGroups = {};
+ expandedMessages = {};
}
function dismissNotification(wrapper) {
wrapper.notification.dismiss();
}
-
- function handleMediaNotification(newMediaWrapper) {
- const groupKey = getGroupKey(newMediaWrapper);
-
- // Find and replace any existing media notification from the same app
- for (let i = notifications.length - 1; i >= 0; i--) {
- const existing = notifications[i];
- if (existing.isMedia && getGroupKey(existing) === groupKey) {
- // Replace the existing media notification
- existing.notification.dismiss();
- break;
+
+ function hidePopup(wrapper) {
+ wrapper.popup = false;
+ }
+
+ function disablePopups(disable) {
+ popupsDisabled = disable;
+ if (disable) {
+ for (const notif of root.notifications) {
+ notif.popup = false;
}
}
-
- // Add the new media notification
- root.notifications.push(newMediaWrapper);
}
+
// Android 16-style notification grouping functions
function getGroupKey(wrapper) {
- const appName = wrapper.appName.toLowerCase();
-
- // Media notifications: replace previous media notification from same app
- if (wrapper.isMedia) {
- return `${appName}:media`;
+ // Priority 1: Use desktopEntry if available
+ if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
+ return wrapper.desktopEntry.toLowerCase();
}
- // System notifications: group by category
- if (wrapper.isSystem) {
- const summary = wrapper.summary.toLowerCase();
-
- if (summary.includes("battery") || summary.includes("power")) {
- return "system:battery";
- }
- if (summary.includes("network") || summary.includes("wifi") || summary.includes("connected") || summary.includes("disconnected")) {
- return "system:network";
- }
- if (summary.includes("update") || summary.includes("upgrade")) {
- return "system:updates";
- }
- if (summary.includes("bluetooth")) {
- return "system:bluetooth";
- }
-
- // Default system grouping
- return "system:general";
- }
-
- // Conversation apps with inline reply
- if (wrapper.isConversation) {
- const summary = wrapper.summary.toLowerCase();
-
- // Group by conversation/channel name from summary
- if (summary.includes("#")) {
- const channelMatch = summary.match(/#[\w-]+/);
- if (channelMatch) {
- return `${appName}:${channelMatch[0]}`;
- }
- }
-
- // Group by sender/conversation name if meaningful
- if (summary && !summary.includes("new message") && !summary.includes("notification")) {
- return `${appName}:${summary}`;
- }
-
- // Default conversation grouping
- return `${appName}:conversation`;
- }
-
- // Default: Group by app
- return appName;
+ // Priority 2: Use appName as fallback
+ return wrapper.appName.toLowerCase();
}
function getGroupedNotifications() {
@@ -340,9 +199,6 @@ Singleton {
latestNotification: null,
count: 0,
hasInlineReply: false,
- isConversation: notif.isConversation,
- isMedia: notif.isMedia,
- isSystem: notif.isSystem
};
}
@@ -356,6 +212,11 @@ Singleton {
}
return Object.values(groups).sort((a, b) => {
+ const aUrgency = a.latestNotification.urgency || 0;
+ const bUrgency = b.latestNotification.urgency || 0;
+ if (aUrgency !== bUrgency) {
+ return bUrgency - aUrgency;
+ }
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
});
}
@@ -373,9 +234,6 @@ Singleton {
latestNotification: null,
count: 0,
hasInlineReply: false,
- isConversation: notif.isConversation,
- isMedia: notif.isMedia,
- isSystem: notif.isSystem
};
}
@@ -389,6 +247,11 @@ Singleton {
}
return Object.values(groups).sort((a, b) => {
+ const aUrgency = a.latestNotification.urgency || 0;
+ const bUrgency = b.latestNotification.urgency || 0;
+ if (aUrgency !== bUrgency) {
+ return bUrgency - aUrgency;
+ }
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
});
}
@@ -403,18 +266,15 @@ Singleton {
}
function dismissGroup(groupKey) {
- console.log("Completely dismissing group:", groupKey);
const group = groupedNotifications.find(g => g.key === groupKey);
if (group) {
for (const notif of group.notifications) {
notif.notification.dismiss();
}
}
- // Note: Expansion state will be cleaned up by onDropped when notifications are removed
}
function clearGroupExpansionState(groupKey) {
- // Immediately remove expansion state for a specific group
let newExpandedGroups = {};
for (const key in expandedGroups) {
if (key !== groupKey && expandedGroups[key]) {
@@ -422,22 +282,16 @@ Singleton {
}
}
expandedGroups = newExpandedGroups;
-
- console.log("Cleared expansion state for group:", groupKey);
}
function cleanupExpansionStates() {
- // Get all current group keys and message IDs
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key));
const currentMessageIds = new Set();
-
for (const group of groupedNotifications) {
for (const notif of group.notifications) {
currentMessageIds.add(notif.notification.id);
}
}
-
- // Clean up expanded groups that no longer exist
let newExpandedGroups = {};
for (const key in expandedGroups) {
if (currentGroupKeys.has(key) && expandedGroups[key]) {
@@ -445,8 +299,6 @@ Singleton {
}
}
expandedGroups = newExpandedGroups;
-
- // Clean up expanded messages that no longer exist
let newExpandedMessages = {};
for (const messageId in expandedMessages) {
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
@@ -469,69 +321,15 @@ Singleton {
if (group.count === 1) {
return group.latestNotification.summary;
}
-
- if (group.isMedia) {
- return "Now Playing";
- }
-
- if (group.isSystem) {
- const keyParts = group.key.split(":");
- if (keyParts.length > 1) {
- const systemCategory = keyParts[1];
- switch (systemCategory) {
- case "battery": return `${group.count} Battery alerts`;
- case "network": return `${group.count} Network updates`;
- case "updates": return `${group.count} System updates`;
- case "bluetooth": return `${group.count} Bluetooth updates`;
- default: return `${group.count} System notifications`;
- }
- }
- return `${group.count} System notifications`;
- }
-
- if (group.isConversation) {
- const keyParts = group.key.split(":");
- if (keyParts.length > 1) {
- const conversationKey = keyParts[keyParts.length - 1];
- if (conversationKey !== "conversation") {
- return `${conversationKey}: ${group.count} messages`;
- }
- }
- return `${group.count} new messages`;
- }
-
return `${group.count} notifications`;
}
-
function getGroupBody(group) {
if (group.count === 1) {
return group.latestNotification.body;
}
-
- if (group.isMedia) {
- const latest = group.latestNotification;
- if (latest.body && latest.body.length > 0) {
- return latest.body;
- }
- return latest.summary;
- }
-
- if (group.isSystem) {
- return `Latest: ${group.latestNotification.summary}`;
- }
-
- if (group.isConversation) {
- const latest = group.latestNotification;
- if (latest.body && latest.body.length > 0) {
- return latest.body;
- }
- return "Tap to view conversation";
- }
-
return `Latest: ${group.latestNotification.summary}`;
}
- // Notification persistence functions
function addToPersistentStorage(wrapper) {
const persistedNotif = {
id: wrapper.notification.id,
@@ -542,45 +340,29 @@ Singleton {
image: wrapper.image,
urgency: wrapper.urgency,
timestamp: wrapper.time.getTime(),
- isConversation: wrapper.isConversation,
- isMedia: wrapper.isMedia,
- isSystem: wrapper.isSystem
};
-
- // Add to beginning of array
persistedNotifications.unshift(persistedNotif);
-
- // Clean up old notifications
cleanupPersistentStorage();
}
function cleanupPersistentStorage() {
const now = new Date().getTime();
let newPersisted = [];
-
for (let i = 0; i < persistedNotifications.length && i < maxStoredNotifications; i++) {
const notif = persistedNotifications[i];
if (now - notif.timestamp < maxNotificationAge) {
newPersisted.push(notif);
}
}
-
persistedNotifications = newPersisted;
}
function getPersistentNotificationsByApp(appName) {
return persistedNotifications.filter(notif => notif.appName.toLowerCase() === appName.toLowerCase());
}
-
function getPersistentNotificationsByType(type) {
- switch (type) {
- case "conversation": return persistedNotifications.filter(notif => notif.isConversation);
- case "media": return persistedNotifications.filter(notif => notif.isMedia);
- case "system": return persistedNotifications.filter(notif => notif.isSystem);
- default: return persistedNotifications;
- }
+ return persistedNotifications;
}
-
function searchPersistentNotifications(query) {
const searchLower = query.toLowerCase();
return persistedNotifications.filter(notif =>
@@ -589,8 +371,6 @@ Singleton {
notif.body.toLowerCase().includes(searchLower)
);
}
-
- // Initialize persistence on component creation
Component.onCompleted: {
cleanupPersistentStorage();
}
diff --git a/Services/NotificationSettings.qml b/Services/NotificationSettings.qml
deleted file mode 100644
index d08943c0..00000000
--- a/Services/NotificationSettings.qml
+++ /dev/null
@@ -1,194 +0,0 @@
-pragma Singleton
-pragma ComponentBehavior: Bound
-
-import QtQuick
-import Quickshell
-
-Singleton {
- id: root
-
- // General notification settings
- property bool notificationsEnabled: true
- property bool soundEnabled: true
- property bool persistNotifications: true
- property int defaultTimeout: 5000 // milliseconds
-
- // Grouping settings
- property bool enableSmartGrouping: true
- property bool autoExpandConversations: true
- property bool replaceMediaNotifications: true
-
- // Persistence settings
- property int maxStoredNotifications: 100
- property int notificationRetentionDays: 7
-
- // Display settings
- property bool showNotificationPopups: true
- property bool showAppIcons: true
- property bool showTimestamps: true
- property bool enableInlineReply: true
- property bool showActionButtons: true
-
- // Priority settings
- property bool allowCriticalNotifications: true
- property bool respectDoNotDisturb: true
-
- // App-specific settings
- property var appSettings: ({})
-
- // Do Not Disturb settings
- property bool doNotDisturbMode: false
- property string doNotDisturbStart: "22:00"
- property string doNotDisturbEnd: "08:00"
- property bool allowCriticalInDND: true
-
- // Sound settings
- property string notificationSound: "default"
- property real soundVolume: 0.7
- property bool vibrationEnabled: false
-
- function getAppSetting(appName, setting, defaultValue) {
- const app = appSettings[appName.toLowerCase()];
- if (app && app.hasOwnProperty(setting)) {
- return app[setting];
- }
- return defaultValue;
- }
-
- function setAppSetting(appName, setting, value) {
- let newAppSettings = {};
- for (const app in appSettings) {
- newAppSettings[app] = appSettings[app];
- }
-
- const appKey = appName.toLowerCase();
- if (!newAppSettings[appKey]) {
- newAppSettings[appKey] = {};
- }
- newAppSettings[appKey][setting] = value;
- appSettings = newAppSettings;
-
- // Save to persistent storage
- saveSettings();
- }
-
- function isAppBlocked(appName) {
- const appKey = appName.toLowerCase();
- if (appKey === "notify-send" || appKey === "libnotify") {
- return false;
- }
- return getAppSetting(appName, "blocked", false);
- }
-
- function isAppMuted(appName) {
- return getAppSetting(appName, "muted", false);
- }
-
- function getAppTimeout(appName) {
- return getAppSetting(appName, "timeout", defaultTimeout);
- }
-
- function isInDoNotDisturbMode() {
- if (!doNotDisturbMode && !respectDoNotDisturb) {
- return false;
- }
-
- const now = new Date();
- const currentTime = now.getHours() * 60 + now.getMinutes();
-
- const startParts = doNotDisturbStart.split(":");
- const endParts = doNotDisturbEnd.split(":");
- const startTime = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
- const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
-
- if (startTime <= endTime) {
- // Same day range (e.g., 9:00 - 17:00)
- return currentTime >= startTime && currentTime <= endTime;
- } else {
- // Overnight range (e.g., 22:00 - 08:00)
- return currentTime >= startTime || currentTime <= endTime;
- }
- }
-
- function shouldShowNotification(notification) {
- // Check if notifications are globally disabled
- if (!notificationsEnabled) {
- return false;
- }
-
- // Check if app is blocked
- if (isAppBlocked(notification.appName)) {
- return false;
- }
-
- // DND logic temporarily disabled for all notifications
- // if (isInDoNotDisturbMode()) {
- // // Allow critical notifications if configured
- // if (allowCriticalInDND && notification.urgency === 2) {
- // return true;
- // }
- // return false;
- // }
-
- return true;
- }
-
- function shouldPlaySound(notification) {
- if (!soundEnabled) {
- return false;
- }
-
- if (isAppMuted(notification.appName)) {
- return false;
- }
-
- if (isInDoNotDisturbMode() && !allowCriticalInDND) {
- return false;
- }
-
- return true;
- }
-
- function saveSettings() {
- // In a real implementation, this would save to a config file
- console.log("NotificationSettings: Settings saved");
- }
-
- function loadSettings() {
- // In a real implementation, this would load from a config file
- console.log("NotificationSettings: Settings loaded");
- }
-
- function resetToDefaults() {
- notificationsEnabled = true;
- soundEnabled = true;
- persistNotifications = true;
- defaultTimeout = 5000;
- enableSmartGrouping = true;
- autoExpandConversations = true;
- replaceMediaNotifications = true;
- maxStoredNotifications = 100;
- notificationRetentionDays = 7;
- showNotificationPopups = true;
- showAppIcons = true;
- showTimestamps = true;
- enableInlineReply = true;
- showActionButtons = true;
- allowCriticalNotifications = true;
- respectDoNotDisturb = true;
- doNotDisturbMode = false;
- doNotDisturbStart = "22:00";
- doNotDisturbEnd = "08:00";
- allowCriticalInDND = true;
- notificationSound = "default";
- soundVolume = 0.7;
- vibrationEnabled = false;
- appSettings = {};
-
- saveSettings();
- }
-
- Component.onCompleted: {
- loadSettings();
- }
-}
\ No newline at end of file
diff --git a/Widgets/StateLayer.qml b/Widgets/StateLayer.qml
index 1f701196..e66810c1 100644
--- a/Widgets/StateLayer.qml
+++ b/Widgets/StateLayer.qml
@@ -7,7 +7,7 @@ MouseArea {
property bool disabled: false
property color stateColor: Theme.surfaceText
- property real cornerRadius: parent?.radius ?? Appearance.rounding.normal
+ property real cornerRadius: parent?.radius ?? Theme.cornerRadius
anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor
diff --git a/verify-notifications.sh b/verify-notifications.sh
index 3aeb0cca..b51054d0 100755
--- a/verify-notifications.sh
+++ b/verify-notifications.sh
@@ -18,40 +18,34 @@ fi
# Test 1: Basic notifications
echo "📱 Test 1: Basic notifications"
-notify-send -i preferences-desktop "Test App" "Basic notification message"
+notify-send -h string:desktop-entry:org.gnome.Settings -i preferences-desktop "Settings" "Basic notification message"
sleep 2
-# Test 2: Media notifications (should replace each other)
-echo "🎵 Test 2: Media notifications (replacement behavior)"
-notify-send -i audio-x-generic "Spotify" "Now Playing: Song 1 - Artist A"
-sleep 2
-notify-send -i audio-x-generic "Spotify" "Now Playing: Song 2 - Artist B"
-sleep 2
-
-# Test 3: System notifications (grouped by category)
-echo "🔋 Test 3: System notifications (grouped by category)"
-notify-send -i battery "UPower" "Battery Low: 15% remaining"
+# Test 2: Media notifications (should group under Spotify)
+echo "🎵 Test 2: Media notifications (grouping)"
+notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 1 - Artist A"
sleep 1
-notify-send -i network-wired "NetworkManager" "Network Connected: WiFi connected"
+notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 2 - Artist B"
sleep 1
-notify-send -i system-software-update "System" "Updates Available: 5 packages can be updated"
+notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 3 - Artist C"
sleep 2
-# Test 4: Conversation notifications (should group and auto-expand)
-echo "💬 Test 4: Conversation notifications (grouping)"
-if command -v discord &> /dev/null; then
- notify-send -i discord "Discord" "#general: User1 says Hello everyone!"
- sleep 1
- notify-send -i discord "Discord" "#general: User2 says Hey there!"
- sleep 1
- notify-send -i discord "Discord" "john_doe: Private message from John"
-else
- notify-send -i internet-chat "Discord" "#general: User1 says Hello everyone!"
- sleep 1
- notify-send -i internet-chat "Discord" "#general: User2 says Hey there!"
- sleep 1
- notify-send -i internet-chat "Discord" "john_doe: Private message from John"
-fi
+# Test 3: System notifications (separate groups)
+echo "🔋 Test 3: System notifications (separate apps)"
+notify-send -h string:desktop-entry:org.gnome.PowerStats -i battery "Power Manager" "Battery Low: 15% remaining"
+sleep 1
+notify-send -h string:desktop-entry:org.gnome.NetworkDisplays -i network-wired "Network Manager" "WiFi Connected: HomeNetwork"
+sleep 1
+notify-send -h string:desktop-entry:org.gnome.Software -i system-software-update "Software" "5 updates available"
+sleep 2
+
+# Test 4: Chat notifications (should group under Discord)
+echo "💬 Test 4: Chat notifications (grouping)"
+notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "#general: User1 says Hello everyone!"
+sleep 1
+notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "#general: User2 says Hey there!"
+sleep 1
+notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "john_doe: Private message from John"
sleep 2
# Test 5: Urgent notifications
@@ -61,16 +55,16 @@ sleep 2
# Test 6: Notifications with actions (simulated)
echo "⚡ Test 6: Action buttons"
-notify-send -i system-upgrade "System Update" "Updates available - Click to install or remind later"
+notify-send -h string:desktop-entry:org.gnome.Software -i system-upgrade "Software" "Updates available - Click to install or remind later"
sleep 2
-# Test 7: Multiple apps generating notifications
-echo "📊 Test 7: Multiple apps"
-notify-send -i mail-message-new "Email" "You have 3 new emails"
+# Test 7: Multiple different apps
+echo "📊 Test 7: Multiple different apps"
+notify-send -h string:desktop-entry:thunderbird -i mail-message-new "Thunderbird" "You have 3 new emails"
sleep 0.5
-notify-send -i office-calendar "Calendar" "Daily standup in 5 minutes"
+notify-send -h string:desktop-entry:org.gnome.Calendar -i office-calendar "Calendar" "Daily standup in 5 minutes"
sleep 0.5
-notify-send -i folder-downloads "File Manager" "document.pdf downloaded"
+notify-send -h string:desktop-entry:org.gnome.Nautilus -i folder-downloads "Files" "document.pdf downloaded"
sleep 2
echo ""