diff --git a/docs/widgets.md b/docs/widgets.md index b356c56..31d957b 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -53,6 +53,15 @@ border, so they read against the panel in any theme. An active tile takes the accent colour. The Wallpaper tile opens `blob.displays`, the Theme tile opens `blob theme menu`. +## Notification centre + +`blob.notification-center` lists the notification history, newest first. A +toast that expires unseen moves into history; a toast you close or click is +deleted instead, so the centre only holds what you missed. The x on an entry +removes it through `blob-shell notifications remove `, the bell button +toggles silencing, and the trash button clears the whole history with +`blob-shell notifications clear`. + ## Deliberately not ported The tiles still poll shell commands the way the GTK widgets did - `pamixer` for diff --git a/shell/plugins/notification-center/HistoryModel.js b/shell/plugins/notification-center/HistoryModel.js index 49a93ad..536c939 100644 --- a/shell/plugins/notification-center/HistoryModel.js +++ b/shell/plugins/notification-center/HistoryModel.js @@ -1,18 +1,23 @@ .pragma library -// Each history file is one line of JSON, so the reader concatenates the -// directory and parses line by line. Newest first, by timestamp. +// Each history file is one line of JSON, and the reader prefixes every line +// with the file's name and a tab so an entry can be removed later. Newest +// first, by timestamp. function parseHistory(raw) { var lines = String(raw || "").split("\n") var entries = [] for (var i = 0; i < lines.length; i++) { var line = lines[i].trim() if (!line) continue + var tab = line.indexOf("\t") + var fileName = tab >= 0 ? line.slice(0, tab) : "" + var json = tab >= 0 ? line.slice(tab + 1) : line try { - var value = JSON.parse(line) + var value = JSON.parse(json) if (!value || typeof value !== "object") continue entries.push({ id: value.id || 0, + fileName: fileName, app: value.app || "", summary: value.summary || "", body: value.body || "", diff --git a/shell/plugins/notification-center/NotificationCenter.qml b/shell/plugins/notification-center/NotificationCenter.qml index d6797f3..59bc74b 100644 --- a/shell/plugins/notification-center/NotificationCenter.qml +++ b/shell/plugins/notification-center/NotificationCenter.qml @@ -45,8 +45,9 @@ Item { } function clearAll() { - root.run("blob-shell notifications clearHistory") + root.run("blob-shell notifications clear") root.entries = [] + refreshTimer.restart() } function toggleSilence() { @@ -57,16 +58,19 @@ Item { function dismissEntry(index) { var entry = root.entries[index] if (!entry) return - root.run("blob-notify-dismiss " + entry.id) + root.run("blob-shell notifications remove " + Util.shellQuote(entry.fileName)) var next = [] for (var i = 0; i < root.entries.length; i++) if (i !== index) next.push(root.entries[i]) root.entries = next + refreshTimer.restart() } Process { id: historyProbe - command: ["bash", "-c", "cat " + root.historyDir + "/*.json 2>/dev/null || true"] + command: ["bash", "-c", + "for f in \"$1\"/*.json; do [[ -e $f ]] || continue; printf '%s\\t' \"${f##*/}\"; awk 1 \"$f\"; done", + "--", root.historyDir] stdout: StdioCollector { onStreamFinished: root.entries = HistoryModel.parseHistory(text) } @@ -139,13 +143,13 @@ Item { spacing: Style.spaceReal(4) CardIconButton { - icon: root.doNotDisturb ? "" : "" + icon: root.doNotDisturb ? "\uf1f6" : "\uf0f3" active: root.doNotDisturb onActivated: root.toggleSilence() } CardIconButton { - icon: "" + icon: "\uf1f8" onActivated: root.clearAll() } } @@ -177,7 +181,7 @@ Item { Text { anchors.horizontalCenter: parent.horizontalCenter - text: "" + text: "\uf0a2" textFormat: Text.PlainText color: Util.alpha(Color.blue, 0.6) font.family: Style.font.family diff --git a/shell/plugins/notification-center/NotificationItem.qml b/shell/plugins/notification-center/NotificationItem.qml index e0c6f17..92d2f91 100644 --- a/shell/plugins/notification-center/NotificationItem.qml +++ b/shell/plugins/notification-center/NotificationItem.qml @@ -48,7 +48,7 @@ Rectangle { Text { anchors.right: parent.right - text: "" + text: "\uf00d" textFormat: Text.PlainText color: closeHover.hovered ? Color.urgent : Color.magenta font.family: Style.font.family diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index 314631c..c0c1422 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -26,8 +26,8 @@ Item { readonly property string settingsPath: stateDir + "notifications.json" // One file per on-screen popup, so live toasts survive shell restarts. // A file exists exactly as long as its popup is showing: written when the - // toast appears, moved into historyDir when it expires, is dismissed, or is - // acted upon. + // toast appears, moved into historyDir when it expires unseen, and deleted + // when it is dismissed or acted upon. readonly property string popupStateDir: stateDir + "notifications/" // The notifications that already left the screen, one file each, trimmed to // the newest historyLimit. This directory IS the history: `showHistory` @@ -327,12 +327,14 @@ Item { // id would dismiss that unrelated notification at the server. var restored = isRestoredRow(entry) var ref = !restored && originalId >= 0 ? liveRefs[originalId] : null - // The popup is leaving the screen — for any reason — so its file must not - // survive to the next shell restart. It becomes the newest history entry - // instead. Rows that never had a file (a history replay, the empty-history - // placeholder) archive to nothing, which the move tolerates. + // The popup is leaving the screen, so its file must not survive to the + // next shell restart. An expired toast was missed and becomes the newest + // history entry; a dismissed or acted-on one was seen and is deleted. + // Rows that never had a file (a history replay, the empty-history + // placeholder) archive or delete nothing, which both jobs tolerate. if (entry) { - archivePopupFileFor(entry) + if (reason === "expire") archivePopupFileFor(entry) + else deletePopupFileFor(entry) if (restored) delete restoredPopups[NotificationLogic.popupFileName(entry)] } popupModel.remove(index) @@ -585,6 +587,15 @@ Item { enqueuePopupFileJob(command, done) } + function removeHistoryEntry(fileName) { + var name = String(fileName || "") + if (!/^[A-Za-z0-9._-]+\.json$/.test(name)) return false + enqueuePopupFileJob(["bash", "-c", + "rm -f \"$1/$2\" \"$3/${2%.json}\"-*", "--", + historyDir, name, imagesDir]) + return true + } + function clearHistory() { enqueuePopupFileJob(["bash", "-c", "for f in \"$1\"/*.json; do\n" + @@ -888,6 +899,10 @@ Item { return "ok" } + function remove(fileName: string): string { + return service.removeHistoryEntry(fileName) ? "ok" : "none" + } + function dismissAll(): string { service.clearPopups() return "ok"