Replay recent notification history
This commit is contained in:
@@ -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 {
|
PopupCard {
|
||||||
id: popup
|
id: popup
|
||||||
anchorItem: button
|
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) {
|
function dumpRows(rows) {
|
||||||
var values = Array.isArray(rows) ? rows : []
|
var values = Array.isArray(rows) ? rows : []
|
||||||
var out = []
|
var out = []
|
||||||
@@ -205,6 +236,7 @@ if (typeof module !== "undefined") {
|
|||||||
historyEntry: historyEntry,
|
historyEntry: historyEntry,
|
||||||
dedupeByOriginalId: dedupeByOriginalId,
|
dedupeByOriginalId: dedupeByOriginalId,
|
||||||
parseHistory: parseHistory,
|
parseHistory: parseHistory,
|
||||||
|
recentHistoryRows: recentHistoryRows,
|
||||||
dumpRows: dumpRows,
|
dumpRows: dumpRows,
|
||||||
popupPlacement: popupPlacement,
|
popupPlacement: popupPlacement,
|
||||||
imageExtension: imageExtension
|
imageExtension: imageExtension
|
||||||
|
|||||||
@@ -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 liveBarSize: shell && shell.bar && !shell.bar.barHidden ? Math.max(0, shell.bar.barSize) : defaultBarSize
|
||||||
readonly property int barClearance: liveBarSize + Style.gapsOut
|
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
|
// PersistentProperties handles in-process QML reloads. The on-disk
|
||||||
// notifications.json file is the cross-restart backstop — its `dnd` key
|
// notifications.json file is the cross-restart backstop — its `dnd` key
|
||||||
// is hydrated into persisted.doNotDisturb on startup and written back via
|
// is hydrated into persisted.doNotDisturb on startup and written back via
|
||||||
@@ -91,6 +87,7 @@ Item {
|
|||||||
ListModel { id: pastModel }
|
ListModel { id: pastModel }
|
||||||
|
|
||||||
readonly property int historyCap: 100
|
readonly property int historyCap: 100
|
||||||
|
readonly property int historyReplayLimit: 5
|
||||||
property var imageCacheQueue: []
|
property var imageCacheQueue: []
|
||||||
|
|
||||||
readonly property int lowPopupDuration: 5000
|
readonly property int lowPopupDuration: 5000
|
||||||
@@ -302,6 +299,31 @@ Item {
|
|||||||
while (popupModel.count > 0) dismissPopup(0)
|
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) {
|
function dismissPending(index) {
|
||||||
if (index < 0 || index >= pendingModel.count) return
|
if (index < 0 || index >= pendingModel.count) return
|
||||||
var entry = pendingModel.get(index)
|
var entry = pendingModel.get(index)
|
||||||
@@ -629,8 +651,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showHistory(): string {
|
function showHistory(): string {
|
||||||
service.historyOpenRequested()
|
return service.showRecentHistory()
|
||||||
return "ok"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `clear` empties the past tab (the "I already saw these" bucket).
|
// `clear` empties the past tab (the "I already saw these" bucket).
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ set -euo pipefail
|
|||||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
run_node_test <<'JS'
|
run_node_test <<'JS'
|
||||||
|
const fs = require('fs')
|
||||||
const notifications = requireFromRoot('shell/plugins/notifications/NotificationLogic.js')
|
const notifications = requireFromRoot('shell/plugins/notifications/NotificationLogic.js')
|
||||||
|
|
||||||
assert(notifications.isChromiumDerived('Brave Browser', ''), 'notifications detect chromium-derived apps by name')
|
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')
|
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/screenshot.PNG'), 'png', 'notifications normalize image extensions')
|
||||||
assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notifications default missing image extension')
|
assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notifications default missing image extension')
|
||||||
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
|
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
|
JS
|
||||||
|
|||||||
Reference in New Issue
Block a user