diff --git a/shell/plugins/notifications/NotificationLogic.js b/shell/plugins/notifications/NotificationLogic.js index 44b048f9..d06e6b26 100644 --- a/shell/plugins/notifications/NotificationLogic.js +++ b/shell/plugins/notifications/NotificationLogic.js @@ -56,6 +56,8 @@ function glyphFromHints(hints) { function snapshotOf(notification, timestamp) { var n = notification || {} var id = n.id || 0 + var expireTimeout = Number(n.expireTimeout || 0) + if (!isFinite(expireTimeout) || expireTimeout < 0) expireTimeout = 0 return { id: id, originalId: id, @@ -66,6 +68,7 @@ function snapshotOf(notification, timestamp) { image: n.image || "", glyph: glyphFromHints(n.hints), urgency: n.urgency, + expireTimeout: expireTimeout, timestamp: timestamp === undefined ? Date.now() : timestamp, ref: notification } @@ -83,6 +86,7 @@ function historyEntry(value, normalUrgency) { image: e.image || "", glyph: e.glyph || "", urgency: typeof e.urgency === "number" ? e.urgency : normalUrgency, + expireTimeout: 0, timestamp: e.timestamp || 0, ref: null } diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index 1f0856f8..16970294 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -92,17 +92,27 @@ Item { readonly property int historyCap: 100 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) { case NotificationUrgency.Critical: return 0 case NotificationUrgency.Low: - return 3000 + return Math.min(maxPopupDuration, Math.max(lowPopupDuration, requestedDuration(expireTimeout))) 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 // and rare. // - omarchy-action: a user-action confirmation toast ("Theme changed", @@ -237,6 +247,7 @@ Item { image: row.image, glyph: row.glyph || "", urgency: row.urgency, + expireTimeout: row.expireTimeout || 0, timestamp: row.timestamp } } @@ -257,6 +268,14 @@ Item { } function dismissPopup(index) { + removePopup(index, "dismiss") + } + + function expirePopup(index) { + removePopup(index, "expire") + } + + function removePopup(index, reason) { if (index < 0 || index >= popupModel.count) return var entry = popupModel.get(index) var ref = entry ? entry.ref : null @@ -264,7 +283,10 @@ Item { popupModel.remove(index) if (ref) { try { - if (ref.tracked) ref.dismiss() + if (ref.tracked) { + if (reason === "expire" && typeof ref.expire === "function") ref.expire() + else ref.dismiss() + } } catch (e) { // Object already torn down by the server — nothing to dismiss. } @@ -555,6 +577,7 @@ Item { image: r.image, glyph: r.glyph || "", urgency: r.urgency, + expireTimeout: r.expireTimeout || 0, timestamp: r.timestamp }) } @@ -753,6 +776,7 @@ Item { required property string image required property string glyph required property int urgency + required property double expireTimeout required property double timestamp // Each card sizes itself based on mode (text vs media); the slot @@ -761,7 +785,7 @@ Item { Layout.alignment: Qt.AlignRight 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 readonly property bool ticking: cardSlot.lifetime > 0 && !card.hovered @@ -774,7 +798,7 @@ Item { cardSlot.remainingLifetime -= 50.0 / cardSlot.lifetime if (cardSlot.remainingLifetime <= 0) { cardSlot.remainingLifetime = 0 - service.dismissPopup(cardSlot.index) + service.expirePopup(cardSlot.index) } } } diff --git a/test/shell.d/notifications-test.sh b/test/shell.d/notifications-test.sh index 928c2927..141fce0e 100644 --- a/test/shell.d/notifications-test.sh +++ b/test/shell.d/notifications-test.sh @@ -53,7 +53,8 @@ const notification = { body: 'Body', image: 'file:///tmp/mail.png', hints: { 'omarchy-glyph': '!' }, - urgency: 1 + urgency: 1, + expireTimeout: 1.5 } const snapshot = notifications.snapshotOf(notification, 12345) assertDeepEqual( @@ -67,6 +68,7 @@ assertDeepEqual( image: snapshot.image, glyph: snapshot.glyph, urgency: snapshot.urgency, + expireTimeout: snapshot.expireTimeout, timestamp: snapshot.timestamp }, { @@ -79,6 +81,7 @@ assertDeepEqual( image: 'file:///tmp/mail.png', glyph: '!', urgency: 1, + expireTimeout: 1.5, timestamp: 12345 }, 'notifications create stable snapshots'