diff --git a/Modules/Notifications/NotificationPopup.qml b/Modules/Notifications/NotificationPopup.qml index c88abe8a..ee4bf175 100644 --- a/Modules/Notifications/NotificationPopup.qml +++ b/Modules/Notifications/NotificationPopup.qml @@ -19,7 +19,7 @@ PanelWindow { WlrLayershell.keyboardFocus: WlrKeyboardFocus.None color: "transparent" implicitWidth: 400 - implicitHeight: 116 + implicitHeight: 122 anchors { top: true @@ -138,13 +138,12 @@ PanelWindow { anchors.right: parent.right anchors.topMargin: 12 anchors.leftMargin: 16 - anchors.rightMargin: 16 - height: 86 + anchors.rightMargin: 56 + height: 98 Rectangle { id: iconContainer readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" - readonly property bool appIconIsImage: notificationData && notificationData.appIcon && (notificationData.appIcon.startsWith("file://") || notificationData.appIcon.startsWith("http://") || notificationData.appIcon.startsWith("https://")) property alias iconImage: iconImage width: 55 @@ -196,89 +195,169 @@ PanelWindow { id: textContainer anchors.left: iconContainer.right anchors.leftMargin: 12 - anchors.right: closeButton.left - anchors.rightMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 0 anchors.top: parent.top anchors.bottom: parent.bottom anchors.bottomMargin: 8 color: "transparent" - Column { + Item { width: parent.width - spacing: 2 - anchors.verticalCenter: parent.verticalCenter + height: parent.height + anchors.top: parent.top + anchors.topMargin: -4 - Text { + Column { width: parent.width - text: { - if (!notificationData) return ""; - const appName = notificationData.appName || ""; - const timeStr = notificationData.timeStr || ""; - if (timeStr.length > 0) - return appName + " • " + timeStr; - else - return appName; - } - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - } + spacing: 2 - Text { - text: notificationData ? (notificationData.summary || "") : "" - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - width: parent.width - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } - - Text { - property bool hasUrls: { - if (!notificationData || !notificationData.body) return false; - const urlRegex = /(https?:\/\/[^\s]+)/g; - return urlRegex.test(notificationData.body); + Text { + width: parent.width + text: { + if (!notificationData) return ""; + const appName = notificationData.appName || ""; + const timeStr = notificationData.timeStr || ""; + if (timeStr.length > 0) + return appName + " • " + timeStr; + else + return appName; + } + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 } - text: { - if (!notificationData || !notificationData.body) return ""; - let bodyText = notificationData.body; - if (bodyText.length > 105) - bodyText = bodyText.substring(0, 102) + "..."; - - const urlRegex = /(https?:\/\/[^\s]+)/g; - return bodyText.replace(urlRegex, '$1'); + Text { + text: notificationData ? (notificationData.summary || "") : "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + width: parent.width + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 } - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - width: parent.width - elide: Text.ElideRight - maximumLineCount: 2 - wrapMode: Text.WordWrap - visible: text.length > 0 - textFormat: Text.RichText - onLinkActivated: function(link) { - Qt.openUrlExternally(link); + + Text { + text: notificationData ? (notificationData.body || "") : "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.WordWrap + visible: text.length > 0 + textFormat: Text.PlainText } } } } - DankActionButton { - id: closeButton - anchors.right: parent.right - anchors.top: parent.top - iconName: "close" - iconSize: 14 - buttonSize: 20 - z: 15 + + } + + DankActionButton { + id: closeButton + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 12 + anchors.rightMargin: 16 + iconName: "close" + iconSize: 18 + buttonSize: 28 + z: 15 + onClicked: { + if (notificationData) + notificationData.popup = false; + } + } + + Row { + anchors.right: dismissButton.left + anchors.rightMargin: 8 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + spacing: 8 + z: 20 + + Repeater { + model: notificationData ? (notificationData.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 + acceptedButtons: Qt.LeftButton + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke(); + } + if (notificationData) { + notificationData.popup = false; + } + } + } + } + } + } + + Rectangle { + id: dismissButton + property bool isHovered: false + + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + 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" + z: 20 + + Text { + id: dismissText + text: "Dismiss" + color: dismissButton.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 + acceptedButtons: Qt.LeftButton + onEntered: dismissButton.isHovered = true + onExited: dismissButton.isHovered = false onClicked: { - if (notificationData) - notificationData.popup = false; + if (notificationData) { + NotificationService.dismissNotification(notificationData); + } } } } @@ -289,7 +368,7 @@ PanelWindow { hoverEnabled: true acceptedButtons: Qt.LeftButton propagateComposedEvents: true - z: 0 + z: -1 onEntered: { if (notificationData && notificationData.timer) notificationData.timer.stop(); diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 319e5bea..a443442f 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -21,7 +21,8 @@ Singleton { property bool addGateBusy: false property int enterAnimMs: 400 property int seqCounter: 0 - + property bool bulkDismissing: false + Timer { id: addGate interval: enterAnimMs + 50 @@ -34,6 +35,7 @@ Singleton { readonly property var groupedNotifications: getGroupedNotifications() readonly property var groupedPopups: getGroupedPopups() + property var expandedGroups: ({}) property var expandedMessages: ({}) property bool popupsDisabled: false @@ -149,24 +151,20 @@ Singleton { function onDropped(): void { const notifIndex = root.notifications.indexOf(wrapper); const allIndex = root.allWrappers.indexOf(wrapper); + if (allIndex !== -1) root.allWrappers.splice(allIndex, 1); + if (notifIndex !== -1) root.notifications.splice(notifIndex, 1); + + if (root.bulkDismissing) return; + + const groupKey = getGroupKey(wrapper); + const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey); - if (allIndex !== -1) { - root.allWrappers.splice(allIndex, 1); + // Only collapse the group if there's 1 or fewer notifications left + if (remainingInGroup.length <= 1) { + clearGroupExpansionState(groupKey); } - if (notifIndex !== -1) { - const groupKey = getGroupKey(wrapper); - root.notifications.splice(notifIndex, 1); - - const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey); - if (remainingInGroup.length === 0) { - clearGroupExpansionState(groupKey); - } else if (remainingInGroup.length === 1) { - clearGroupExpansionState(groupKey); - } - - cleanupExpansionStates(); - } + cleanupExpansionStates(); } function onAboutToDestroy(): void { @@ -182,17 +180,34 @@ Singleton { // Helper functions function clearAllNotifications() { - // Actually dismiss all notifications from center - const notificationsCopy = [...root.notifications]; - notificationsCopy.forEach(notif => { - notif.notification.dismiss(); - }); - // Clear all expansion states + bulkDismissing = true; + popupsDisabled = true; + addGate.stop(); + addGateBusy = false; + notificationQueue = []; + + for (const w of visibleNotifications) w.popup = false; + visibleNotifications = []; + + const toDismiss = notifications.slice(); + + if (notifications.length) notifications.splice(0, notifications.length); expandedGroups = {}; expandedMessages = {}; + + for (let i = 0; i < toDismiss.length; ++i) { + const w = toDismiss[i]; + if (w && w.notification) { + try { w.notification.dismiss(); } catch (e) { /* ignore */ } + } + } + + bulkDismissing = false; + popupsDisabled = false; } function dismissNotification(wrapper) { + wrapper.popup = false; wrapper.notification.dismiss(); } @@ -367,6 +382,7 @@ Singleton { } } + function clearGroupExpansionState(groupKey) { let newExpandedGroups = {}; for (const key in expandedGroups) { @@ -377,6 +393,7 @@ Singleton { expandedGroups = newExpandedGroups; } + function cleanupExpansionStates() { const currentGroupKeys = new Set(groupedNotifications.map(g => g.key)); const currentMessageIds = new Set(); @@ -464,6 +481,7 @@ Singleton { notif.body.toLowerCase().includes(searchLower) ); } + Component.onCompleted: { cleanupPersistentStorage(); }