import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Services.Notifications import Quickshell.Wayland import qs.Commons import qs.Modules.MainScreen import qs.Modules.Panels.Settings import qs.Services.System import qs.Services.UI import qs.Widgets // Notification History panel SmartPanel { id: root preferredWidth: Math.round((Settings.data.notifications.enableMarkdown ? 540 : 440) * Style.uiScaleRatio) preferredHeight: Math.round((Settings.data.notifications.enableMarkdown ? 640 : 540) * Style.uiScaleRatio) onOpened: { NotificationService.updateLastSeenTs(); } panelContent: Rectangle { id: panelContent color: "transparent" focus: true // Force focus when opened Connections { target: root function onOpened() { panelContent.forceActiveFocus(); } } Keys.onPressed: event => { // Tab navigation for categories if (event.key === Qt.Key_Tab) { currentRange = (currentRange + 1) % 4; event.accepted = true; return; } if (event.key === Qt.Key_Backtab) { // Shift+Tab currentRange = (currentRange - 1 + 4) % 4; event.accepted = true; return; } // Navigation Up/Down if (checkKey(event, 'up')) { moveSelection(-1); event.accepted = true; return; } if (checkKey(event, 'down')) { moveSelection(1); event.accepted = true; return; } // Action Navigation Left/Right if (checkKey(event, 'left')) { moveAction(-1); event.accepted = true; return; } if (checkKey(event, 'right')) { moveAction(1); event.accepted = true; return; } // Activation (Enter) if (checkKey(event, 'enter')) { activateSelection(); event.accepted = true; return; } // Removal (Delete/Remove) if (checkKey(event, 'remove') || event.key === Qt.Key_Delete) { removeSelection(); event.accepted = true; return; } } function parseActions(actions) { try { return JSON.parse(actions || "[]"); } catch (e) { return []; } } function moveSelection(dir) { var m = NotificationService.historyModel; if (!m || m.count === 0) return; var newIndex = focusIndex; var found = false; var count = m.count; // If no selection yet, start from beginning (or end if up) if (focusIndex === -1) { if (dir > 0) newIndex = -1; else newIndex = count; } // Loop to find next visible item var loopCount = 0; while (loopCount < count) { newIndex += dir; // Bounds check if (newIndex < 0 || newIndex >= count) { break; // Stop at edges } var item = m.get(newIndex); if (item && isInCurrentRange(item.timestamp)) { found = true; break; } loopCount++; } if (found) { focusIndex = newIndex; actionIndex = -1; // Reset action selection scrollToItem(focusIndex); } } function moveAction(dir) { if (focusIndex === -1) return; var item = NotificationService.historyModel.get(focusIndex); if (!item) return; var actions = parseActions(item.actionsJson); if (actions.length === 0) return; var newActionIndex = actionIndex + dir; // Clamp between -1 (body) and actions.length - 1 if (newActionIndex < -1) newActionIndex = -1; if (newActionIndex >= actions.length) newActionIndex = actions.length - 1; actionIndex = newActionIndex; } function activateSelection() { if (focusIndex === -1) return; var item = NotificationService.historyModel.get(focusIndex); if (!item) return; if (actionIndex >= 0) { var actions = parseActions(item.actionsJson); if (actionIndex < actions.length) { if (NotificationService.invokeAction(item.id, actions[actionIndex].identifier)) root.close(); } } else { var delegate = notificationColumn.children[focusIndex]; if (!delegate) return; if (!(delegate.canExpand || delegate.isExpanded)) return; if (scrollView.expandedId === item.id) { scrollView.expandedId = ""; } else { scrollView.expandedId = item.id; } } } function removeSelection() { if (focusIndex === -1) return; var item = NotificationService.historyModel.get(focusIndex); if (!item) return; NotificationService.removeFromHistory(item.id); // selection updates automatically? // If we remove item at index i, the next item becomes index i. // So focusIndex is still valid (unless it was last item). // But we should re-verify if it exists. // Actually NotificationService removal might be async or immediate. // If immediate, model count decreases. // We might need to clamp focusIndex. // Let's handle this in a helper or just let the user navigate again. // Better UX: select next available or previous if last. } function scrollToItem(index) { // Find the delegate item if (index < 0 || index >= notificationColumn.children.length) return; var item = notificationColumn.children[index]; if (item && item.visible) { // Use the internal flickable from NScrollView for accurate scrolling var flickable = scrollView._internalFlickable; if (!flickable || !flickable.contentItem) return; var pos = flickable.contentItem.mapFromItem(item, 0, 0); var itemY = pos.y; var itemHeight = item.height; var currentContentY = flickable.contentY; var viewHeight = flickable.height; // Check if above visible area if (itemY < currentContentY) { flickable.contentY = Math.max(0, itemY - Style.marginM); } else // Check if below visible area if (itemY + itemHeight > currentContentY + viewHeight) { flickable.contentY = (itemY + itemHeight) - viewHeight + Style.marginM; } } } // Calculate content height based on header + tabs (if visible) + content property real calculatedHeight: { if (NotificationService.historyModel.count === 0) { return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; } return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; } property real contentPreferredHeight: Math.min(root.preferredHeight, Math.ceil(calculatedHeight)) property real layoutWidth: Math.max(1, root.preferredWidth - Style.margin2L) // State (lazy-loaded with panelContent) property var rangeCounts: [0, 0, 0, 0] property var lastKnownDate: null // Track the current date to detect day changes // UI state (lazy-loaded with panelContent) // 0 = All, 1 = Today, 2 = Yesterday, 3 = Earlier property int currentRange: 1 // start on Today by default property bool groupByDate: true onCurrentRangeChanged: resetFocus() // Keyboard navigation state property int focusIndex: -1 property int actionIndex: -1 // For actions within a notification function resetFocus() { focusIndex = -1; actionIndex = -1; } function checkKey(event, settingName) { return Keybinds.checkKey(event, settingName, Settings); } // Helper functions (lazy-loaded with panelContent) function dateOnly(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); } function getDateKey(d) { // Returns a string key for the date (YYYY-MM-DD) for comparison var date = dateOnly(d); return date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate(); } function rangeForTimestamp(ts) { var dt = new Date(ts); var today = dateOnly(new Date()); var thatDay = dateOnly(dt); var diffMs = today - thatDay; var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 0; if (diffDays === 1) return 1; return 2; } function recalcRangeCounts() { var m = NotificationService.historyModel; if (!m || typeof m.count === "undefined" || m.count <= 0) { panelContent.rangeCounts = [0, 0, 0, 0]; return; } var counts = [0, 0, 0, 0]; counts[0] = m.count; for (var i = 0; i < m.count; ++i) { var item = m.get(i); if (!item || typeof item.timestamp === "undefined") continue; var r = rangeForTimestamp(item.timestamp); counts[r + 1] = counts[r + 1] + 1; } panelContent.rangeCounts = counts; } function isInCurrentRange(ts) { if (currentRange === 0) return true; return rangeForTimestamp(ts) === (currentRange - 1); } function countForRange(range) { return rangeCounts[range] || 0; } function hasNotificationsInCurrentRange() { var m = NotificationService.historyModel; if (!m || m.count === 0) { return false; } for (var i = 0; i < m.count; ++i) { var item = m.get(i); if (item && isInCurrentRange(item.timestamp)) return true; } return false; } Component.onCompleted: { recalcRangeCounts(); // Initialize lastKnownDate lastKnownDate = getDateKey(new Date()); } Connections { target: NotificationService.historyModel function onCountChanged() { panelContent.recalcRangeCounts(); } } // Timer to check for day changes at midnight Timer { id: dayChangeTimer interval: 60000 // Check every minute repeat: true running: true // Always runs when panelContent exists (panel is open) onTriggered: { var currentDateKey = panelContent.getDateKey(new Date()); if (panelContent.lastKnownDate !== null && panelContent.lastKnownDate !== currentDateKey) { // Day has changed, recalculate counts panelContent.recalcRangeCounts(); } panelContent.lastKnownDate = currentDateKey; } } ColumnLayout { id: mainColumn anchors.fill: parent anchors.margins: Style.marginL spacing: Style.marginM // Header section NBox { id: headerBox Layout.fillWidth: true implicitHeight: header.implicitHeight + Style.margin2M ColumnLayout { id: header anchors.fill: parent anchors.margins: Style.marginM spacing: Style.marginM RowLayout { id: headerRow NIcon { icon: "bell" pointSize: Style.fontSizeXXL color: Color.mPrimary } NText { text: I18n.tr("common.notifications") pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true } NIconButton { icon: NotificationService.doNotDisturb ? "bell-off" : "bell" tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-enabled") baseSize: Style.baseWidgetSize * 0.8 onClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb } NIconButton { icon: "trash" tooltipText: I18n.tr("actions.clear-history") baseSize: Style.baseWidgetSize * 0.8 onClicked: { NotificationService.clearHistory(); // Close panel as there is nothing more to see. root.close(); } } NIconButton { icon: "settings" tooltipText: I18n.tr("common.settings") baseSize: Style.baseWidgetSize * 0.8 onClicked: { SettingsPanelService.openToTab(SettingsPanel.Tab.Notifications, 0, screen); root.close(); } } NIconButton { icon: "close" tooltipText: I18n.tr("common.close") baseSize: Style.baseWidgetSize * 0.8 onClicked: root.close() } } // Time range tabs ([All] / [Today] / [Yesterday] / [Earlier]) NTabBar { id: tabsBox Layout.fillWidth: true visible: NotificationService.historyModel.count > 0 && panelContent.groupByDate currentIndex: panelContent.currentRange tabHeight: Style.toOdd(Style.baseWidgetSize * 0.8) spacing: Style.marginXS distributeEvenly: true NTabButton { tabIndex: 0 text: I18n.tr("launcher.categories.all") + " (" + panelContent.countForRange(0) + ")" checked: tabsBox.currentIndex === 0 onClicked: panelContent.currentRange = 0 pointSize: Style.fontSizeXS } NTabButton { tabIndex: 1 text: I18n.tr("notifications.range.today") + " (" + panelContent.countForRange(1) + ")" checked: tabsBox.currentIndex === 1 onClicked: panelContent.currentRange = 1 pointSize: Style.fontSizeXS } NTabButton { tabIndex: 2 text: I18n.tr("notifications.range.yesterday") + " (" + panelContent.countForRange(2) + ")" checked: tabsBox.currentIndex === 2 onClicked: panelContent.currentRange = 2 pointSize: Style.fontSizeXS } NTabButton { tabIndex: 3 text: I18n.tr("notifications.range.earlier") + " (" + panelContent.countForRange(3) + ")" checked: tabsBox.currentIndex === 3 onClicked: panelContent.currentRange = 3 pointSize: Style.fontSizeXS } } } } // Notification list container with gradient overlay Item { Layout.fillWidth: true Layout.fillHeight: true NScrollView { id: scrollView anchors.fill: parent horizontalPolicy: ScrollBar.AlwaysOff verticalPolicy: ScrollBar.AsNeeded reserveScrollbarSpace: false gradientColor: Color.mSurface // Track which notification is expanded property string expandedId: "" ColumnLayout { width: panelContent.layoutWidth spacing: Style.marginM // Empty state when no notifications NBox { visible: !panelContent.hasNotificationsInCurrentRange() Layout.fillWidth: true Layout.preferredHeight: emptyState.implicitHeight + Style.marginXL ColumnLayout { id: emptyState anchors.fill: parent anchors.margins: Style.marginM spacing: Style.marginM Item { Layout.fillHeight: true } NIcon { icon: "bell-off" pointSize: (NotificationService.historyModel.count === 0) ? 48 : Style.baseWidgetSize color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: I18n.tr("notifications.panel.no-notifications") pointSize: (NotificationService.historyModel.count === 0) ? Style.fontSizeL : Style.fontSizeM color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { visible: NotificationService.historyModel.count === 0 text: I18n.tr("notifications.panel.description") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true wrapMode: Text.WordWrap } Item { Layout.fillHeight: true } } } // Notification list container Item { visible: panelContent.hasNotificationsInCurrentRange() Layout.fillWidth: true Layout.preferredHeight: notificationColumn.implicitHeight Column { id: notificationColumn width: panelContent.layoutWidth spacing: Style.marginM Repeater { model: NotificationService.historyModel delegate: Item { id: notificationDelegate width: parent.width visible: panelContent.isInCurrentRange(model.timestamp) height: visible && !isRemoving ? contentColumn.height + Style.margin2M : 0 property int listIndex: index property string notificationId: model.id property string appName: model.appName || "" property bool isExpanded: scrollView.expandedId === notificationId property bool canExpand: summaryText.truncated || bodyText.truncated property real swipeOffset: 0 property real pressGlobalX: 0 property real pressGlobalY: 0 property bool isSwiping: false property bool isRemoving: false property string pendingLink: "" readonly property real swipeStartThreshold: Math.round(16 * Style.uiScaleRatio) readonly property real swipeDismissThreshold: Math.max(110, width * 0.3) readonly property int removeAnimationDuration: Style.animationNormal readonly property int notificationTextFormat: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? Text.MarkdownText : Text.StyledText readonly property real actionButtonSize: Style.baseWidgetSize * 0.7 readonly property real buttonClusterWidth: notificationDelegate.actionButtonSize * 2 + Style.marginXS readonly property real iconSize: Math.round(40 * Style.uiScaleRatio) function isSafeLink(link) { if (!link) return false; const lower = link.toLowerCase(); const schemes = ["http://", "https://", "mailto:"]; return schemes.some(scheme => lower.startsWith(scheme)); } function linkAtPoint(x, y) { if (!Settings.data.notifications.enableMarkdown || !notificationDelegate.isExpanded) return ""; if (summaryText) { const summaryPoint = summaryText.mapFromItem(historyInteractionArea, x, y); if (summaryPoint.x >= 0 && summaryPoint.y >= 0 && summaryPoint.x <= summaryText.width && summaryPoint.y <= summaryText.height) { const summaryLink = summaryText.linkAt ? summaryText.linkAt(summaryPoint.x, summaryPoint.y) : ""; if (isSafeLink(summaryLink)) return summaryLink; } } if (bodyText) { const bodyPoint = bodyText.mapFromItem(historyInteractionArea, x, y); if (bodyPoint.x >= 0 && bodyPoint.y >= 0 && bodyPoint.x <= bodyText.width && bodyPoint.y <= bodyText.height) { const bodyLink = bodyText.linkAt ? bodyText.linkAt(bodyPoint.x, bodyPoint.y) : ""; if (isSafeLink(bodyLink)) return bodyLink; } } return ""; } function updateCursorAt(x, y) { if (notificationDelegate.isExpanded && notificationDelegate.linkAtPoint(x, y)) { historyInteractionArea.cursorShape = Qt.PointingHandCursor; } else { historyInteractionArea.cursorShape = Qt.ArrowCursor; } } transform: Translate { x: notificationDelegate.swipeOffset } function dismissBySwipe() { if (isRemoving) return; isRemoving = true; isSwiping = false; if (Settings.data.general.animationDisabled) { NotificationService.removeFromHistory(notificationId); return; } swipeOffset = swipeOffset >= 0 ? width + Style.marginL : -width - Style.marginL; opacity = 0; removeTimer.restart(); } Timer { id: removeTimer interval: notificationDelegate.removeAnimationDuration repeat: false onTriggered: NotificationService.removeFromHistory(notificationId) } Behavior on swipeOffset { enabled: !Settings.data.general.animationDisabled && !notificationDelegate.isSwiping NumberAnimation { duration: notificationDelegate.removeAnimationDuration easing.type: Easing.OutCubic } } Behavior on opacity { enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving NumberAnimation { duration: notificationDelegate.removeAnimationDuration easing.type: Easing.OutCubic } } Behavior on height { enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving NumberAnimation { duration: notificationDelegate.removeAnimationDuration easing.type: Easing.OutCubic } } Behavior on y { enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving NumberAnimation { duration: notificationDelegate.removeAnimationDuration easing.type: Easing.OutCubic } } // Parse actions safely property var actionsList: parseActions(model.actionsJson) property bool isFocused: index === panelContent.focusIndex Rectangle { anchors.fill: parent radius: Style.radiusM color: Color.mSurfaceVariant border.color: { if (notificationDelegate.isFocused) return Color.mPrimary; if (Settings.data.ui.boxBorderEnabled) return Qt.alpha(Color.mOutline, Style.opacityHeavy); return "transparent"; } border.width: notificationDelegate.isFocused ? Style.borderM : Style.borderS Behavior on color { enabled: !Settings.data.general.animationDisabled ColorAnimation { duration: Style.animationFast } } } // Click to expand/collapse MouseArea { id: historyInteractionArea anchors.fill: parent anchors.rightMargin: notificationDelegate.buttonClusterWidth + Style.marginM enabled: !notificationDelegate.isRemoving hoverEnabled: true cursorShape: Qt.ArrowCursor onPressed: mouse => { panelContent.focusIndex = index; panelContent.actionIndex = -1; if (notificationDelegate.isExpanded) { const link = notificationDelegate.linkAtPoint(mouse.x, mouse.y); if (link) { notificationDelegate.pendingLink = link; } else { notificationDelegate.pendingLink = ""; } } if (mouse.button !== Qt.LeftButton) return; const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y); notificationDelegate.pressGlobalX = globalPoint.x; notificationDelegate.pressGlobalY = globalPoint.y; notificationDelegate.isSwiping = false; } onPositionChanged: mouse => { if (!(mouse.buttons & Qt.LeftButton) || notificationDelegate.isRemoving) return; const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y); const deltaX = globalPoint.x - notificationDelegate.pressGlobalX; const deltaY = globalPoint.y - notificationDelegate.pressGlobalY; if (!notificationDelegate.isSwiping) { if (Math.abs(deltaX) < notificationDelegate.swipeStartThreshold) return; // Only start a swipe-dismiss when horizontal movement is dominant. if (Math.abs(deltaX) <= Math.abs(deltaY) * 1.15) { return; } notificationDelegate.isSwiping = true; } if (notificationDelegate.pendingLink && Math.abs(deltaX) >= notificationDelegate.swipeStartThreshold) { notificationDelegate.pendingLink = ""; } notificationDelegate.swipeOffset = deltaX; } onReleased: mouse => { if (mouse.button !== Qt.LeftButton) return; if (notificationDelegate.isSwiping) { if (Math.abs(notificationDelegate.swipeOffset) >= notificationDelegate.swipeDismissThreshold) { notificationDelegate.dismissBySwipe(); } else { notificationDelegate.swipeOffset = 0; } notificationDelegate.isSwiping = false; notificationDelegate.pendingLink = ""; return; } if (notificationDelegate.pendingLink) { Qt.openUrlExternally(notificationDelegate.pendingLink); notificationDelegate.pendingLink = ""; return; } // Without a default action, or if invoking it fails, // fall back to focusing the sender window by app identity. var actions = notificationDelegate.actionsList; var hasDefault = actions.some(function (a) { return a.identifier === "default"; }); if (hasDefault && NotificationService.invokeAction(notificationDelegate.notificationId, "default")) { root.close(); } else { NotificationService.focusSenderWindow(notificationDelegate.appName); root.close(); } } onCanceled: { notificationDelegate.isSwiping = false; notificationDelegate.swipeOffset = 0; notificationDelegate.pendingLink = ""; historyInteractionArea.cursorShape = Qt.ArrowCursor; } } HoverHandler { target: historyInteractionArea onPointChanged: notificationDelegate.updateCursorAt(point.position.x, point.position.y) onActiveChanged: { if (!active) { historyInteractionArea.cursorShape = Qt.ArrowCursor; } } } onVisibleChanged: { if (!visible) { notificationDelegate.isSwiping = false; notificationDelegate.swipeOffset = 0; notificationDelegate.opacity = 1; notificationDelegate.isRemoving = false; removeTimer.stop(); } } Component.onDestruction: removeTimer.stop() Column { id: contentColumn anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Style.marginM spacing: Style.marginM Row { width: parent.width spacing: Style.marginM // Icon NImageRounded { anchors.verticalCenter: notificationDelegate.isExpanded ? undefined : parent.verticalCenter width: notificationDelegate.iconSize height: notificationDelegate.iconSize radius: Math.min(Style.radiusL, width / 2) imagePath: model.cachedImage || model.originalImage || "" borderColor: "transparent" borderWidth: 0 fallbackIcon: "bell" fallbackIconSize: 24 } // Content Column { width: parent.width - notificationDelegate.iconSize - notificationDelegate.buttonClusterWidth - Style.margin2M spacing: Style.marginXS // Header row with app name and timestamp Row { width: parent.width spacing: Style.marginS // Urgency indicator Rectangle { width: 6 height: 6 anchors.verticalCenter: parent.verticalCenter radius: 3 visible: model.urgency !== 1 color: { if (model.urgency === 2) return Color.mError; else if (model.urgency === 0) return Color.mOnSurfaceVariant; else return "transparent"; } } NText { text: model.appName || "Unknown App" pointSize: Style.fontSizeXS font.weight: Style.fontWeightBold color: Color.mSecondary } NText { textFormat: Text.PlainText text: " " + Time.formatRelativeTime(model.timestamp) pointSize: Style.fontSizeXXS color: Color.mOnSurfaceVariant anchors.bottom: parent.bottom } } // Summary NText { id: summaryText width: parent.width text: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? (model.summaryMarkdown || I18n.tr("common.no-summary")) : (model.summary || I18n.tr("common.no-summary")) pointSize: Style.fontSizeM color: Color.mOnSurface textFormat: notificationDelegate.notificationTextFormat wrapMode: Text.Wrap maximumLineCount: notificationDelegate.isExpanded ? 999 : 2 elide: Text.ElideRight } // Body NText { id: bodyText width: parent.width text: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? (model.bodyMarkdown || "") : (model.body || "") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant textFormat: notificationDelegate.notificationTextFormat wrapMode: Text.Wrap maximumLineCount: notificationDelegate.isExpanded ? 999 : 3 elide: Text.ElideRight visible: text.length > 0 } // Actions Flow Flow { width: parent.width spacing: Style.marginS visible: notificationDelegate.actionsList.length > 0 Repeater { model: notificationDelegate.actionsList delegate: NButton { text: modelData.text fontSize: Style.fontSizeS readonly property bool actionNavActive: notificationDelegate.isFocused && panelContent.actionIndex !== -1 readonly property bool isSelected: actionNavActive && panelContent.actionIndex === index backgroundColor: isSelected ? Color.mSecondary : Color.mPrimary textColor: isSelected ? Color.mOnSecondary : Color.mOnPrimary outlined: false implicitHeight: 24 onHoveredChanged: { if (hovered) { panelContent.focusIndex = notificationDelegate.listIndex; } } // Capture modelData in a property to avoid reference errors property var actionData: modelData onClicked: { if (NotificationService.invokeAction(notificationDelegate.notificationId, actionData.identifier)) root.close(); } } } } } Item { width: notificationDelegate.buttonClusterWidth height: notificationDelegate.actionButtonSize Row { anchors.right: parent.right spacing: Style.marginXS NIconButton { id: expandButton icon: notificationDelegate.isExpanded ? "chevron-up" : "chevron-down" tooltipText: notificationDelegate.isExpanded ? I18n.tr("notifications.panel.click-to-collapse") || "Click to collapse" : I18n.tr("notifications.panel.click-to-expand") || "Click to expand" baseSize: notificationDelegate.actionButtonSize opacity: (notificationDelegate.canExpand || notificationDelegate.isExpanded) ? 1.0 : 0.0 enabled: notificationDelegate.canExpand || notificationDelegate.isExpanded onClicked: { notificationDelegate.pendingLink = ""; historyInteractionArea.cursorShape = Qt.ArrowCursor; if (scrollView.expandedId === notificationId) { scrollView.expandedId = ""; } else { scrollView.expandedId = notificationId; } } } // Delete button NIconButton { icon: "trash" tooltipText: I18n.tr("tooltips.delete-notification") baseSize: notificationDelegate.actionButtonSize onClicked: { NotificationService.removeFromHistory(notificationId); } } } } } } } } } } } } } } } }