Persist notification images so history keeps avatars (#6804)
* Persist notification images so history keeps avatars Persisted popup and history entries stored image/appIcon as URLs into resources that die with the live notification: Chromium-family senders (every Omarchy web app, WhatsApp included) pass avatars as files in a scoped /tmp dir deleted when the notification closes, and raw image-data hints surface as in-process image:// URLs that die with the server object. Replaying history then found dead references and hid the icon. Copy file-backed images into the notification state dir when persisting, keyed by the entry's file stem, and reference the copies from the JSON. Blank dead image:// URLs so the card falls back to the app icon. The copies die with their JSON: superseded-popup deletes, history trims and clears remove them, and a startup sweep collects copies orphaned by a restart killing a queued job mid-write. Hold DND-silenced notifications open until their history write has run, since untracking tells the sender to delete its avatar file, and carry replayed on-screen rows over via their persisted copies, since the replay dismisses their live notifications first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Coalesce silenced updates and bound image copies through temp files A replaces_id update lands on a held DND notification without a second onNotification, so releasing after the first write could persist a stale snapshot. Re-snapshot when the write completes and write again until the content is stable, reusing the original file identity. The image copy reopened the sender-controlled path after checking it, so a file growing or becoming a FIFO mid-copy defeated the size bound. Read through head -c under a timeout into a temp file, validate its size, and rename it into place; the startup sweep clears temp files a killed job leaves behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
edcbcb4691
commit
b2207c3357
@@ -186,8 +186,59 @@ function popupEntry(value, normalUrgency) {
|
||||
}
|
||||
|
||||
function popupFileName(entry) {
|
||||
return imageStem(entry) + ".json"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------- persisted images
|
||||
//
|
||||
// A notification's images only exist while it is live: Chromium-family
|
||||
// senders (all Omarchy web apps) delete their scoped /tmp files on close,
|
||||
// and image-data hints surface as in-process image:// URLs that die with
|
||||
// the server object. Persisted entries therefore reference their own
|
||||
// copies, named by the entry's file stem so cleanup can find them from
|
||||
// the JSON file name alone.
|
||||
|
||||
var PERSISTED_IMAGE_ROLES = ["appIcon", "image"]
|
||||
|
||||
function imageStem(entry) {
|
||||
var e = entry || {}
|
||||
return String(e.timestamp || 0) + "-" + String(e.originalId || 0) + ".json"
|
||||
return String(e.timestamp || 0) + "-" + String(e.originalId || 0)
|
||||
}
|
||||
|
||||
// The filesystem path behind a file-backed image value, or "" for anything
|
||||
// a copy can't capture: themed icon names, in-process image:// URLs, empty.
|
||||
function localImageFile(value) {
|
||||
var s = String(value || "")
|
||||
if (s.indexOf("file://") === 0) {
|
||||
s = s.slice(7)
|
||||
try { s = decodeURIComponent(s) } catch (e) {}
|
||||
}
|
||||
return s.charAt(0) === "/" ? s : ""
|
||||
}
|
||||
|
||||
// The entry as it should hit the disk, plus the copies that make it true.
|
||||
// File-backed images redirect to their copy under imagesDir; dead image://
|
||||
// URLs drop to "" (the card falls back to the app icon). Already-redirected
|
||||
// values map onto themselves and produce no copy, keeping restores no-ops.
|
||||
function persistablePopup(entry, imagesDir) {
|
||||
var e = entry || {}
|
||||
var out = {}
|
||||
for (var key in e) out[key] = e[key]
|
||||
var copies = []
|
||||
for (var i = 0; i < PERSISTED_IMAGE_ROLES.length; i++) {
|
||||
var role = PERSISTED_IMAGE_ROLES[i]
|
||||
var value = String(out[role] || "")
|
||||
if (!value) continue
|
||||
var source = localImageFile(value)
|
||||
if (source) {
|
||||
var copy = String(imagesDir || "") + imageStem(e) + "-" + role
|
||||
if (source !== copy) copies.push({ from: source, to: copy })
|
||||
out[role] = "file://" + copy
|
||||
} else if (value.indexOf("image://") === 0) {
|
||||
out[role] = ""
|
||||
}
|
||||
}
|
||||
return { entry: out, copies: copies }
|
||||
}
|
||||
|
||||
function serializePopup(entry, normalUrgency) {
|
||||
@@ -306,6 +357,9 @@ if (typeof module !== "undefined") {
|
||||
historyRows: historyRows,
|
||||
popupEntry: popupEntry,
|
||||
popupFileName: popupFileName,
|
||||
imageStem: imageStem,
|
||||
localImageFile: localImageFile,
|
||||
persistablePopup: persistablePopup,
|
||||
serializePopup: serializePopup,
|
||||
parsePopupFiles: parsePopupFiles,
|
||||
popupExpired: popupExpired,
|
||||
|
||||
Reference in New Issue
Block a user