From 033d8b034b24fe29602677a534e55b7570c79e4a Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 18 Jul 2025 23:21:10 -0400 Subject: [PATCH] Implement 2nd tier group expansion on notifications --- Modules/NotificationCenter.qml | 318 ++++++++++++++++++++--------- Modules/NotificationPopup.qml | 330 +++++++++++++++++++++---------- Services/NotificationService.qml | 10 + test_notifications.sh | 18 ++ 4 files changed, 478 insertions(+), 198 deletions(-) create mode 100755 test_notifications.sh diff --git a/Modules/NotificationCenter.qml b/Modules/NotificationCenter.qml index 6e387514..d8c7bc62 100644 --- a/Modules/NotificationCenter.qml +++ b/Modules/NotificationCenter.qml @@ -402,26 +402,38 @@ PanelWindow { width: parent.width height: parent.height - readonly property bool isScreenshot: { - // Check if this is a screenshot notification using NotificationService detection - return modelData.latestNotification.isScreenshot; + readonly property bool isScreenshot: modelData.latestNotification.isScreenshot + readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== "" + + // Priority 1: Notification image using Quickshell IconImage + Rectangle { + anchors.fill: parent + anchors.margins: 2 + radius: 20 + clip: true + visible: parent.hasNotificationImage && centerNotificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: centerNotificationImage + anchors.fill: parent + source: modelData.latestNotification.image || "" + } } - - - // Use Material Symbols icon for screenshots with fallback + // Priority 2: Material Symbols icon for screenshots without notification images DankIcon { anchors.centerIn: parent name: "screenshot_monitor" size: 20 color: Theme.primaryText - visible: parent.isScreenshot + visible: parent.isScreenshot && !parent.hasNotificationImage } - // Fallback to first letter for non-screenshot notifications + // Priority 3: Fallback to first letter for other notifications Text { anchors.centerIn: parent - visible: !parent.isScreenshot + visible: !parent.hasNotificationImage && !parent.isScreenshot text: { const appName = modelData.appName || "?"; return appName.charAt(0).toUpperCase(); @@ -670,6 +682,71 @@ PanelWindow { spacing: Theme.spacingM visible: expanded + // 1st tier controls - moved above group header as per mockup + Item { + width: parent.width + height: 32 + + // Controls container - fixed position on right + Item { + width: 72 + height: 32 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 32 + height: 32 + radius: 16 + anchors.left: parent.left + color: collapseAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "expand_less" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: collapseAreaTop + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.toggleGroupExpansion(modelData.key) + } + + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + anchors.right: parent.right + color: dismissAllAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 16 + color: Theme.surfaceText + } + + MouseArea { + id: dismissAllAreaTop + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.dismissGroup(modelData.key) + } + + } + + } + } + // Group header in expanded view Item { width: parent.width @@ -686,34 +763,50 @@ PanelWindow { border.width: 1 clip: true + readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== "" + + // Priority 1: Notification image using Quickshell IconImage + Rectangle { + anchors.fill: parent + anchors.margins: 2 + radius: 16 + clip: true + visible: parent.hasNotificationImage && expandedHeaderNotificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: expandedHeaderNotificationImage + anchors.fill: parent + source: modelData.latestNotification.image || "" + } + } + + // Priority 2: App icon for non-screenshots without notification images IconImage { anchors.fill: parent anchors.margins: 4 source: { - // Don't try to load icons for screenshots - let fallback handle them - const isScreenshot = modelData.latestNotification.isScreenshot; - - if (isScreenshot) { + if (parent.hasNotificationImage || modelData.latestNotification.isScreenshot) { return ""; } - return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : ""; } visible: status === Image.Ready } - // Material Symbols icon for screenshots in expanded header + // Priority 3: Material Symbols icon for screenshots without notification images DankIcon { anchors.centerIn: parent name: "screenshot_monitor" size: 16 color: Theme.primaryText - visible: modelData.latestNotification.isScreenshot + visible: modelData.latestNotification.isScreenshot && !parent.hasNotificationImage } + // Priority 4: Fallback text Text { anchors.centerIn: parent - visible: !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "") + visible: !parent.hasNotificationImage && !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "") text: { const appName = modelData.appName || "?"; return appName.charAt(0).toUpperCase(); @@ -725,74 +818,41 @@ PanelWindow { } - Text { + // App name and count badge + Row { anchors.left: parent.left anchors.leftMargin: 52 anchors.verticalCenter: parent.verticalCenter - text: modelData.appName - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Bold - } - - Item { - width: 72 - height: 32 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - width: 32 - height: 32 - radius: 16 - 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: 18 - color: Theme.surfaceText - } - - MouseArea { - id: collapseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleGroupExpansion(modelData.key) - } + spacing: Theme.spacingS + Text { + text: modelData.appName + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter } + // Message count badge when expanded Rectangle { - width: 32 - height: 32 - radius: 16 - anchors.right: parent.right - color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + width: 20 + height: 20 + radius: 10 + color: Theme.primary + visible: modelData.count > 1 + anchors.verticalCenter: parent.verticalCenter - DankIcon { + Text { anchors.centerIn: parent - name: "close" - size: 16 - color: Theme.surfaceText + text: modelData.count.toString() + color: Theme.primaryText + font.pixelSize: 10 + font.weight: Font.Bold } - - MouseArea { - id: dismissAllArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissGroup(modelData.key) - } - } - } + } // Individual notifications @@ -833,18 +893,37 @@ PanelWindow { border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) border.width: 1 - // Material Symbols icon for individual screenshot notifications + readonly property bool hasNotificationImage: modelData.image && modelData.image !== "" + + // Priority 1: Notification image using Quickshell IconImage + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: 14 + clip: true + visible: parent.hasNotificationImage && centerIndividualNotificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: centerIndividualNotificationImage + anchors.fill: parent + source: modelData.image || "" + } + } + + // Priority 2: Material Symbols icon for screenshots without notification images DankIcon { anchors.centerIn: parent name: "screenshot_monitor" size: 12 color: Theme.primaryText - visible: modelData.isScreenshot + visible: modelData.isScreenshot && !parent.hasNotificationImage } + // Priority 3: Fallback text Text { anchors.centerIn: parent - visible: !modelData.isScreenshot + visible: !parent.hasNotificationImage && !modelData.isScreenshot text: { const appName = modelData.appName || "?"; return appName.charAt(0).toUpperCase(); @@ -856,30 +935,64 @@ PanelWindow { } - Rectangle { - width: 24 + // Individual controls - expand and dismiss buttons + Row { + width: 50 height: 24 - radius: 12 anchors.right: parent.right anchors.top: parent.top - color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + spacing: 2 - DankIcon { - anchors.centerIn: parent - name: "close" - size: 12 - color: Theme.surfaceVariantText + // Expand/collapse button for 2nd tier + Rectangle { + width: 24 + height: 24 + radius: 12 + color: individualExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + visible: modelData.body && modelData.body.length > 50 // Only show if body text is long enough + + property bool isExpanded: NotificationService.expandedMessages[modelData.notification.id] || false + + DankIcon { + anchors.centerIn: parent + name: parent.isExpanded ? "expand_less" : "expand_more" + size: 12 + color: Theme.surfaceVariantText + } + + MouseArea { + id: individualExpandArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id) + } } - MouseArea { - id: individualDismissArea + // Individual dismiss button + Rectangle { + width: 24 + height: 24 + radius: 12 + color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissNotification(modelData) + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: Theme.surfaceVariantText + } + + MouseArea { + id: individualDismissArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.dismissNotification(modelData) + } } - } Column { @@ -888,27 +1001,42 @@ PanelWindow { anchors.left: parent.left anchors.leftMargin: 44 anchors.right: parent.right - anchors.rightMargin: 36 + anchors.rightMargin: 60 // More space for expanded controls anchors.top: parent.top spacing: Theme.spacingXS + property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false + + // Title • timestamp format Text { - text: modelData.summary + text: { + const summary = modelData.summary || ""; + const timeStr = modelData.timeStr || ""; + if (summary && timeStr) { + return summary + " • " + timeStr; + } else if (summary) { + return summary; + } else { + return "Message • " + timeStr; + } + } color: Theme.surfaceText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium width: parent.width elide: Text.ElideRight + maximumLineCount: 1 } + // Body text with expandable behavior Text { text: modelData.body color: Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall width: parent.width wrapMode: Text.WordWrap - maximumLineCount: 3 - elide: Text.ElideRight + maximumLineCount: parent.isMessageExpanded ? -1 : 3 // Unlimited when expanded, 3 when collapsed (more space in center) + elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight visible: text.length > 0 } diff --git a/Modules/NotificationPopup.qml b/Modules/NotificationPopup.qml index cd4bde9d..c3624088 100644 --- a/Modules/NotificationPopup.qml +++ b/Modules/NotificationPopup.qml @@ -173,23 +173,37 @@ PanelWindow { height: parent.height readonly property bool isScreenshot: modelData.latestNotification.isScreenshot + readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== "" + // Priority 1: Notification image using Quickshell IconImage (handles qs://image-X URIs) + Rectangle { + anchors.fill: parent + anchors.margins: 2 + radius: 20 + clip: true + visible: parent.hasNotificationImage && notificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: notificationImage + anchors.fill: parent + source: modelData.latestNotification.image || "" + } + } - - - // Use Material Symbols icon for screenshots with fallback + // Priority 2: Material Symbols icon for screenshots (when no image available) DankIcon { anchors.centerIn: parent name: "screenshot_monitor" size: 24 color: Theme.primaryText - visible: parent.isScreenshot + visible: parent.isScreenshot && !parent.hasNotificationImage } - // Fallback to first letter for non-screenshot notifications + // Priority 3: Fallback to first letter for other notifications Text { anchors.centerIn: parent - visible: !parent.isScreenshot + visible: !parent.hasNotificationImage && !parent.isScreenshot text: { const appName = modelData.appName || "?"; return appName.charAt(0).toUpperCase(); @@ -447,71 +461,10 @@ PanelWindow { spacing: Theme.spacingM visible: expanded - // Group header with fixed anchored positioning + // 1st tier controls - moved above group header as per mockup Item { width: parent.width - height: 48 - - // Round app icon - fixed position on left - Rectangle { - width: 40 - height: 40 - radius: 20 - anchors.left: parent.left - 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.3) - border.width: 1 - clip: true - - IconImage { - anchors.fill: parent - anchors.margins: 4 - source: { - // Don't try to load icons for screenshots - let fallback handle them - if (modelData.latestNotification.isScreenshot) { - return ""; - } - - return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : ""; - } - visible: status === Image.Ready - } - - // Material Symbols icon for screenshots in expanded view - DankIcon { - anchors.centerIn: parent - name: "screenshot_monitor" - size: 16 - color: Theme.primaryText - visible: modelData.latestNotification.isScreenshot - } - - // Fallback for expanded view - Text { - anchors.centerIn: parent - visible: !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "") - text: { - const appName = modelData.appName || "?"; - return appName.charAt(0).toUpperCase(); - } - font.pixelSize: 16 - font.weight: Font.Bold - color: Theme.primaryText - } - - } - - // App name and count badge - centered area - Text { - anchors.left: parent.left - anchors.leftMargin: 52 - anchors.verticalCenter: parent.verticalCenter - text: modelData.appName - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Bold - } + height: 32 // Controls container - fixed position on right Item { @@ -525,7 +478,7 @@ PanelWindow { height: 32 radius: 16 anchors.left: parent.left - color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + color: collapseAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" DankIcon { anchors.centerIn: parent @@ -535,7 +488,7 @@ PanelWindow { } MouseArea { - id: collapseArea + id: collapseAreaTop anchors.fill: parent hoverEnabled: true @@ -554,7 +507,7 @@ PanelWindow { height: 32 radius: 16 anchors.right: parent.right - color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + color: dismissAllAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" DankIcon { anchors.centerIn: parent @@ -564,7 +517,7 @@ PanelWindow { } MouseArea { - id: dismissAllArea + id: dismissAllAreaTop anchors.fill: parent hoverEnabled: true @@ -575,6 +528,114 @@ PanelWindow { } } + } + + // Group header with fixed anchored positioning + Item { + width: parent.width + height: 48 + + // Round app icon - fixed position on left + Rectangle { + width: 40 + height: 40 + radius: 20 + anchors.left: parent.left + 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.3) + border.width: 1 + clip: true + + readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== "" + + // Priority 1: Notification image using Quickshell IconImage + Rectangle { + anchors.fill: parent + anchors.margins: 2 + radius: 16 + clip: true + visible: parent.hasNotificationImage && expandedNotificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: expandedNotificationImage + anchors.fill: parent + source: modelData.latestNotification.image || "" + } + } + + // Priority 2: App icon for non-screenshots without notification images + IconImage { + anchors.fill: parent + anchors.margins: 4 + source: { + if (parent.hasNotificationImage || modelData.latestNotification.isScreenshot) { + return ""; + } + return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : ""; + } + visible: status === Image.Ready + } + + // Priority 3: Material Symbols icon for screenshots without notification images + DankIcon { + anchors.centerIn: parent + name: "screenshot_monitor" + size: 16 + color: Theme.primaryText + visible: modelData.latestNotification.isScreenshot && !parent.hasNotificationImage + } + + // Priority 4: Fallback text + Text { + anchors.centerIn: parent + visible: !parent.hasNotificationImage && !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "") + text: { + const appName = modelData.appName || "?"; + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: 16 + font.weight: Font.Bold + color: Theme.primaryText + } + + } + + // App name and count badge - centered area + Row { + anchors.left: parent.left + anchors.leftMargin: 52 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Text { + text: modelData.appName + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + } + + // Message count badge when expanded + Rectangle { + width: 20 + height: 20 + radius: 10 + color: Theme.primary + visible: modelData.count > 1 + anchors.verticalCenter: parent.verticalCenter + + Text { + anchors.centerIn: parent + text: modelData.count.toString() + color: Theme.primaryText + font.pixelSize: 10 + font.weight: Font.Bold + } + } + } + } @@ -619,33 +680,50 @@ PanelWindow { border.width: 1 clip: true + readonly property bool hasNotificationImage: modelData.image && modelData.image !== "" + + // Priority 1: Notification image using Quickshell IconImage + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: 14 + clip: true + visible: parent.hasNotificationImage && individualNotificationImage.status === Image.Ready + color: "transparent" + + IconImage { + id: individualNotificationImage + anchors.fill: parent + source: modelData.image || "" + } + } + + // Priority 2: App icon for non-screenshots without notification images IconImage { anchors.fill: parent anchors.margins: 3 source: { - // Don't try to load icons for screenshots - if (modelData.isScreenshot) { + if (parent.hasNotificationImage || modelData.isScreenshot) { return ""; } - return modelData.appIcon ? Quickshell.iconPath(modelData.appIcon, "") : ""; } visible: status === Image.Ready } - // Material Symbols icon for individual screenshot notifications + // Priority 3: Material Symbols icon for screenshots without notification images DankIcon { anchors.centerIn: parent name: "screenshot_monitor" size: 12 color: Theme.primaryText - visible: modelData.isScreenshot + visible: modelData.isScreenshot && !parent.hasNotificationImage } - // Fallback for individual notifications + // Priority 4: Fallback text Text { anchors.centerIn: parent - visible: !modelData.isScreenshot && (!modelData.appIcon || modelData.appIcon === "") + visible: !parent.hasNotificationImage && !modelData.isScreenshot && (!modelData.appIcon || modelData.appIcon === "") text: { const appName = modelData.appName || "?"; return appName.charAt(0).toUpperCase(); @@ -657,31 +735,64 @@ PanelWindow { } - // Individual dismiss button - fixed position on right - Rectangle { - width: 24 + // Individual controls - expand and dismiss buttons + Row { + width: 50 height: 24 - radius: 12 anchors.right: parent.right anchors.top: parent.top - color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + spacing: 2 - DankIcon { - anchors.centerIn: parent - name: "close" - size: 12 - color: Theme.surfaceVariantText + // Expand/collapse button for 2nd tier + Rectangle { + width: 24 + height: 24 + radius: 12 + color: individualExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + visible: modelData.body && modelData.body.length > 50 // Only show if body text is long enough + + property bool isExpanded: NotificationService.expandedMessages[modelData.notification.id] || false + + DankIcon { + anchors.centerIn: parent + name: parent.isExpanded ? "expand_less" : "expand_more" + size: 12 + color: Theme.surfaceVariantText + } + + MouseArea { + id: individualExpandArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id) + } } - MouseArea { - id: individualDismissArea + // Individual dismiss button + Rectangle { + width: 24 + height: 24 + radius: 12 + color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissNotification(modelData) + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: Theme.surfaceVariantText + } + + MouseArea { + id: individualDismissArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NotificationService.dismissNotification(modelData) + } } - } // Notification content - fills space between icon and dismiss button @@ -691,29 +802,42 @@ PanelWindow { anchors.left: parent.left anchors.leftMargin: 44 anchors.right: parent.right - anchors.rightMargin: 36 + anchors.rightMargin: 60 // More space for expanded controls anchors.top: parent.top spacing: Theme.spacingXS - // Title and timestamp + property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false + + // Title • timestamp format Text { - text: modelData.summary + text: { + const summary = modelData.summary || ""; + const timeStr = modelData.timeStr || ""; + if (summary && timeStr) { + return summary + " • " + timeStr; + } else if (summary) { + return summary; + } else { + return "Message • " + timeStr; + } + } color: Theme.surfaceText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium width: parent.width elide: Text.ElideRight + maximumLineCount: 1 } - // Body text + // Body text with expandable behavior Text { text: modelData.body color: Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall width: parent.width wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight + maximumLineCount: parent.isMessageExpanded ? -1 : 2 // Unlimited when expanded, 2 when collapsed + elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight visible: text.length > 0 } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 1c9711d1..b98ece3b 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -16,6 +16,7 @@ Singleton { readonly property var groupedPopups: getGroupedPopups() property var expandedGroups: ({}) // Track which groups are expanded + property var expandedMessages: ({}) // Track which individual messages are expanded NotificationServer { id: server @@ -403,6 +404,15 @@ Singleton { } } + function toggleMessageExpansion(messageId) { + let newExpandedMessages = {}; + for (const key in expandedMessages) { + newExpandedMessages[key] = expandedMessages[key]; + } + newExpandedMessages[messageId] = !newExpandedMessages[messageId]; + expandedMessages = newExpandedMessages; + } + function getGroupTitle(group) { if (group.count === 1) { return group.latestNotification.summary; diff --git a/test_notifications.sh b/test_notifications.sh new file mode 100755 index 00000000..4e5ca9d5 --- /dev/null +++ b/test_notifications.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Test script for the new 2nd tier notification system + +echo "Testing 2nd tier notification system..." + +# Send a few test notifications to create a group +notify-send "Test App" "Short message 1" +sleep 1 +notify-send "Test App" "This is a much longer message that should trigger the expand/collapse functionality for individual messages within the notification group system" +sleep 1 +notify-send "Test App" "Message 3 with some content" + +echo "Test notifications sent. Check the notification popup and center for:" +echo "1. 1st tier controls moved above group header" +echo "2. Message count badge next to app name when expanded" +echo "3. 'title • timestamp' format for individual messages" +echo "4. Expand/collapse buttons for long individual messages" \ No newline at end of file