Replay recent notification history

This commit is contained in:
David Heinemeier Hansson
2026-06-29 15:19:32 -05:00
parent 6ef0c0192b
commit beff26d8e0
4 changed files with 97 additions and 17 deletions
-11
View File
@@ -91,17 +91,6 @@ BarWidget {
}
}
// Service-side IPC (omarchy-shell notifications showHistory) flips
// historyOpenRequested; we toggle our local popup state from here so the
// keybind path lands in the same PopupCard the click path uses.
Connections {
target: root.notificationService
ignoreUnknownSignals: true
function onHistoryOpenRequested() {
root.popupOpen = true
}
}
PopupCard {
id: popup
anchorItem: button
@@ -143,6 +143,37 @@ function parseHistory(raw, normalUrgency, historyCap) {
}
}
function recentHistoryRows(pending, past, limit, normalUrgency) {
var max = limit === undefined || limit === null ? 5 : Number(limit)
if (isNaN(max)) max = 5
max = Math.max(0, max)
var values = []
function collect(rows) {
var source = Array.isArray(rows) ? rows : []
for (var i = 0; i < source.length; i++) {
if (source[i]) values.push(source[i])
}
}
collect(pending)
collect(past)
var keep = {}
for (var j = 0; j < values.length; j++) {
var row = values[j]
var key = row.originalId
if (key === undefined || key === null) key = row.id
if (key === undefined || key === null) key = "_" + j
var prior = keep[key]
if (!prior || (row.timestamp || 0) >= (prior.timestamp || 0)) keep[key] = row
}
var out = []
for (var id in keep) out.push(historyEntry(keep[id], normalUrgency))
out.sort(function(a, b) { return (b.timestamp || 0) - (a.timestamp || 0) })
return out.slice(0, max)
}
function dumpRows(rows) {
var values = Array.isArray(rows) ? rows : []
var out = []
@@ -205,6 +236,7 @@ if (typeof module !== "undefined") {
historyEntry: historyEntry,
dedupeByOriginalId: dedupeByOriginalId,
parseHistory: parseHistory,
recentHistoryRows: recentHistoryRows,
dumpRows: dumpRows,
popupPlacement: popupPlacement,
imageExtension: imageExtension
+27 -6
View File
@@ -43,10 +43,6 @@ Item {
readonly property int liveBarSize: shell && shell.bar && !shell.bar.barHidden ? Math.max(0, shell.bar.barSize) : defaultBarSize
readonly property int barClearance: liveBarSize + Style.gapsOut
// Fired by IPC (`omarchy-shell notifications showHistory`) so the
// bar widget can drop its PopupCard from the same anchor a click would.
signal historyOpenRequested()
// PersistentProperties handles in-process QML reloads. The on-disk
// notifications.json file is the cross-restart backstop — its `dnd` key
// is hydrated into persisted.doNotDisturb on startup and written back via
@@ -91,6 +87,7 @@ Item {
ListModel { id: pastModel }
readonly property int historyCap: 100
readonly property int historyReplayLimit: 5
property var imageCacheQueue: []
readonly property int lowPopupDuration: 5000
@@ -302,6 +299,31 @@ Item {
while (popupModel.count > 0) dismissPopup(0)
}
function rowsFromModel(model) {
var rows = []
for (var i = 0; i < model.count; i++) {
var entry = model.get(i)
if (entry) rows.push(snapshotFromRow(entry))
}
return rows
}
function showRecentHistory() {
var rows = NotificationLogic.recentHistoryRows(
rowsFromModel(pendingModel),
rowsFromModel(pastModel),
service.historyReplayLimit,
NotificationUrgency.Normal)
if (rows.length === 0) return "none"
clearPopups()
for (var i = 0; i < rows.length; i++) {
popupModel.append(rows[i])
}
return "ok"
}
function dismissPending(index) {
if (index < 0 || index >= pendingModel.count) return
var entry = pendingModel.get(index)
@@ -629,8 +651,7 @@ Item {
}
function showHistory(): string {
service.historyOpenRequested()
return "ok"
return service.showRecentHistory()
}
// `clear` empties the past tab (the "I already saw these" bucket).
+38
View File
@@ -5,6 +5,7 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const notifications = requireFromRoot('shell/plugins/notifications/NotificationLogic.js')
assert(notifications.isChromiumDerived('Brave Browser', ''), 'notifications detect chromium-derived apps by name')
@@ -159,7 +160,44 @@ assertDeepEqual(
)
assert(notifications.parseHistory('{', 1, 100).error, 'notifications flag invalid history JSON')
const recentRows = notifications.recentHistoryRows(
[
{ id: 1, originalId: 10, summary: 'pending-old', timestamp: 100 },
{ id: 2, originalId: 11, summary: 'pending-new', timestamp: 700 },
{ id: 3, originalId: 12, summary: 'pending-mid', timestamp: 300 }
],
[
{ id: 4, originalId: 13, summary: 'past-newest', timestamp: 900 },
{ id: 5, originalId: 14, summary: 'past-second', timestamp: 800 },
{ id: 6, originalId: 10, summary: 'past-replaced', timestamp: 200 },
{ id: 7, originalId: 15, summary: 'past-extra', timestamp: 50 }
],
5,
1
)
assertDeepEqual(
recentRows.map(row => row.summary),
['past-newest', 'past-second', 'pending-new', 'pending-mid', 'past-replaced'],
'notifications pick the last five history rows across pending and past'
)
assertEqual(recentRows.length, 5, 'notifications history replay is capped at five rows')
assertEqual(notifications.imageExtension('/tmp/screenshot.PNG'), 'png', 'notifications normalize image extensions')
assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notifications default missing image extension')
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
const barWidgetQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/BarWidget.qml'), 'utf8')
assert(
/readonly property int historyReplayLimit: 5/.test(serviceQml),
'notifications service limits history replay to five rows'
)
assert(
/function showHistory\(\): string \{\s*return service\.showRecentHistory\(\)\s*\}/.test(serviceQml),
'notifications history IPC replays recent notifications'
)
assert(
!barWidgetQml.includes('historyOpenRequested'),
'notifications history IPC does not depend on the bar widget'
)
JS