From 9008d6bab9db686daf6ef6ae6d31915c5a36089c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 12:16:30 +0200 Subject: [PATCH] Add notification history --- Modules/Bar/Bar.qml | 5 + Modules/Notification/Notification.qml | 4 +- Modules/Notification/NotificationHistory.qml | 180 +++++++++++++++++++ Services/NotificationService.qml | 101 +++++++++++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 Modules/Notification/NotificationHistory.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9ad9815..a494664 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import Quickshell import qs.Services import qs.Widgets +import qs.Modules.Notification Variants { model: Quickshell.screens @@ -83,6 +84,10 @@ Variants { } // TODO: Notification Icon + NotificationHistory { + anchors.verticalCenter: parent.verticalCenter + } + WiFi { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 40a3cff..36ae1da 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -24,8 +24,8 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusionMode: ExclusionMode.Ignore - // Use the notification service - property var notificationService: NotificationService {} + // Use the notification service singleton + property var notificationService: NotificationService // Access the notification model from the service property ListModel notificationModel: notificationService.notificationModel diff --git a/Modules/Notification/NotificationHistory.qml b/Modules/Notification/NotificationHistory.qml new file mode 100644 index 0000000..80fa1d6 --- /dev/null +++ b/Modules/Notification/NotificationHistory.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property real scaling: Scaling.scale(screen) + sizeMultiplier: 0.8 + showBorder: false + icon: "notifications" + tooltipText: "Notification History" + onClicked: { + notificationHistoryLoader.active = !notificationHistoryLoader.active + } + + // Loader for Notification History menu + NLoader { + id: notificationHistoryLoader + active: false + + content: Component { + NPanel { + id: notificationPanel + + Connections { + target: notificationPanel + ignoreUnknownSignals: true + function onDismissed() { + notificationHistoryLoader.active = false + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "notifications" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentPrimary + } + + NText { + text: "Notification History" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "delete" + sizeMultiplier: 0.8 + tooltipText: "Clear history" + onClicked: NotificationService.clearHistory() + } + + NIconButton { + icon: "close" + sizeMultiplier: 0.8 + onClicked: { + notificationHistoryLoader.active = false + } + } + } + + NDivider {} + + ListView { + id: notificationList + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.historyModel + spacing: Style.marginMedium * scaling + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Rectangle { + width: notificationList ? (notificationList.width - 20) : 380 * scaling + height: 80 + radius: Style.radiusMedium * scaling + color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" + + RowLayout { + anchors { + fill: parent + margins: 15 + } + spacing: 15 + + + + // Notification content + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 5 + + NText { + text: (summary || "No summary").substring(0, 100) + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Font.Medium + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 2 + elide: Text.ElideRight + } + + NText { + text: (body || "").substring(0, 150) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: NotificationService.formatTimestamp(timestamp) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } + } + + + } + + MouseArea { + id: notificationMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + console.log("[NotificationHistory] Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() + } + } + } + + ScrollBar.vertical: ScrollBar { + active: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 74533bc..9c80c2c 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -1,6 +1,9 @@ import QtQuick +import Quickshell +import Quickshell.Io import qs.Services import Quickshell.Services.Notifications +pragma Singleton QtObject { id: root @@ -34,12 +37,45 @@ QtObject { // Add to our model root.addNotification(notification) + // Also add to history + root.addToHistory(notification) } } // List model to hold notifications property ListModel notificationModel: ListModel {} + // Persistent history of notifications (most recent first) + property ListModel historyModel: ListModel {} + property int maxHistory: 100 + + // Cached history file path + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + + // Persisted storage for history + property FileView historyFileView: FileView { + id: historyFileView + objectName: "notificationHistoryFileView" + path: historyFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: reload() + onLoaded: loadFromHistory() + onLoadFailed: function (error) { + // Create file on first use + if (error.toString().includes("No such file") || error === 2) { + writeAdapter() + } + } + + JsonAdapter { + id: historyAdapter + property var history: [] + property double timestamp: 0 + } + } + // Maximum visible notifications property int maxVisible: 5 @@ -84,6 +120,71 @@ QtObject { } } + // Add a simplified copy into persistent history + function addToHistory(notification) { + historyModel.insert(0, { + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) + while (historyModel.count > maxHistory) { + historyModel.remove(historyModel.count - 1) + } + saveHistory() + } + + function clearHistory() { + historyModel.clear() + saveHistory() + } + + function loadFromHistory() { + // Populate in-memory model from adapter + try { + historyModel.clear() + const items = historyAdapter.history || [] + for (var i = 0; i < items.length; i++) { + const it = items[i] + historyModel.append({ + "summary": it.summary || "", + "body": it.body || "", + "appName": it.appName || "", + "urgency": it.urgency, + "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() + }) + } + } catch (e) { + console.error("[Notifications] Failed to load history:", e) + } + } + + function saveHistory() { + try { + // Serialize model back to adapter + var arr = [] + for (var i = 0; i < historyModel.count; i++) { + const n = historyModel.get(i) + arr.push({ + summary: n.summary, + body: n.body, + appName: n.appName, + urgency: n.urgency, + timestamp: (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp + }) + } + historyAdapter.history = arr + historyAdapter.timestamp = Time.timestamp + + Qt.callLater(function () { + historyFileView.writeAdapter() + }) + } catch (e) { + console.error("[Notifications] Failed to save history:", e) + } + } + // Signal to trigger animation before removal signal animateAndRemove(var notification, int index)