From ea41ee4241353672d8f82267130263c557bf4d6b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 26 May 2025 09:37:57 +0200 Subject: [PATCH] notifications: timeout, prevent some warnings when dismissing notif --- .../common/widgets/NotificationGroup.qml | 22 ++-- .../common/widgets/notification_utils.js | 1 + .config/quickshell/services/Notifications.qml | 120 ++++++++++++++---- 3 files changed, 106 insertions(+), 37 deletions(-) diff --git a/.config/quickshell/modules/common/widgets/NotificationGroup.qml b/.config/quickshell/modules/common/widgets/NotificationGroup.qml index d96f2d8a8..62677ae10 100644 --- a/.config/quickshell/modules/common/widgets/NotificationGroup.qml +++ b/.config/quickshell/modules/common/widgets/NotificationGroup.qml @@ -21,7 +21,7 @@ import Quickshell.Services.Notifications Item { // Notification group area id: root property var notificationGroup - property var notifications: notificationGroup.notifications + property var notifications: notificationGroup?.notifications ?? [] property int notificationCount: notifications.length property bool multipleNotifications: notificationCount > 1 property bool expanded: false @@ -60,7 +60,9 @@ Item { // Notification group area } onFinished: () => { root.notifications.forEach((notif) => { - Notifications.discardNotification(notif.id); + Qt.callLater(() => { + Notifications.discardNotification(notif.id); + }); }); } } @@ -153,16 +155,16 @@ Item { // Notification group area NotificationAppIcon { // Icons Layout.alignment: Qt.AlignTop Layout.fillWidth: false - image: root.multipleNotifications ? "" : notificationGroup.notifications[0].image - appIcon: notificationGroup.appIcon - summary: notificationGroup.notifications[root.notificationCount - 1].summary + image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? "" + appIcon: notificationGroup?.appIcon + summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary } ColumnLayout { // Content Layout.fillWidth: true spacing: expanded ? ((root.multipleNotifications && - notificationGroup.notifications[root.notificationCount - 1].image != "") ? 35 : + notificationGroup?.notifications[root.notificationCount - 1].image != "") ? 35 : 5) : 0 Behavior on spacing { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) @@ -176,9 +178,9 @@ Item { // Notification group area StyledText { id: appName - text: topRow.showAppName ? - notificationGroup.appName : - notificationGroup.notifications[0].summary + text: (topRow.showAppName ? + notificationGroup?.appName : + notificationGroup?.notifications[0]?.summary) || "" font.pixelSize: topRow.showAppName ? topRow.fontSize : Appearance.font.pixelSize.small @@ -188,7 +190,7 @@ Item { // Notification group area } StyledText { id: timeText - text: " • " + NotificationUtils.getFriendlyNotifTimeString(notificationGroup.time) + text: " • " + NotificationUtils.getFriendlyNotifTimeString(notificationGroup?.time) font.pixelSize: topRow.fontSize color: Appearance.colors.colSubtext Layout.alignment: Qt.AlignRight diff --git a/.config/quickshell/modules/common/widgets/notification_utils.js b/.config/quickshell/modules/common/widgets/notification_utils.js index ad22a0fbb..01bf3d87f 100644 --- a/.config/quickshell/modules/common/widgets/notification_utils.js +++ b/.config/quickshell/modules/common/widgets/notification_utils.js @@ -60,6 +60,7 @@ function findSuitableMaterialSymbol(summary = "") { * @returns { string } */ const getFriendlyNotifTimeString = (timestamp) => { + if (!timestamp) return ''; const messageTime = new Date(timestamp); const now = new Date(); const oneMinuteAgo = new Date(now.getTime() - 60000); diff --git a/.config/quickshell/services/Notifications.qml b/.config/quickshell/services/Notifications.qml index 67b2351e5..1b20a0fda 100644 --- a/.config/quickshell/services/Notifications.qml +++ b/.config/quickshell/services/Notifications.qml @@ -11,11 +11,67 @@ import Qt.labs.platform Singleton { id: root + component Notif: QtObject { + required property int id + property Notification notification + property list actions: notification?.actions.map((action) => ({ + "identifier": action.identifier, + "text": action.text, + })) ?? [] + property bool popup: false + property string appIcon: notification?.appIcon ?? "" + property string appName: notification?.appName ?? "" + property string body: notification?.body ?? "" + property string image: notification?.image ?? "" + property string summary: notification?.summary ?? "" + property double time + property string urgency: notification?.urgency.toString() ?? "normal" + property Timer timer + } + + function notifToJSON(notif) { + return { + "id": notif.id, + "actions": notif.actions, + "appIcon": notif.appIcon, + "appName": notif.appName, + "body": notif.body, + "image": notif.image, + "summary": notif.summary, + "time": notif.time, + "urgency": notif.urgency, + } + } + function notifToString(notif) { + return JSON.stringify(notifToJSON(notif), null, 2); + } + + component NotifTimer: Timer { + required property int id + interval: 5000 + running: true + onTriggered: () => { + root.timeoutNotification(id); + destroy() + } + } property var filePath: `${XdgDirectories.cache}/notifications/notifications.json` - property var list: [] - property var popupList: [] + property list list: [] + property var popupList: list.filter((notif) => notif.popup); property bool popupInhibited: GlobalStates?.sidebarRightOpen ?? false property var latestTimeForApp: ({}) + Component { + id: notifComponent + Notif {} + } + Component { + id: notifTimerComponent + NotifTimer {} + } + + function stringifyList(list) { + return JSON.stringify(list.map((notif) => notifToJSON(notif)), null, 2); + } onListChanged: { // Update latest time for each app @@ -85,29 +141,25 @@ Singleton { onNotification: (notification) => { notification.tracked = true - const newNotifObject = { + const newNotifObject = notifComponent.createObject(root, { "id": notification.id + root.idOffset, - "actions": notification.actions.map((action) => { - return { - "identifier": action.identifier, - "text": action.text, - } - }), - "appIcon": notification.appIcon, - "appName": notification.appName, - "body": notification.body, - "image": notification.image, - "summary": notification.summary, + "notification": notification, "time": Date.now(), - "urgency": notification.urgency.toString(), - } + }); root.list = [...root.list, newNotifObject]; - // console.log(root.popupInhibited) + + // Popup if (!root.popupInhibited) { - root.popupList = [...root.popupList, newNotifObject]; + newNotifObject.popup = true; + newNotifObject.timer = notifTimerComponent.createObject(root, { + "id": newNotifObject.id, + "interval": notification.expireTimeout < 0 ? 5000 : notification.expireTimeout, + }); } + root.notify(newNotifObject); - notifFileView.setText(JSON.stringify(root.list, null, 2)) + // console.log(notifToString(newNotifObject)); + notifFileView.setText(stringifyList(root.list)); } } @@ -116,20 +168,19 @@ Singleton { const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id); if (index !== -1) { root.list.splice(index, 1); - notifFileView.setText(JSON.stringify(root.list, null, 2)) + notifFileView.setText(stringifyList(root.list)); triggerListChange() } if (notifServerIndex !== -1) { notifServer.trackedNotifications.values[notifServerIndex].dismiss() } - root.popupList = root.popupList.filter((notif) => notif.id !== id); root.discard(id); } function discardAllNotifications() { root.list = [] triggerListChange() - notifFileView.setText(JSON.stringify(root.list, null, 2)) + notifFileView.setText(stringifyList(root.list)); notifServer.trackedNotifications.values.forEach((notif) => { notif.dismiss() }) @@ -138,15 +189,18 @@ Singleton { function timeoutNotification(id) { const index = root.list.findIndex((notif) => notif.id === id); - root.popupList = root.popupList.filter((notif) => notif.id !== id); + if (root.list[index] != null) + root.list[index].popup = false; root.timeout(id); } function timeoutAll() { - root.list.forEach((notif) => { + root.popupList.forEach((notif) => { root.timeout(notif.id); }) - root.popupList = [] + root.popupList.forEach((notif) => { + notif.popup = false; + }); } function attemptInvokeAction(id, notifIdentifier) { @@ -177,7 +231,19 @@ Singleton { path: filePath onLoaded: { const fileContents = notifFileView.text() - root.list = JSON.parse(fileContents) + root.list = JSON.parse(fileContents).map((notif) => { + return notifComponent.createObject(root, { + "id": notif.id, + "actions": notif.actions, + "appIcon": notif.appIcon, + "appName": notif.appName, + "body": notif.body, + "image": notif.image, + "summary": notif.summary, + "time": notif.time, + "urgency": notif.urgency, + }); + }); // Find largest id let maxId = 0 root.list.forEach((notif) => { @@ -192,7 +258,7 @@ Singleton { if(error == FileViewError.FileNotFound) { console.log("[Notifications] File not found, creating new file.") root.list = [] - notifFileView.setText(JSON.stringify(root.list)) + notifFileView.setText(stringifyList(root.list)); } else { console.log("[Notifications] Error loading file: " + error) }