refactor(notif): rename internals for clarity (popup vs history)

This commit is contained in:
Lemmy
2026-03-12 11:00:50 -04:00
parent 683e617447
commit 9d142fbaf3
5 changed files with 144 additions and 149 deletions
+3 -3
View File
@@ -43,7 +43,7 @@ NIconButton {
function computeUnreadCount() { function computeUnreadCount() {
var since = NotificationService.lastSeenTs; var since = NotificationService.lastSeenTs;
var count = 0; var count = 0;
var model = NotificationService.historyList; var model = NotificationService.historyModel;
for (var i = 0; i < model.count; i++) { for (var i = 0; i < model.count; i++) {
var item = model.get(i); var item = model.get(i);
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp; var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp;
@@ -71,8 +71,8 @@ NIconButton {
colorFg: Color.resolveColorKey(iconColorKey) colorFg: Color.resolveColorKey(iconColorKey)
border.color: Style.capsuleBorderColor border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth border.width: Style.capsuleBorderWidth
visible: !((hideWhenZero && NotificationService.historyList.count === 0) || (hideWhenZeroUnread && count === 0)) visible: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0))
opacity: !((hideWhenZero && NotificationService.historyList.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0 opacity: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0
NPopupContextMenu { NPopupContextMenu {
id: contextMenu id: contextMenu
+3 -3
View File
@@ -26,7 +26,7 @@ Variants {
required property ShellScreen modelData required property ShellScreen modelData
property ListModel notificationModel: NotificationService.activeList property ListModel notificationModel: NotificationService.popupModel
// Deferred activation to prevent re-entrant QML incubation crash. // Deferred activation to prevent re-entrant QML incubation crash.
// Direct binding to notificationModel.count would activate the Loader // Direct binding to notificationModel.count would activate the Loader
@@ -177,7 +177,7 @@ Variants {
} }
} catch (e) { } catch (e) {
// Service fallback if delegate is already invalid // Service fallback if delegate is already invalid
NotificationService.dismissActiveNotification(notificationId); NotificationService.dismissPopup(notificationId);
} }
}; };
@@ -369,7 +369,7 @@ Variants {
interval: Style.animationSlow interval: Style.animationSlow
repeat: false repeat: false
onTriggered: { onTriggered: {
NotificationService.dismissActiveNotification(notificationId); NotificationService.dismissPopup(notificationId);
} }
} }
@@ -95,7 +95,7 @@ SmartPanel {
} }
function moveSelection(dir) { function moveSelection(dir) {
var m = NotificationService.historyList; var m = NotificationService.historyModel;
if (!m || m.count === 0) if (!m || m.count === 0)
return; return;
@@ -139,7 +139,7 @@ SmartPanel {
function moveAction(dir) { function moveAction(dir) {
if (focusIndex === -1) if (focusIndex === -1)
return; return;
var item = NotificationService.historyList.get(focusIndex); var item = NotificationService.historyModel.get(focusIndex);
if (!item) if (!item)
return; return;
@@ -162,7 +162,7 @@ SmartPanel {
function activateSelection() { function activateSelection() {
if (focusIndex === -1) if (focusIndex === -1)
return; return;
var item = NotificationService.historyList.get(focusIndex); var item = NotificationService.historyModel.get(focusIndex);
if (!item) if (!item)
return; return;
@@ -189,7 +189,7 @@ SmartPanel {
function removeSelection() { function removeSelection() {
if (focusIndex === -1) if (focusIndex === -1)
return; return;
var item = NotificationService.historyList.get(focusIndex); var item = NotificationService.historyModel.get(focusIndex);
if (!item) if (!item)
return; return;
@@ -237,7 +237,7 @@ SmartPanel {
// Calculate content height based on header + tabs (if visible) + content // Calculate content height based on header + tabs (if visible) + content
property real calculatedHeight: { property real calculatedHeight: {
if (NotificationService.historyList.count === 0) { if (NotificationService.historyModel.count === 0) {
return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM;
} }
return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM;
@@ -296,7 +296,7 @@ SmartPanel {
} }
function recalcRangeCounts() { function recalcRangeCounts() {
var m = NotificationService.historyList; var m = NotificationService.historyModel;
if (!m || typeof m.count === "undefined" || m.count <= 0) { if (!m || typeof m.count === "undefined" || m.count <= 0) {
panelContent.rangeCounts = [0, 0, 0, 0]; panelContent.rangeCounts = [0, 0, 0, 0];
return; return;
@@ -328,7 +328,7 @@ SmartPanel {
} }
function hasNotificationsInCurrentRange() { function hasNotificationsInCurrentRange() {
var m = NotificationService.historyList; var m = NotificationService.historyModel;
if (!m || m.count === 0) { if (!m || m.count === 0) {
return false; return false;
} }
@@ -347,7 +347,7 @@ SmartPanel {
} }
Connections { Connections {
target: NotificationService.historyList target: NotificationService.historyModel
function onCountChanged() { function onCountChanged() {
panelContent.recalcRangeCounts(); panelContent.recalcRangeCounts();
} }
@@ -433,7 +433,7 @@ SmartPanel {
NTabBar { NTabBar {
id: tabsBox id: tabsBox
Layout.fillWidth: true Layout.fillWidth: true
visible: NotificationService.historyList.count > 0 && panelContent.groupByDate visible: NotificationService.historyModel.count > 0 && panelContent.groupByDate
currentIndex: panelContent.currentRange currentIndex: panelContent.currentRange
tabHeight: Style.toOdd(Style.baseWidgetSize * 0.8) tabHeight: Style.toOdd(Style.baseWidgetSize * 0.8)
spacing: Style.marginXS spacing: Style.marginXS
@@ -512,20 +512,20 @@ SmartPanel {
NIcon { NIcon {
icon: "bell-off" icon: "bell-off"
pointSize: (NotificationService.historyList.count === 0) ? 48 : Style.baseWidgetSize pointSize: (NotificationService.historyModel.count === 0) ? 48 : Style.baseWidgetSize
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NText { NText {
text: I18n.tr("notifications.panel.no-notifications") text: I18n.tr("notifications.panel.no-notifications")
pointSize: (NotificationService.historyList.count === 0) ? Style.fontSizeL : Style.fontSizeM pointSize: (NotificationService.historyModel.count === 0) ? Style.fontSizeL : Style.fontSizeM
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
NText { NText {
visible: NotificationService.historyList.count === 0 visible: NotificationService.historyModel.count === 0
text: I18n.tr("notifications.panel.description") text: I18n.tr("notifications.panel.description")
pointSize: Style.fontSizeS pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
@@ -552,7 +552,7 @@ SmartPanel {
spacing: Style.marginM spacing: Style.marginM
Repeater { Repeater {
model: NotificationService.historyList model: NotificationService.historyModel
delegate: Item { delegate: Item {
id: notificationDelegate id: notificationDelegate
+6 -6
View File
@@ -37,11 +37,11 @@ Singleton {
Logger.w("IPC", "Argument to ipc call '" + funcName + "' must be a number"); Logger.w("IPC", "Argument to ipc call '" + funcName + "' must be a number");
return null; return null;
} }
if (idx < 0 || idx >= NotificationService.activeList.count) { if (idx < 0 || idx >= NotificationService.popupModel.count) {
Logger.w("IPC", "Notification index out of range: " + idx); Logger.w("IPC", "Notification index out of range: " + idx);
return null; return null;
} }
return NotificationService.activeList.get(idx); return NotificationService.popupModel.get(idx);
} }
IpcHandler { IpcHandler {
@@ -191,7 +191,7 @@ Singleton {
} }
function dismissOldest() { function dismissOldest() {
NotificationService.dismissOldestActive(); NotificationService.dismissOldestPopup();
} }
function removeOldestHistory() { function removeOldestHistory() {
@@ -199,7 +199,7 @@ Singleton {
} }
function dismissAll() { function dismissAll() {
NotificationService.dismissAllActive(); NotificationService.dismissAllPopups();
} }
function getHistory(): string { function getHistory(): string {
@@ -230,13 +230,13 @@ Singleton {
var actions = JSON.parse(notif.actionsJson || "[]"); var actions = JSON.parse(notif.actionsJson || "[]");
if (actions.length === 0) { if (actions.length === 0) {
NotificationService.dismissActiveNotification(notif.id); NotificationService.dismissPopup(notif.id);
return false; return false;
} }
var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier; var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier;
var result = NotificationService.invokeAction(notif.id, actionId); var result = NotificationService.invokeAction(notif.id, actionId);
NotificationService.dismissActiveNotification(notif.id); NotificationService.dismissPopup(notif.id);
return result; return result;
} }
+119 -124
View File
@@ -17,7 +17,7 @@ Singleton {
id: root id: root
// Configuration // Configuration
property int maxVisible: 5 property int maxPopups: 5
property int maxHistory: 100 property int maxHistory: 100
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
@@ -27,11 +27,11 @@ Singleton {
property bool doNotDisturb: false property bool doNotDisturb: false
// Models // Models
property ListModel activeList: ListModel {} property ListModel popupModel: ListModel {}
property ListModel historyList: ListModel {} property ListModel historyModel: ListModel {}
// Internal state // Internal state
property var activeNotifications: ({}) // Maps internal ID to {notification, watcher, metadata} property var popupState: ({}) // Maps internal ID to {notification, watcher, cachedActions, metadata}
property var quickshellIdToInternalId: ({}) property var quickshellIdToInternalId: ({})
// Rate limiting for notification sounds (minimum 100ms between sounds) // Rate limiting for notification sounds (minimum 100ms between sounds)
@@ -167,19 +167,19 @@ Singleton {
// Check if this is a replacement notification // Check if this is a replacement notification
const existingInternalId = quickshellIdToInternalId[quickshellId]; const existingInternalId = quickshellIdToInternalId[quickshellId];
if (existingInternalId && activeNotifications[existingInternalId]) { if (existingInternalId && popupState[existingInternalId]) {
updateExistingNotification(existingInternalId, notification, data); updatePopup(existingInternalId, notification, data);
return; return;
} }
// Check for duplicate content // Check for duplicate content
const duplicateId = findDuplicateNotification(data); const duplicateId = findDuplicateNotification(data);
if (duplicateId) { if (duplicateId) {
removeNotification(duplicateId); removePopup(duplicateId);
} }
// Add new notification // Add new notification
addNewNotification(quickshellId, notification, data); addPopup(quickshellId, notification, data);
playNotificationSound(data.urgency, notification.appName); playNotificationSound(data.urgency, notification.appName);
} }
@@ -277,30 +277,30 @@ Singleton {
return defaultSoundFile; return defaultSoundFile;
} }
function updateExistingNotification(internalId, notification, data) { function updatePopup(internalId, notification, data) {
const index = findNotificationIndex(internalId); const index = findPopupIndex(internalId);
if (index < 0) if (index < 0)
return; return;
const existing = activeList.get(index); const existing = popupModel.get(index);
const oldTimestamp = existing.timestamp; const oldTimestamp = existing.timestamp;
const oldProgress = existing.progress; const oldProgress = existing.progress;
// Update properties (keeping original timestamp and progress) // Update properties (keeping original timestamp and progress)
activeList.setProperty(index, "summary", data.summary); popupModel.setProperty(index, "summary", data.summary);
activeList.setProperty(index, "summaryMarkdown", data.summaryMarkdown); popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
activeList.setProperty(index, "body", data.body); popupModel.setProperty(index, "body", data.body);
activeList.setProperty(index, "bodyMarkdown", data.bodyMarkdown); popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
activeList.setProperty(index, "appName", data.appName); popupModel.setProperty(index, "appName", data.appName);
activeList.setProperty(index, "urgency", data.urgency); popupModel.setProperty(index, "urgency", data.urgency);
activeList.setProperty(index, "expireTimeout", data.expireTimeout); popupModel.setProperty(index, "expireTimeout", data.expireTimeout);
activeList.setProperty(index, "originalImage", data.originalImage); popupModel.setProperty(index, "originalImage", data.originalImage);
activeList.setProperty(index, "cachedImage", data.cachedImage); popupModel.setProperty(index, "cachedImage", data.cachedImage);
activeList.setProperty(index, "actionsJson", data.actionsJson); popupModel.setProperty(index, "actionsJson", data.actionsJson);
activeList.setProperty(index, "timestamp", oldTimestamp); popupModel.setProperty(index, "timestamp", oldTimestamp);
activeList.setProperty(index, "progress", oldProgress); popupModel.setProperty(index, "progress", oldProgress);
// Update stored notification object // Update stored notification object
const notifData = activeNotifications[internalId]; const notifData = popupState[internalId];
notifData.notification = notification; notifData.notification = notification;
// Deep copy actions to preserve them even if QML object clears list // Deep copy actions to preserve them even if QML object clears list
@@ -319,7 +319,7 @@ Singleton {
notification.tracked = true; notification.tracked = true;
function onClosed() { function onClosed() {
userDismissNotification(internalId); dismissPopup(internalId);
} }
notification.closed.connect(onClosed); notification.closed.connect(onClosed);
notifData.onClosed = onClosed; notifData.onClosed = onClosed;
@@ -329,7 +329,7 @@ Singleton {
notifData.metadata.duration = calculateDuration(data); notifData.metadata.duration = calculateDuration(data);
} }
function addNewNotification(quickshellId, notification, data) { function addPopup(quickshellId, notification, data) {
// Map IDs // Map IDs
quickshellIdToInternalId[quickshellId] = data.id; quickshellIdToInternalId[quickshellId] = data.id;
@@ -351,7 +351,7 @@ Singleton {
} }
// Store notification data // Store notification data
activeNotifications[data.id] = { popupState[data.id] = {
"notification": notification, "notification": notification,
"watcher": watcher, "watcher": watcher,
"cachedActions": safeActions // Cache actions "cachedActions": safeActions // Cache actions
@@ -370,24 +370,24 @@ Singleton {
notification.tracked = true; notification.tracked = true;
function onClosed() { function onClosed() {
userDismissNotification(data.id); dismissPopup(data.id);
} }
notification.closed.connect(onClosed); notification.closed.connect(onClosed);
activeNotifications[data.id].onClosed = onClosed; popupState[data.id].onClosed = onClosed;
// Defer list insertion to prevent re-entrant QML incubation crash. // Defer list insertion to prevent re-entrant QML incubation crash.
// Direct insert triggers Repeater.modelUpdated synchronously, which // Direct insert triggers Repeater.modelUpdated synchronously, which
// incubates delegates whose signal handlers can re-enter the V4 engine // incubates delegates whose signal handlers can re-enter the V4 engine
// and crash in QV4::Object::insertMember. // and crash in QV4::Object::insertMember.
Qt.callLater(() => { Qt.callLater(() => {
activeList.insert(0, data); popupModel.insert(0, data);
// Remove overflow // Remove overflow
while (activeList.count > maxVisible) { while (popupModel.count > maxPopups) {
const last = activeList.get(activeList.count - 1); const last = popupModel.get(popupModel.count - 1);
// Overflow only removes from ACTIVE view, but keeps it for history // Overflow only removes from ACTIVE view, but keeps it for history
activeNotifications[last.id]?.notification?.dismiss(); // Visually dismiss popupState[last.id]?.notification?.dismiss(); // Visually dismiss
activeList.remove(activeList.count - 1); popupModel.remove(popupModel.count - 1);
// DO NOT call cleanupNotification here, we want to keep it for history actions // DO NOT call cleanupNotification here, we want to keep it for history actions
} }
}); });
@@ -396,8 +396,8 @@ Singleton {
function findDuplicateNotification(data) { function findDuplicateNotification(data) {
const contentId = getContentId(data.summary, data.body, data.appName); const contentId = getContentId(data.summary, data.body, data.appName);
for (var i = 0; i < activeList.count; i++) { for (var i = 0; i < popupModel.count; i++) {
const existing = activeList.get(i); const existing = popupModel.get(i);
const existingContentId = getContentId(existing.summary, existing.body, existing.appName); const existingContentId = getContentId(existing.summary, existing.body, existing.appName);
if (existingContentId === contentId) { if (existingContentId === contentId) {
return existing.id; return existing.id;
@@ -455,9 +455,9 @@ Singleton {
}; };
} }
function findNotificationIndex(internalId) { function findPopupIndex(internalId) {
for (var i = 0; i < activeList.count; i++) { for (var i = 0; i < popupModel.count; i++) {
if (activeList.get(i).id === internalId) { if (popupModel.get(i).id === internalId) {
return i; return i;
} }
} }
@@ -465,45 +465,45 @@ Singleton {
} }
function updateNotificationFromObject(internalId) { function updateNotificationFromObject(internalId) {
const notifData = activeNotifications[internalId]; const notifData = popupState[internalId];
if (!notifData) if (!notifData)
return; return;
const index = findNotificationIndex(internalId); const index = findPopupIndex(internalId);
if (index < 0) if (index < 0)
return; return;
const data = createData(notifData.notification); const data = createData(notifData.notification);
const existing = activeList.get(index); const existing = popupModel.get(index);
// Update properties (keeping timestamp and progress) // Update properties (keeping timestamp and progress)
activeList.setProperty(index, "summary", data.summary); popupModel.setProperty(index, "summary", data.summary);
activeList.setProperty(index, "summaryMarkdown", data.summaryMarkdown); popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown);
activeList.setProperty(index, "body", data.body); popupModel.setProperty(index, "body", data.body);
activeList.setProperty(index, "bodyMarkdown", data.bodyMarkdown); popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown);
activeList.setProperty(index, "appName", data.appName); popupModel.setProperty(index, "appName", data.appName);
activeList.setProperty(index, "urgency", data.urgency); popupModel.setProperty(index, "urgency", data.urgency);
activeList.setProperty(index, "expireTimeout", data.expireTimeout); popupModel.setProperty(index, "expireTimeout", data.expireTimeout);
activeList.setProperty(index, "originalImage", data.originalImage); popupModel.setProperty(index, "originalImage", data.originalImage);
activeList.setProperty(index, "cachedImage", data.cachedImage); popupModel.setProperty(index, "cachedImage", data.cachedImage);
activeList.setProperty(index, "actionsJson", data.actionsJson); popupModel.setProperty(index, "actionsJson", data.actionsJson);
// Update metadata // Update metadata
notifData.metadata.urgency = data.urgency; notifData.metadata.urgency = data.urgency;
notifData.metadata.duration = calculateDuration(data); notifData.metadata.duration = calculateDuration(data);
} }
function removeNotification(id) { function removePopup(id) {
const index = findNotificationIndex(id); const index = findPopupIndex(id);
if (index >= 0) { if (index >= 0) {
activeList.remove(index); popupModel.remove(index);
} }
cleanupNotification(id); cleanupNotification(id);
} }
function cleanupNotification(id) { function cleanupNotification(id) {
const notifData = activeNotifications[id]; const notifData = popupState[id];
if (notifData) { if (notifData) {
notifData.watcher?.destroy(); notifData.watcher?.destroy();
delete activeNotifications[id]; delete popupState[id];
} }
// Clean up quickshell ID mapping // Clean up quickshell ID mapping
@@ -519,7 +519,7 @@ Singleton {
Timer { Timer {
interval: 50 interval: 50
repeat: true repeat: true
running: activeList.count > 0 running: popupModel.count > 0
onTriggered: updateAllProgress() onTriggered: updateAllProgress()
} }
@@ -527,9 +527,9 @@ Singleton {
const now = Date.now(); const now = Date.now();
const toRemove = []; const toRemove = [];
for (var i = 0; i < activeList.count; i++) { for (var i = 0; i < popupModel.count; i++) {
const notif = activeList.get(i); const notif = popupModel.get(i);
const notifData = activeNotifications[notif.id]; const notifData = popupState[notif.id];
if (!notifData) if (!notifData)
continue; continue;
const meta = notifData.metadata; const meta = notifData.metadata;
@@ -541,7 +541,7 @@ Singleton {
if (progress <= 0) { if (progress <= 0) {
toRemove.push(notif.id); toRemove.push(notif.id);
} else if (Math.abs(notif.progress - progress) > 0.005) { } else if (Math.abs(notif.progress - progress) > 0.005) {
activeList.setProperty(i, "progress", progress); popupModel.setProperty(i, "progress", progress);
} }
} }
@@ -571,8 +571,8 @@ Singleton {
} }
function updateImagePath(notificationId, path) { function updateImagePath(notificationId, path) {
updateModel(activeList, notificationId, "cachedImage", path); updateModel(popupModel, notificationId, "cachedImage", path);
updateModel(historyList, notificationId, "cachedImage", path); updateModel(historyModel, notificationId, "cachedImage", path);
saveHistory(); saveHistory();
} }
@@ -588,18 +588,18 @@ Singleton {
// History management // History management
function addToHistory(data) { function addToHistory(data) {
// Defer list insertion to prevent re-entrant QML incubation crash. // Defer list insertion to prevent re-entrant QML incubation crash.
// See addNewNotification for full explanation. // See addPopup for full explanation.
Qt.callLater(() => { Qt.callLater(() => {
historyList.insert(0, data); historyModel.insert(0, data);
while (historyList.count > maxHistory) { while (historyModel.count > maxHistory) {
const old = historyList.get(historyList.count - 1); const old = historyModel.get(historyModel.count - 1);
// Only delete cached images that are in our cache directory // Only delete cached images that are in our cache directory
const cachedPath = old.cachedImage ? old.cachedImage.replace(/^file:\/\//, "") : ""; const cachedPath = old.cachedImage ? old.cachedImage.replace(/^file:\/\//, "") : "";
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
Quickshell.execDetached(["rm", "-f", cachedPath]); Quickshell.execDetached(["rm", "-f", cachedPath]);
} }
historyList.remove(historyList.count - 1); historyModel.remove(historyModel.count - 1);
} }
saveHistory(); saveHistory();
}); });
@@ -635,8 +635,8 @@ Singleton {
function performSaveHistory() { function performSaveHistory() {
try { try {
const items = []; const items = [];
for (var i = 0; i < historyList.count; i++) { for (var i = 0; i < historyModel.count; i++) {
const n = historyList.get(i); const n = historyModel.get(i);
const copy = Object.assign({}, n); const copy = Object.assign({}, n);
copy.timestamp = n.timestamp.getTime(); copy.timestamp = n.timestamp.getTime();
items.push(copy); items.push(copy);
@@ -650,7 +650,7 @@ Singleton {
function loadHistory() { function loadHistory() {
try { try {
historyList.clear(); historyModel.clear();
for (const item of adapter.notifications || []) { for (const item of adapter.notifications || []) {
const time = new Date(item.timestamp); const time = new Date(item.timestamp);
@@ -660,20 +660,20 @@ Singleton {
cachedImage = item.originalImage || ""; cachedImage = item.originalImage || "";
} }
historyList.append({ historyModel.append({
"id": item.id || "", "id": item.id || "",
"summary": item.summary || "", "summary": item.summary || "",
"summaryMarkdown": processNotificationMarkdown(item.summary || ""), "summaryMarkdown": processNotificationMarkdown(item.summary || ""),
"body": item.body || "", "body": item.body || "",
"bodyMarkdown": processNotificationMarkdown(item.body || ""), "bodyMarkdown": processNotificationMarkdown(item.body || ""),
"appName": item.appName || "", "appName": item.appName || "",
"urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency, "urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
"timestamp": time, "timestamp": time,
"originalImage": item.originalImage || "", "originalImage": item.originalImage || "",
"cachedImage": cachedImage, "cachedImage": cachedImage,
"actionsJson": item.actionsJson || "[]", "actionsJson": item.actionsJson || "[]",
"originalId": item.originalId || 0 "originalId": item.originalId || 0
}); });
} }
} catch (e) { } catch (e) {
Logger.e("Notifications", "Load failed:", e); Logger.e("Notifications", "Load failed:", e);
@@ -870,7 +870,7 @@ Singleton {
} }
function pauseTimeout(id) { function pauseTimeout(id) {
const notifData = activeNotifications[id]; const notifData = popupState[id];
if (notifData && !notifData.metadata.paused) { if (notifData && !notifData.metadata.paused) {
notifData.metadata.paused = true; notifData.metadata.paused = true;
notifData.metadata.pauseTime = Date.now(); notifData.metadata.pauseTime = Date.now();
@@ -878,46 +878,41 @@ Singleton {
} }
function resumeTimeout(id) { function resumeTimeout(id) {
const notifData = activeNotifications[id]; const notifData = popupState[id];
if (notifData && notifData.metadata.paused) { if (notifData && notifData.metadata.paused) {
notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime; notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime;
notifData.metadata.paused = false; notifData.metadata.paused = false;
} }
} }
// Public API // Dismiss a popup notification (e.g. clicked close, swipe, or overflow).
function dismissActiveNotification(id) { // Removes from popup list but KEEPS data for history.
userDismissNotification(id); function dismissPopup(id) {
} const index = findPopupIndex(id);
// User dismissed from active view (e.g. clicked close, or swipe)
// This behaves like "overflow" - removes from active list but KEEPS data for history
function userDismissNotification(id) {
const index = findNotificationIndex(id);
if (index >= 0) { if (index >= 0) {
activeList.remove(index); popupModel.remove(index);
} }
} }
function dismissOldestActive() { function dismissOldestPopup() {
if (activeList.count > 0) { if (popupModel.count > 0) {
const lastNotif = activeList.get(activeList.count - 1); const lastNotif = popupModel.get(popupModel.count - 1);
dismissActiveNotification(lastNotif.id); dismissPopup(lastNotif.id);
} }
} }
function dismissAllActive() { function dismissAllPopups() {
for (const id in activeNotifications) { for (const id in popupState) {
activeNotifications[id].notification?.dismiss(); popupState[id].notification?.dismiss();
activeNotifications[id].watcher?.destroy(); popupState[id].watcher?.destroy();
} }
activeList.clear(); popupModel.clear();
activeNotifications = {}; popupState = {};
quickshellIdToInternalId = {}; quickshellIdToInternalId = {};
} }
function invokeActionAndSuppressClose(id, actionId) { function invokeActionAndSuppressClose(id, actionId) {
const notifData = activeNotifications[id]; const notifData = popupState[id];
if (notifData && notifData.notification && notifData.onClosed) { if (notifData && notifData.notification && notifData.onClosed) {
try { try {
notifData.notification.closed.disconnect(notifData.onClosed); notifData.notification.closed.disconnect(notifData.onClosed);
@@ -929,7 +924,7 @@ Singleton {
function invokeAction(id, actionId) { function invokeAction(id, actionId) {
let invoked = false; let invoked = false;
const notifData = activeNotifications[id]; const notifData = popupState[id];
if (notifData && notifData.notification) { if (notifData && notifData.notification) {
const actionsToUse = (notifData.notification.actions && notifData.notification.actions.length > 0) ? notifData.notification.actions : (notifData.cachedActions || []); const actionsToUse = (notifData.notification.actions && notifData.notification.actions.length > 0) ? notifData.notification.actions : (notifData.cachedActions || []);
@@ -966,9 +961,9 @@ Singleton {
} }
} else if (!notifData) { } else if (!notifData) {
Logger.w("NotificationService", "No active notification data for id=" + id + ", searching history for manual invoke"); Logger.w("NotificationService", "No active notification data for id=" + id + ", searching history for manual invoke");
for (var i = 0; i < historyList.count; i++) { for (var i = 0; i < historyModel.count; i++) {
if (historyList.get(i).id === id) { if (historyModel.get(i).id === id) {
const histEntry = historyList.get(i); const histEntry = historyModel.get(i);
if (histEntry.originalId) { if (histEntry.originalId) {
invoked = manualInvoke(histEntry.originalId, actionId); invoked = manualInvoke(histEntry.originalId, actionId);
} }
@@ -983,8 +978,8 @@ Singleton {
} }
// Clear actions after use // Clear actions after use
updateModel(activeList, id, "actionsJson", "[]"); updateModel(popupModel, id, "actionsJson", "[]");
updateModel(historyList, id, "actionsJson", "[]"); updateModel(historyModel, id, "actionsJson", "[]");
saveHistory(); saveHistory();
return true; return true;
@@ -1032,15 +1027,15 @@ Singleton {
} }
function removeFromHistory(notificationId) { function removeFromHistory(notificationId) {
for (var i = 0; i < historyList.count; i++) { for (var i = 0; i < historyModel.count; i++) {
const notif = historyList.get(i); const notif = historyModel.get(i);
if (notif.id === notificationId) { if (notif.id === notificationId) {
// Only delete cached images that are in our cache directory // Only delete cached images that are in our cache directory
const cachedPath = notif.cachedImage ? notif.cachedImage.replace(/^file:\/\//, "") : ""; const cachedPath = notif.cachedImage ? notif.cachedImage.replace(/^file:\/\//, "") : "";
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
Quickshell.execDetached(["rm", "-f", cachedPath]); Quickshell.execDetached(["rm", "-f", cachedPath]);
} }
historyList.remove(i); historyModel.remove(i);
saveHistory(); saveHistory();
return true; return true;
} }
@@ -1049,14 +1044,14 @@ Singleton {
} }
function removeOldestHistory() { function removeOldestHistory() {
if (historyList.count > 0) { if (historyModel.count > 0) {
const oldest = historyList.get(historyList.count - 1); const oldest = historyModel.get(historyModel.count - 1);
// Only delete cached images that are in our cache directory // Only delete cached images that are in our cache directory
const cachedPath = oldest.cachedImage ? oldest.cachedImage.replace(/^file:\/\//, "") : ""; const cachedPath = oldest.cachedImage ? oldest.cachedImage.replace(/^file:\/\//, "") : "";
if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) {
Quickshell.execDetached(["rm", "-f", cachedPath]); Quickshell.execDetached(["rm", "-f", cachedPath]);
} }
historyList.remove(historyList.count - 1); historyModel.remove(historyModel.count - 1);
saveHistory(); saveHistory();
return true; return true;
} }
@@ -1070,14 +1065,14 @@ Singleton {
Logger.e("Notifications", "Failed to clear cache directory:", e); Logger.e("Notifications", "Failed to clear cache directory:", e);
} }
historyList.clear(); historyModel.clear();
saveHistory(); saveHistory();
} }
function getHistorySnapshot() { function getHistorySnapshot() {
const items = []; const items = [];
for (var i = 0; i < historyList.count; i++) { for (var i = 0; i < historyModel.count; i++) {
const entry = historyList.get(i); const entry = historyModel.get(i);
items.push({ items.push({
"id": entry.id, "id": entry.id,
"summary": entry.summary, "summary": entry.summary,