Fix notification centre icons and dismissal

This commit is contained in:
SirBlob committed 2026-09-24 03:12:31 +00:00
1 parent 77ba1dadff
commit ad46fe4a53
5 files changed
+50 -17

No files matched your search

+9
View File
@@ -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 accent colour. The Wallpaper tile opens `blob.displays`, the Theme tile opens
`blob theme menu`. `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 <file>`, the bell button
toggles silencing, and the trash button clears the whole history with
`blob-shell notifications clear`.
## Deliberately not ported ## Deliberately not ported
The tiles still poll shell commands the way the GTK widgets did - `pamixer` for The tiles still poll shell commands the way the GTK widgets did - `pamixer` for
@@ -1,18 +1,23 @@
.pragma library .pragma library
// Each history file is one line of JSON, so the reader concatenates the // Each history file is one line of JSON, and the reader prefixes every line
// directory and parses line by line. Newest first, by timestamp. // with the file's name and a tab so an entry can be removed later. Newest
// first, by timestamp.
function parseHistory(raw) { function parseHistory(raw) {
var lines = String(raw || "").split("\n") var lines = String(raw || "").split("\n")
var entries = [] var entries = []
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim() var line = lines[i].trim()
if (!line) continue 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 { try {
var value = JSON.parse(line) var value = JSON.parse(json)
if (!value || typeof value !== "object") continue if (!value || typeof value !== "object") continue
entries.push({ entries.push({
id: value.id || 0, id: value.id || 0,
fileName: fileName,
app: value.app || "", app: value.app || "",
summary: value.summary || "", summary: value.summary || "",
body: value.body || "", body: value.body || "",
@@ -45,8 +45,9 @@ Item {
} }
function clearAll() { function clearAll() {
root.run("blob-shell notifications clearHistory") root.run("blob-shell notifications clear")
root.entries = [] root.entries = []
refreshTimer.restart()
} }
function toggleSilence() { function toggleSilence() {
@@ -57,16 +58,19 @@ Item {
function dismissEntry(index) { function dismissEntry(index) {
var entry = root.entries[index] var entry = root.entries[index]
if (!entry) return if (!entry) return
root.run("blob-notify-dismiss " + entry.id) root.run("blob-shell notifications remove " + Util.shellQuote(entry.fileName))
var next = [] var next = []
for (var i = 0; i < root.entries.length; i++) for (var i = 0; i < root.entries.length; i++)
if (i !== index) next.push(root.entries[i]) if (i !== index) next.push(root.entries[i])
root.entries = next root.entries = next
refreshTimer.restart()
} }
Process { Process {
id: historyProbe 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 { stdout: StdioCollector {
onStreamFinished: root.entries = HistoryModel.parseHistory(text) onStreamFinished: root.entries = HistoryModel.parseHistory(text)
} }
@@ -139,13 +143,13 @@ Item {
spacing: Style.spaceReal(4) spacing: Style.spaceReal(4)
CardIconButton { CardIconButton {
icon: root.doNotDisturb ? "" : "" icon: root.doNotDisturb ? "\uf1f6" : "\uf0f3"
active: root.doNotDisturb active: root.doNotDisturb
onActivated: root.toggleSilence() onActivated: root.toggleSilence()
} }
CardIconButton { CardIconButton {
icon: "" icon: "\uf1f8"
onActivated: root.clearAll() onActivated: root.clearAll()
} }
} }
@@ -177,7 +181,7 @@ Item {
Text { Text {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: "" text: "\uf0a2"
textFormat: Text.PlainText textFormat: Text.PlainText
color: Util.alpha(Color.blue, 0.6) color: Util.alpha(Color.blue, 0.6)
font.family: Style.font.family font.family: Style.font.family
@@ -48,7 +48,7 @@ Rectangle {
Text { Text {
anchors.right: parent.right anchors.right: parent.right
text: "" text: "\uf00d"
textFormat: Text.PlainText textFormat: Text.PlainText
color: closeHover.hovered ? Color.urgent : Color.magenta color: closeHover.hovered ? Color.urgent : Color.magenta
font.family: Style.font.family font.family: Style.font.family
+22 -7
View File
@@ -26,8 +26,8 @@ Item {
readonly property string settingsPath: stateDir + "notifications.json" readonly property string settingsPath: stateDir + "notifications.json"
// One file per on-screen popup, so live toasts survive shell restarts. // 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 // 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 // toast appears, moved into historyDir when it expires unseen, and deleted
// acted upon. // when it is dismissed or acted upon.
readonly property string popupStateDir: stateDir + "notifications/" readonly property string popupStateDir: stateDir + "notifications/"
// The notifications that already left the screen, one file each, trimmed to // The notifications that already left the screen, one file each, trimmed to
// the newest historyLimit. This directory IS the history: `showHistory` // the newest historyLimit. This directory IS the history: `showHistory`
@@ -327,12 +327,14 @@ Item {
// id would dismiss that unrelated notification at the server. // id would dismiss that unrelated notification at the server.
var restored = isRestoredRow(entry) var restored = isRestoredRow(entry)
var ref = !restored && originalId >= 0 ? liveRefs[originalId] : null var ref = !restored && originalId >= 0 ? liveRefs[originalId] : null
// The popup is leaving the screen — for any reason — so its file must not // The popup is leaving the screen, so its file must not survive to the
// survive to the next shell restart. It becomes the newest history entry // next shell restart. An expired toast was missed and becomes the newest
// instead. Rows that never had a file (a history replay, the empty-history // history entry; a dismissed or acted-on one was seen and is deleted.
// placeholder) archive to nothing, which the move tolerates. // Rows that never had a file (a history replay, the empty-history
// placeholder) archive or delete nothing, which both jobs tolerate.
if (entry) { if (entry) {
archivePopupFileFor(entry) if (reason === "expire") archivePopupFileFor(entry)
else deletePopupFileFor(entry)
if (restored) delete restoredPopups[NotificationLogic.popupFileName(entry)] if (restored) delete restoredPopups[NotificationLogic.popupFileName(entry)]
} }
popupModel.remove(index) popupModel.remove(index)
@@ -585,6 +587,15 @@ Item {
enqueuePopupFileJob(command, done) 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() { function clearHistory() {
enqueuePopupFileJob(["bash", "-c", enqueuePopupFileJob(["bash", "-c",
"for f in \"$1\"/*.json; do\n" + "for f in \"$1\"/*.json; do\n" +
@@ -888,6 +899,10 @@ Item {
return "ok" return "ok"
} }
function remove(fileName: string): string {
return service.removeHistoryEntry(fileName) ? "ok" : "none"
}
function dismissAll(): string { function dismissAll(): string {
service.clearPopups() service.clearPopups()
return "ok" return "ok"