diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Services/NotificationService.qml')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Services/NotificationService.qml | 1237 |
1 files changed, 0 insertions, 1237 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Services/NotificationService.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Services/NotificationService.qml deleted file mode 100644 index 0615b58..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Services/NotificationService.qml +++ /dev/null @@ -1,1237 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Services.Notifications -import qs.Common -import "../Common/markdown2html.js" as Markdown2Html - -Singleton { - id: root - - readonly property list<NotifWrapper> notifications: [] - readonly property list<NotifWrapper> allWrappers: [] - readonly property list<NotifWrapper> popups: allWrappers.filter(n => n && n.popup) - - property var historyList: [] - readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json" - readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images" - property bool historyLoaded: false - property int historyEntryCounter: 0 - - property list<NotifWrapper> notificationQueue: [] - property list<NotifWrapper> visibleNotifications: [] - property int maxVisibleNotifications: 4 - property bool addGateBusy: false - property int enterAnimMs: 400 - property int seqCounter: 0 - property bool bulkDismissing: false - - property int maxQueueSize: 32 - property int maxIngressPerSecond: 20 - property double _lastIngressSec: 0 - property int _ingressCountThisSec: 0 - - property var _dismissQueue: [] - property int _dismissBatchSize: 8 - property int _dismissTickMs: 8 - property bool _suspendGrouping: false - property var _groupCache: ({ - "notifications": [], - "popups": [] - }) - property bool _groupsDirty: false - - Component.onCompleted: { - _recomputeGroups(); - Quickshell.execDetached(["mkdir", "-p", Paths.strip(Paths.cache)]); - Quickshell.execDetached(["mkdir", "-p", imageCacheDir]); - } - - FileView { - id: historyFileView - path: root.historyFile - printErrors: false - onLoaded: root.loadHistory() - onLoadFailed: error => { - if (error === 2) { - root.historyLoaded = true; - historyFileView.writeAdapter(); - } - } - - JsonAdapter { - id: historyAdapter - property var notifications: [] - } - } - - Timer { - id: historySaveTimer - interval: 200 - onTriggered: root.performSaveHistory() - } - - function _makeHistoryEntryId(sourceId, timestamp) { - historyEntryCounter += 1; - const safeSource = sourceId && sourceId !== "" ? sourceId : "notification"; - return safeSource + "_" + (timestamp || Date.now()) + "_" + historyEntryCounter; - } - - function getImageCachePath(wrapper) { - const ts = wrapper.time ? wrapper.time.getTime() : Date.now(); - const id = wrapper.notification?.id?.toString() || "0"; - return imageCacheDir + "/notif_" + ts + "_" + id + ".png"; - } - - function updateHistoryImage(wrapperId, imagePath) { - const idx = historyList.findIndex(n => n.sourceNotificationId === wrapperId || n.id === wrapperId); - if (idx < 0) - return; - const item = historyList[idx]; - const updated = { - id: item.id, - sourceNotificationId: item.sourceNotificationId || item.id, - summary: item.summary, - body: item.body, - htmlBody: item.htmlBody, - appName: item.appName, - appIcon: item.appIcon, - image: "file://" + imagePath, - urgency: item.urgency, - timestamp: item.timestamp, - desktopEntry: item.desktopEntry - }; - const newList = historyList.slice(); - newList[idx] = updated; - historyList = newList; - saveHistory(); - } - - function addToHistory(wrapper) { - if (!wrapper) - return; - const urg = typeof wrapper.urgency === "number" ? wrapper.urgency : 1; - const imageUrl = wrapper.image || ""; - let persistableImage = ""; - if (wrapper.persistedImagePath) { - persistableImage = "file://" + wrapper.persistedImagePath; - } else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) { - persistableImage = imageUrl; - } - const sourceNotificationId = wrapper.notification?.id?.toString() || ""; - const timestamp = wrapper.time.getTime(); - const data = { - id: _makeHistoryEntryId(sourceNotificationId, timestamp), - sourceNotificationId: sourceNotificationId, - summary: wrapper.summary || "", - body: wrapper.body || "", - htmlBody: wrapper.htmlBody || wrapper.body || "", - appName: wrapper.appName || "", - appIcon: wrapper.appIcon || "", - image: persistableImage, - urgency: urg, - timestamp: timestamp, - desktopEntry: wrapper.desktopEntry || "" - }; - let newList = [data, ...historyList]; - if (newList.length > SettingsData.notificationHistoryMaxCount) { - newList = newList.slice(0, SettingsData.notificationHistoryMaxCount); - } - historyList = newList; - saveHistory(); - } - - function saveHistory() { - historySaveTimer.restart(); - } - - function performSaveHistory() { - try { - historyAdapter.notifications = historyList; - historyFileView.writeAdapter(); - } catch (e) { - console.warn("NotificationService: save history failed:", e); - } - } - - function loadHistory() { - try { - const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays; - const now = Date.now(); - const maxAgeMs = maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : 0; - const loaded = []; - const seenIds = {}; - let needsRewrite = false; - - for (const item of historyAdapter.notifications || []) { - if (maxAgeMs > 0 && (now - item.timestamp) > maxAgeMs) - continue; - const urg = typeof item.urgency === "number" ? item.urgency : 1; - const body = item.body || ""; - let htmlBody = item.htmlBody || _resolveHtmlBody(body); - if (htmlBody) { - htmlBody = htmlBody.replace(/<img\b[^>]*>/gi, ""); - } - const sourceNotificationId = (item.sourceNotificationId || item.id || "").toString(); - let historyId = (item.id || "").toString(); - if (!historyId || seenIds[historyId]) { - historyId = _makeHistoryEntryId(sourceNotificationId, item.timestamp || now); - needsRewrite = true; - } - if (!item.sourceNotificationId) - needsRewrite = true; - seenIds[historyId] = true; - loaded.push({ - id: historyId, - sourceNotificationId: sourceNotificationId, - summary: item.summary || "", - body: body, - htmlBody: htmlBody, - appName: item.appName || "", - appIcon: item.appIcon || "", - image: item.image || "", - urgency: urg, - timestamp: item.timestamp || 0, - desktopEntry: item.desktopEntry || "" - }); - } - historyList = loaded; - historyLoaded = true; - if ((maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length) || needsRewrite) - saveHistory(); - } catch (e) { - console.warn("NotificationService: load history failed:", e); - historyLoaded = true; - } - } - - function _deleteCachedImage(imagePath) { - if (!imagePath || !imagePath.startsWith("file://")) - return; - const filePath = imagePath.replace("file://", ""); - if (filePath.startsWith(imageCacheDir)) { - Quickshell.execDetached(["rm", "-f", filePath]); - } - } - - function removeFromHistory(notificationId) { - const idx = historyList.findIndex(n => n.id === notificationId); - if (idx >= 0) { - _deleteCachedImage(historyList[idx].image); - historyList = historyList.filter((_, i) => i !== idx); - saveHistory(); - return true; - } - return false; - } - - function clearHistory() { - for (const item of historyList) { - _deleteCachedImage(item.image); - } - historyList = []; - saveHistory(); - } - - function getHistoryTimeRange(timestamp) { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const itemDate = new Date(timestamp); - const itemDay = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); - const diffDays = Math.floor((today - itemDay) / (1000 * 60 * 60 * 24)); - if (diffDays === 0) - return 0; - if (diffDays === 1) - return 1; - return 2; - } - - function getHistoryCountForRange(range) { - if (range === -1) - return historyList.length; - return historyList.filter(n => getHistoryTimeRange(n.timestamp) === range).length; - } - - function formatHistoryTime(timestamp) { - root.timeUpdateTick; - root.clockFormatChanged; - const now = new Date(); - const date = new Date(timestamp); - const diff = now.getTime() - timestamp; - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(minutes / 60); - if (hours < 1) { - if (minutes < 1) - return I18n.tr("now"); - return I18n.tr("%1m ago").arg(minutes); - } - const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); - const daysDiff = Math.floor((nowDate - itemDate) / (1000 * 60 * 60 * 24)); - const timeStr = SettingsData.use24HourClock ? date.toLocaleTimeString(Qt.locale(), "HH:mm") : date.toLocaleTimeString(Qt.locale(), "h:mm AP"); - if (daysDiff === 0) - return timeStr; - try { - const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US"; - const weekday = date.toLocaleDateString(localeName, { - weekday: "long" - }); - return weekday + ", " + timeStr; - } catch (e) { - return timeStr; - } - } - - function _nowSec() { - return Date.now() / 1000.0; - } - - function _ingressAllowed(urgency) { - const t = _nowSec(); - if (t - _lastIngressSec >= 1.0) { - _lastIngressSec = t; - _ingressCountThisSec = 0; - } - _ingressCountThisSec += 1; - if (urgency === NotificationUrgency.Critical) { - return true; - } - return _ingressCountThisSec <= maxIngressPerSecond; - } - - function _enqueuePopup(wrapper) { - if (notificationQueue.length >= maxQueueSize) { - const gk = getGroupKey(wrapper); - let idx = notificationQueue.findIndex(w => w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical); - if (idx === -1) { - idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical); - } - if (idx === -1) { - idx = 0; - } - const victim = notificationQueue[idx]; - if (victim) { - victim.popup = false; - } - notificationQueue.splice(idx, 1); - } - notificationQueue = [...notificationQueue, wrapper]; - } - - function _initWrapperPersistence(wrapper) { - const timeoutMs = wrapper.timer ? wrapper.timer.interval : 5000; - const isCritical = wrapper && wrapper.urgency === NotificationUrgency.Critical; - wrapper.isPersistent = isCritical || (timeoutMs === 0); - } - - function _shouldSaveToHistory(urgency, forceDisable) { - if (forceDisable === true) - return false; - if (!SettingsData.notificationHistoryEnabled) - return false; - switch (urgency) { - case NotificationUrgency.Low: - return SettingsData.notificationHistorySaveLow; - case NotificationUrgency.Critical: - return SettingsData.notificationHistorySaveCritical; - default: - return SettingsData.notificationHistorySaveNormal; - } - } - - function _resolveAppNameForRule(notif) { - if (!notif) - return ""; - if (notif.appName && notif.appName !== "") - return notif.appName; - const entry = DesktopEntries.heuristicLookup(notif.desktopEntry); - if (entry && entry.name) - return entry.name; - return ""; - } - - function _ruleFieldValue(field, info) { - switch ((field || "").toString()) { - case "desktopEntry": - return info.desktopEntry; - case "summary": - return info.summary; - case "body": - return info.body; - case "appName": - default: - return info.appName; - } - } - - function _coerceRuleUrgency(value, fallbackUrgency) { - if (typeof value === "number" && value >= NotificationUrgency.Low && value <= NotificationUrgency.Critical) - return value; - - const mapped = (value || "default").toString().toLowerCase(); - switch (mapped) { - case "low": - return NotificationUrgency.Low; - case "normal": - return NotificationUrgency.Normal; - case "critical": - return NotificationUrgency.Critical; - default: - return fallbackUrgency; - } - } - - function _matchesNotificationRule(rule, info) { - if (!rule) - return false; - if (rule.enabled === false) - return false; - - const pattern = (rule.pattern || "").toString(); - if (!pattern.trim()) - return false; - - const value = (_ruleFieldValue(rule.field, info) || "").toString(); - const matchType = (rule.matchType || "contains").toString().toLowerCase(); - - if (matchType === "exact") - return value.toLowerCase() === pattern.toLowerCase(); - if (matchType === "regex") { - try { - return new RegExp(pattern, "i").test(value); - } catch (e) { - console.warn("NotificationService: invalid notification rule regex:", pattern); - return false; - } - } - - return value.toLowerCase().includes(pattern.toLowerCase()); - } - - function _evaluateNotificationPolicy(notif) { - const baseUrgency = typeof notif.urgency === "number" ? notif.urgency : NotificationUrgency.Normal; - const policy = { - "drop": false, - "disablePopup": false, - "hideFromCenter": false, - "disableHistory": false, - "urgency": baseUrgency - }; - - const rules = SettingsData.notificationRules || []; - if (!rules.length) - return policy; - - const info = { - "appName": _resolveAppNameForRule(notif), - "desktopEntry": notif.desktopEntry || "", - "summary": notif.summary || "", - "body": notif.body || "" - }; - - for (const rule of rules) { - if (!_matchesNotificationRule(rule, info)) - continue; - - const action = (rule.action || "default").toString().toLowerCase(); - switch (action) { - case "ignore": - policy.drop = true; - break; - case "mute": - policy.disablePopup = true; - break; - case "popup_only": - policy.hideFromCenter = true; - policy.disableHistory = true; - break; - case "no_history": - policy.disableHistory = true; - break; - default: - break; - } - - policy.urgency = _coerceRuleUrgency(rule.urgency, policy.urgency); - return policy; - } - - return policy; - } - - function pruneHistory() { - const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays; - if (maxAgeDays <= 0) - return; - - const now = Date.now(); - const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000; - const toRemove = historyList.filter(item => (now - item.timestamp) > maxAgeMs); - const pruned = historyList.filter(item => (now - item.timestamp) <= maxAgeMs); - - if (pruned.length !== historyList.length) { - for (const item of toRemove) { - _deleteCachedImage(item.image); - } - historyList = pruned; - saveHistory(); - } - } - - function deleteHistory() { - for (const item of historyList) { - _deleteCachedImage(item.image); - } - historyList = []; - historyAdapter.notifications = []; - historyFileView.writeAdapter(); - } - - function onOverlayOpen() { - popupsDisabled = true; - addGate.stop(); - addGateBusy = false; - - notificationQueue = []; - for (const w of visibleNotifications) { - if (w) { - w.popup = false; - } - } - visibleNotifications = []; - _recomputeGroupsLater(); - pruneHistory(); - } - - function onOverlayClose() { - popupsDisabled = false; - processQueue(); - } - - Timer { - id: addGate - interval: 80 - running: false - repeat: false - onTriggered: { - addGateBusy = false; - processQueue(); - } - } - - Timer { - id: timeUpdateTimer - interval: 30000 - repeat: true - running: root.allWrappers.length > 0 || visibleNotifications.length > 0 - triggeredOnStart: false - onTriggered: { - root.timeUpdateTick = !root.timeUpdateTick; - } - } - - Timer { - id: dismissPump - interval: _dismissTickMs - repeat: true - running: false - onTriggered: { - let n = Math.min(_dismissBatchSize, _dismissQueue.length); - for (var i = 0; i < n; ++i) { - const w = _dismissQueue.pop(); - try { - if (w && w.notification) { - w.notification.dismiss(); - } - } catch (e) {} - } - if (_dismissQueue.length === 0) { - dismissPump.stop(); - _suspendGrouping = false; - bulkDismissing = false; - popupsDisabled = false; - _recomputeGroupsLater(); - } - } - } - - Timer { - id: groupsDebounce - interval: 16 - repeat: false - onTriggered: _recomputeGroups() - } - - property bool timeUpdateTick: false - property bool clockFormatChanged: false - - readonly property var groupedNotifications: _groupCache.notifications - readonly property var groupedPopups: _groupCache.popups - - property var expandedGroups: ({}) - property var expandedMessages: ({}) - property bool popupsDisabled: false - - NotificationServer { - id: server - - keepOnReload: false - actionsSupported: true - actionIconsSupported: true - bodyHyperlinksSupported: true - bodyImagesSupported: true - bodyMarkupSupported: true - imageSupported: true - inlineReplySupported: true - persistenceSupported: true - - onNotification: notif => { - notif.tracked = true; - - const policy = _evaluateNotificationPolicy(notif); - if (policy.drop) { - try { - notif.dismiss(); - } catch (e) {} - return; - } - - if (!_ingressAllowed(policy.urgency)) { - if (policy.urgency !== NotificationUrgency.Critical) { - try { - notif.dismiss(); - } catch (e) {} - return; - } - } - - if (SettingsData.soundsEnabled && SettingsData.soundNewNotification) { - if (policy.urgency === NotificationUrgency.Critical) { - AudioService.playCriticalNotificationSound(); - } else { - AudioService.playNormalNotificationSound(); - } - } - - const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb && !policy.disablePopup; - const isTransient = notif.transient; - const shouldKeepInCenter = !isTransient && !policy.hideFromCenter; - - if (!shouldShowPopup && !shouldKeepInCenter) { - try { - notif.dismiss(); - } catch (e) {} - return; - } - - const wrapper = notifComponent.createObject(root, { - "popup": shouldShowPopup, - "notification": notif, - "urgencyOverride": policy.urgency - }); - - if (wrapper) { - root.allWrappers.push(wrapper); - if (shouldKeepInCenter) { - root.notifications.push(wrapper); - if (_shouldSaveToHistory(wrapper.urgency, policy.disableHistory)) { - root.addToHistory(wrapper); - } - } - Qt.callLater(() => { - _initWrapperPersistence(wrapper); - }); - - if (shouldShowPopup) { - _enqueuePopup(wrapper); - processQueue(); - } - } - - _recomputeGroupsLater(); - } - } - - component NotifWrapper: QtObject { - id: wrapper - - property bool popup: false - property bool removedByLimit: false - property bool isPersistent: true - property int seq: 0 - property string persistedImagePath: "" - - onPopupChanged: { - if (!popup) { - removeFromVisibleNotifications(wrapper); - } - } - - readonly property Timer timer: Timer { - interval: { - if (!wrapper.notification) - return 5000; - switch (wrapper.urgency) { - case NotificationUrgency.Low: - return SettingsData.notificationTimeoutLow; - case NotificationUrgency.Critical: - return SettingsData.notificationTimeoutCritical; - default: - return SettingsData.notificationTimeoutNormal; - } - } - repeat: false - running: false - onTriggered: { - if (interval > 0) { - wrapper.popup = false; - } - } - } - - readonly property date time: new Date() - readonly property string timeStr: { - root.timeUpdateTick; - root.clockFormatChanged; - - const now = new Date(); - const diff = now.getTime() - time.getTime(); - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(minutes / 60); - - if (hours < 1) { - if (minutes < 1) { - return "now"; - } - return `${minutes}m ago`; - } - - const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()); - const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24)); - - if (daysDiff === 0) { - return formatTime(time); - } - - try { - const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US"; - const weekday = time.toLocaleDateString(localeName, { - weekday: "long" - }); - return `${weekday}, ${formatTime(time)}`; - } catch (e) { - return formatTime(time); - } - } - - function formatTime(date) { - let use24Hour = true; - try { - if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) { - use24Hour = SettingsData.use24HourClock; - } - } catch (e) { - use24Hour = true; - } - - if (use24Hour) { - return date.toLocaleTimeString(Qt.locale(), "HH:mm"); - } else { - return date.toLocaleTimeString(Qt.locale(), "h:mm AP"); - } - } - - required property Notification notification - readonly property string summary: (notification?.summary ?? "").replace(/<img\b[^>]*>/gi, "") - readonly property string body: (notification?.body ?? "").replace(/<img\b[^>]*>/gi, "") - readonly property string htmlBody: root._resolveHtmlBody(body) - readonly property string appIcon: notification?.appIcon ?? "" - readonly property string appName: { - if (!notification) - return "app"; - if (notification.appName == "") { - const entry = DesktopEntries.heuristicLookup(notification.desktopEntry); - if (entry && entry.name) - return entry.name.toLowerCase(); - } - return notification.appName || "app"; - } - readonly property string desktopEntry: notification?.desktopEntry ?? "" - readonly property string image: notification?.image ?? "" - readonly property string cleanImage: { - if (!image) - return ""; - return Paths.strip(image); - } - property int urgencyOverride: notification?.urgency ?? NotificationUrgency.Normal - readonly property int urgency: urgencyOverride - readonly property list<NotificationAction> actions: notification?.actions ?? [] - - readonly property Connections conn: Connections { - target: wrapper.notification?.Retainable ?? null - - function onDropped(): void { - root.allWrappers = root.allWrappers.filter(w => w !== wrapper); - root.notifications = root.notifications.filter(w => w !== wrapper); - - if (root.bulkDismissing) { - return; - } - - const groupKey = getGroupKey(wrapper); - const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey); - - if (remainingInGroup.length <= 1) { - clearGroupExpansionState(groupKey); - } - - cleanupExpansionStates(); - root._recomputeGroupsLater(); - } - - function onAboutToDestroy(): void { - wrapper.destroy(); - } - } - } - - Component { - id: notifComponent - NotifWrapper {} - } - - function dismissAllPopups() { - for (const w of visibleNotifications) { - if (w) { - w.popup = false; - } - } - visibleNotifications = []; - notificationQueue = []; - } - - function clearAllNotifications() { - if (!notifications.length) { - return; - } - bulkDismissing = true; - popupsDisabled = true; - addGate.stop(); - addGateBusy = false; - notificationQueue = []; - - for (const w of allWrappers) { - if (w) { - w.popup = false; - } - } - visibleNotifications = []; - - _dismissQueue = notifications.slice(); - if (notifications.length) { - notifications = []; - } - expandedGroups = {}; - expandedMessages = {}; - - _suspendGrouping = true; - - if (!dismissPump.running && _dismissQueue.length) { - dismissPump.start(); - } - } - - function dismissNotification(wrapper) { - if (!wrapper || !wrapper.notification) { - return; - } - wrapper.popup = false; - wrapper.notification.dismiss(); - } - - function disablePopups(disable) { - popupsDisabled = disable; - if (disable) { - notificationQueue = []; - for (const notif of visibleNotifications) { - notif.popup = false; - } - visibleNotifications = []; - } - } - - property bool _processingQueue: false - - function processQueue() { - if (addGateBusy || _processingQueue) - return; - if (popupsDisabled) - return; - if (SessionData.doNotDisturb) - return; - if (notificationQueue.length === 0) - return; - - _processingQueue = true; - - const next = notificationQueue.shift(); - if (!next) { - _processingQueue = false; - return; - } - - next.seq = ++seqCounter; - - const activePopups = visibleNotifications.filter(n => n && n.popup); - let evicted = null; - if (activePopups.length >= maxVisibleNotifications) { - const unhovered = activePopups.filter(n => n.timer?.running); - const pool = unhovered.length > 0 ? unhovered : activePopups; - evicted = pool.reduce((min, n) => (n.seq < min.seq) ? n : min, pool[0]); - if (evicted) - evicted.removedByLimit = true; - } - - if (evicted) { - visibleNotifications = [...visibleNotifications.filter(n => n !== evicted), next]; - } else { - visibleNotifications = [...visibleNotifications, next]; - } - - if (evicted) - evicted.popup = false; - next.popup = true; - - if (next.timer.interval > 0) - next.timer.start(); - - addGateBusy = true; - addGate.restart(); - _processingQueue = false; - } - - function removeFromVisibleNotifications(wrapper) { - visibleNotifications = visibleNotifications.filter(n => n !== wrapper); - processQueue(); - } - - function releaseWrapper(w) { - visibleNotifications = visibleNotifications.filter(n => n !== w); - notificationQueue = notificationQueue.filter(n => n !== w); - - if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) { - Qt.callLater(() => { - try { - w.destroy(); - } catch (e) {} - }); - } - } - - function _decodeEntities(s) { - s = s.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10))); - s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCodePoint(parseInt(n, 16))); - return s.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => { - switch (name) { - case "amp": - return "&"; - case "lt": - return "<"; - case "gt": - return ">"; - case "quot": - return "\""; - case "apos": - return "'"; - case "nbsp": - return "\u00A0"; - case "ndash": - return "\u2013"; - case "mdash": - return "\u2014"; - case "lsquo": - return "\u2018"; - case "rsquo": - return "\u2019"; - case "ldquo": - return "\u201C"; - case "rdquo": - return "\u201D"; - case "bull": - return "\u2022"; - case "hellip": - return "\u2026"; - case "trade": - return "\u2122"; - case "copy": - return "\u00A9"; - case "reg": - return "\u00AE"; - case "deg": - return "\u00B0"; - case "plusmn": - return "\u00B1"; - case "times": - return "\u00D7"; - case "divide": - return "\u00F7"; - case "micro": - return "\u00B5"; - case "middot": - return "\u00B7"; - case "laquo": - return "\u00AB"; - case "raquo": - return "\u00BB"; - case "larr": - return "\u2190"; - case "rarr": - return "\u2192"; - case "uarr": - return "\u2191"; - case "darr": - return "\u2193"; - default: - return match; - } - }); - } - - function _resolveHtmlBody(body) { - if (!body) - return ""; - - let result = body; - - if (/<\/?[a-z][\s\S]*>/i.test(body)) { - result = body; - } else { - // Decode percent-encoded URLs (e.g. https%3A%2F%2F → https://) - let processed = body.replace(/\bhttps?%3A%2F%2F[^\s]+/gi, match => { - try { - return decodeURIComponent(match); - } catch (e) { - return match; - } - }); - - if (/&(#\d+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]+);/.test(processed)) { - const decoded = _decodeEntities(processed); - if (/<\/?[a-z][\s\S]*>/i.test(decoded)) - result = decoded; - else - result = Markdown2Html.markdownToHtml(decoded); - } else { - result = Markdown2Html.markdownToHtml(processed); - } - } - - // Strip out image tags to prevent IP tracking - return result.replace(/<img\b[^>]*>/gi, ""); - } - - function getGroupKey(wrapper) { - if (wrapper.desktopEntry && wrapper.desktopEntry !== "") { - return wrapper.desktopEntry.toLowerCase(); - } - - return wrapper.appName.toLowerCase(); - } - - function _recomputeGroups() { - if (_suspendGrouping) { - _groupsDirty = true; - return; - } - _groupCache = { - "notifications": _calcGroupedNotifications(), - "popups": _calcGroupedPopups() - }; - _groupsDirty = false; - } - - function _recomputeGroupsLater() { - _groupsDirty = true; - if (!groupsDebounce.running) { - groupsDebounce.start(); - } - } - - function _calcGroupedNotifications() { - const groups = {}; - - for (const notif of notifications) { - if (!notif || !notif.notification) - continue; - const groupKey = getGroupKey(notif); - if (!groups[groupKey]) { - groups[groupKey] = { - "key": groupKey, - "appName": notif.appName, - "notifications": [], - "latestNotification": null, - "count": 0, - "hasInlineReply": false - }; - } - - groups[groupKey].notifications.unshift(notif); - groups[groupKey].latestNotification = groups[groupKey].notifications[0]; - groups[groupKey].count = groups[groupKey].notifications.length; - - if (notif.notification?.hasInlineReply) - groups[groupKey].hasInlineReply = true; - } - - return Object.values(groups).sort((a, b) => { - if (!a.latestNotification || !b.latestNotification) - return 0; - const aUrgency = a.latestNotification.urgency ?? NotificationUrgency.Low; - const bUrgency = b.latestNotification.urgency ?? NotificationUrgency.Low; - if (aUrgency !== bUrgency) { - return bUrgency - aUrgency; - } - return b.latestNotification.time.getTime() - a.latestNotification.time.getTime(); - }); - } - - function _calcGroupedPopups() { - const groups = {}; - - for (const notif of popups) { - if (!notif || !notif.notification) - continue; - const groupKey = getGroupKey(notif); - if (!groups[groupKey]) { - groups[groupKey] = { - "key": groupKey, - "appName": notif.appName, - "notifications": [], - "latestNotification": null, - "count": 0, - "hasInlineReply": false - }; - } - - groups[groupKey].notifications.unshift(notif); - groups[groupKey].latestNotification = groups[groupKey].notifications[0]; - groups[groupKey].count = groups[groupKey].notifications.length; - - if (notif.notification?.hasInlineReply) - groups[groupKey].hasInlineReply = true; - } - - return Object.values(groups).sort((a, b) => { - if (!a.latestNotification || !b.latestNotification) - return 0; - return b.latestNotification.time.getTime() - a.latestNotification.time.getTime(); - }); - } - - function toggleGroupExpansion(groupKey) { - let newExpandedGroups = {}; - for (const key in expandedGroups) { - newExpandedGroups[key] = expandedGroups[key]; - } - newExpandedGroups[groupKey] = !newExpandedGroups[groupKey]; - expandedGroups = newExpandedGroups; - } - - function dismissGroup(groupKey) { - const group = groupedNotifications.find(g => g.key === groupKey); - if (group) { - for (const notif of group.notifications) { - if (notif && notif.notification) { - notif.notification.dismiss(); - } - } - } else { - for (const notif of allWrappers) { - if (notif && notif.notification && getGroupKey(notif) === groupKey) { - notif.notification.dismiss(); - } - } - } - } - - function clearGroupExpansionState(groupKey) { - let newExpandedGroups = {}; - for (const key in expandedGroups) { - if (key !== groupKey && expandedGroups[key]) { - newExpandedGroups[key] = true; - } - } - expandedGroups = newExpandedGroups; - } - - function cleanupExpansionStates() { - const currentGroupKeys = new Set(groupedNotifications.map(g => g.key)); - const currentMessageIds = new Set(); - for (const group of groupedNotifications) { - for (const notif of group.notifications) { - if (notif && notif.notification) { - currentMessageIds.add(notif.notification.id); - } - } - } - let newExpandedGroups = {}; - for (const key in expandedGroups) { - if (currentGroupKeys.has(key) && expandedGroups[key]) { - newExpandedGroups[key] = true; - } - } - expandedGroups = newExpandedGroups; - let newExpandedMessages = {}; - for (const messageId in expandedMessages) { - if (currentMessageIds.has(messageId) && expandedMessages[messageId]) { - newExpandedMessages[messageId] = true; - } - } - expandedMessages = newExpandedMessages; - } - - function toggleMessageExpansion(messageId) { - let newExpandedMessages = {}; - for (const key in expandedMessages) { - newExpandedMessages[key] = expandedMessages[key]; - } - newExpandedMessages[messageId] = !newExpandedMessages[messageId]; - expandedMessages = newExpandedMessages; - } - - Connections { - target: SessionData - function onDoNotDisturbChanged() { - if (SessionData.doNotDisturb) { - // Hide all current popups when DND is enabled - for (const notif of visibleNotifications) { - notif.popup = false; - } - visibleNotifications = []; - notificationQueue = []; - } else { - // Re-enable popup processing when DND is disabled - processQueue(); - } - } - } - - Connections { - target: typeof SettingsData !== "undefined" ? SettingsData : null - function onUse24HourClockChanged() { - root.clockFormatChanged = !root.clockFormatChanged; - } - function onNotificationHistoryMaxAgeDaysChanged() { - root.pruneHistory(); - } - function onNotificationHistoryEnabledChanged() { - if (!SettingsData.notificationHistoryEnabled) { - root.deleteHistory(); - } - } - } -} |