Extend notification popup lifetime

This commit is contained in:
David Heinemeier Hansson
2026-05-27 10:50:31 +02:00
parent ee97329aee
commit 88816ab9be
3 changed files with 38 additions and 7 deletions
@@ -56,6 +56,8 @@ function glyphFromHints(hints) {
function snapshotOf(notification, timestamp) { function snapshotOf(notification, timestamp) {
var n = notification || {} var n = notification || {}
var id = n.id || 0 var id = n.id || 0
var expireTimeout = Number(n.expireTimeout || 0)
if (!isFinite(expireTimeout) || expireTimeout < 0) expireTimeout = 0
return { return {
id: id, id: id,
originalId: id, originalId: id,
@@ -66,6 +68,7 @@ function snapshotOf(notification, timestamp) {
image: n.image || "", image: n.image || "",
glyph: glyphFromHints(n.hints), glyph: glyphFromHints(n.hints),
urgency: n.urgency, urgency: n.urgency,
expireTimeout: expireTimeout,
timestamp: timestamp === undefined ? Date.now() : timestamp, timestamp: timestamp === undefined ? Date.now() : timestamp,
ref: notification ref: notification
} }
@@ -83,6 +86,7 @@ function historyEntry(value, normalUrgency) {
image: e.image || "", image: e.image || "",
glyph: e.glyph || "", glyph: e.glyph || "",
urgency: typeof e.urgency === "number" ? e.urgency : normalUrgency, urgency: typeof e.urgency === "number" ? e.urgency : normalUrgency,
expireTimeout: 0,
timestamp: e.timestamp || 0, timestamp: e.timestamp || 0,
ref: null ref: null
} }
+30 -6
View File
@@ -92,17 +92,27 @@ Item {
readonly property int historyCap: 100 readonly property int historyCap: 100
property var imageCacheQueue: [] property var imageCacheQueue: []
function durationFor(urgency) { readonly property int lowPopupDuration: 5000
readonly property int normalPopupDuration: 8000
readonly property int maxPopupDuration: 30000
function durationFor(urgency, expireTimeout) {
switch (urgency) { switch (urgency) {
case NotificationUrgency.Critical: case NotificationUrgency.Critical:
return 0 return 0
case NotificationUrgency.Low: case NotificationUrgency.Low:
return 3000 return Math.min(maxPopupDuration, Math.max(lowPopupDuration, requestedDuration(expireTimeout)))
default: default:
return 5000 return Math.min(maxPopupDuration, Math.max(normalPopupDuration, requestedDuration(expireTimeout)))
} }
} }
function requestedDuration(expireTimeout) {
var seconds = Number(expireTimeout || 0)
if (!isFinite(seconds) || seconds <= 0) return 0
return Math.round(seconds * 1000)
}
// DND bypass: only let through notifications we trust to be intentional // DND bypass: only let through notifications we trust to be intentional
// and rare. // and rare.
// - omarchy-action: a user-action confirmation toast ("Theme changed", // - omarchy-action: a user-action confirmation toast ("Theme changed",
@@ -237,6 +247,7 @@ Item {
image: row.image, image: row.image,
glyph: row.glyph || "", glyph: row.glyph || "",
urgency: row.urgency, urgency: row.urgency,
expireTimeout: row.expireTimeout || 0,
timestamp: row.timestamp timestamp: row.timestamp
} }
} }
@@ -257,6 +268,14 @@ Item {
} }
function dismissPopup(index) { function dismissPopup(index) {
removePopup(index, "dismiss")
}
function expirePopup(index) {
removePopup(index, "expire")
}
function removePopup(index, reason) {
if (index < 0 || index >= popupModel.count) return if (index < 0 || index >= popupModel.count) return
var entry = popupModel.get(index) var entry = popupModel.get(index)
var ref = entry ? entry.ref : null var ref = entry ? entry.ref : null
@@ -264,7 +283,10 @@ Item {
popupModel.remove(index) popupModel.remove(index)
if (ref) { if (ref) {
try { try {
if (ref.tracked) ref.dismiss() if (ref.tracked) {
if (reason === "expire" && typeof ref.expire === "function") ref.expire()
else ref.dismiss()
}
} catch (e) { } catch (e) {
// Object already torn down by the server — nothing to dismiss. // Object already torn down by the server — nothing to dismiss.
} }
@@ -555,6 +577,7 @@ Item {
image: r.image, image: r.image,
glyph: r.glyph || "", glyph: r.glyph || "",
urgency: r.urgency, urgency: r.urgency,
expireTimeout: r.expireTimeout || 0,
timestamp: r.timestamp timestamp: r.timestamp
}) })
} }
@@ -753,6 +776,7 @@ Item {
required property string image required property string image
required property string glyph required property string glyph
required property int urgency required property int urgency
required property double expireTimeout
required property double timestamp required property double timestamp
// Each card sizes itself based on mode (text vs media); the slot // Each card sizes itself based on mode (text vs media); the slot
@@ -761,7 +785,7 @@ Item {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
implicitHeight: card.implicitHeight implicitHeight: card.implicitHeight
readonly property real lifetime: service.durationFor(cardSlot.urgency) readonly property real lifetime: service.durationFor(cardSlot.urgency, cardSlot.expireTimeout)
property real remainingLifetime: 1.0 property real remainingLifetime: 1.0
readonly property bool ticking: cardSlot.lifetime > 0 && !card.hovered readonly property bool ticking: cardSlot.lifetime > 0 && !card.hovered
@@ -774,7 +798,7 @@ Item {
cardSlot.remainingLifetime -= 50.0 / cardSlot.lifetime cardSlot.remainingLifetime -= 50.0 / cardSlot.lifetime
if (cardSlot.remainingLifetime <= 0) { if (cardSlot.remainingLifetime <= 0) {
cardSlot.remainingLifetime = 0 cardSlot.remainingLifetime = 0
service.dismissPopup(cardSlot.index) service.expirePopup(cardSlot.index)
} }
} }
} }
+4 -1
View File
@@ -53,7 +53,8 @@ const notification = {
body: 'Body', body: 'Body',
image: 'file:///tmp/mail.png', image: 'file:///tmp/mail.png',
hints: { 'omarchy-glyph': '!' }, hints: { 'omarchy-glyph': '!' },
urgency: 1 urgency: 1,
expireTimeout: 1.5
} }
const snapshot = notifications.snapshotOf(notification, 12345) const snapshot = notifications.snapshotOf(notification, 12345)
assertDeepEqual( assertDeepEqual(
@@ -67,6 +68,7 @@ assertDeepEqual(
image: snapshot.image, image: snapshot.image,
glyph: snapshot.glyph, glyph: snapshot.glyph,
urgency: snapshot.urgency, urgency: snapshot.urgency,
expireTimeout: snapshot.expireTimeout,
timestamp: snapshot.timestamp timestamp: snapshot.timestamp
}, },
{ {
@@ -79,6 +81,7 @@ assertDeepEqual(
image: 'file:///tmp/mail.png', image: 'file:///tmp/mail.png',
glyph: '!', glyph: '!',
urgency: 1, urgency: 1,
expireTimeout: 1.5,
timestamp: 12345 timestamp: 12345
}, },
'notifications create stable snapshots' 'notifications create stable snapshots'