pragma ComponentBehavior: Bound import QtQuick import "Util.js" as Util Column { id: root required property Theme theme readonly property Flickable scrollView: scroll required property Notifications notificationService property var panelState: notificationService ? Util.parseJson(notificationService.stateJson(), { "groups": [], "dnd": false, "activeCount": 0 }) : ({ "groups": [], "dnd": false, "activeCount": 0 }) readonly property var groups: panelState.groups || [] // mutators trigger side-effects only; never assign to `state` — that // would destroy the binding above and freeze the open popup, so // externally-arriving notifications would stop appearing until reopen. // stateJson() reads reactive props (notifications/dnd/expandedGroups/…) // so the binding re-evaluates on its own after every mutation. function runAction(id, actionId) { if (notificationService) notificationService.runAction(id, actionId); } function editReply(id, text) { if (notificationService) notificationService.setReplyDraft(id, text); } function sendReply(id, text) { if (notificationService) notificationService.sendReply(id, text); } function toggleGroup(appName) { if (notificationService) notificationService.toggleGroup(appName); } function toggleDnd() { if (notificationService) notificationService.toggleDnd(); } function clearAll() { if (notificationService) notificationService.discardAllNotifications(); } spacing: 8 width: parent ? parent.width : 0 StableListModel { id: groupRows rows: root.groups } Item { width: parent.width height: 28 NuguText { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "title" text: `notifications${root.panelState.activeCount ? " · " + root.panelState.activeCount : ""}` } Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 PopupButton { theme: root.theme height: 24 text: root.panelState.dnd ? "dnd on" : "dnd" checked: root.panelState.dnd onClicked: root.toggleDnd() } PopupButton { visible: (root.panelState.activeCount || 0) > 0 theme: root.theme height: 24 text: "clear all" role: "destructive" onClicked: root.clearAll() } } } // History can outgrow the popup; the header stays fixed and everything // below scrolls. The Loader hosting this Column forces a concrete // root.height, so the viewport can bound on it. Item { id: scrollArea width: parent.width height: Math.max(0, root.height - 28 - root.spacing) Flickable { id: scroll anchors.fill: parent contentHeight: scrollColumn.height clip: true interactive: contentHeight > height boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick Column { id: scrollColumn width: scroll.width spacing: 8 Item { width: parent.width height: 220 visible: root.groups.length === 0 Column { anchors.centerIn: parent spacing: 8 NuguText { anchors.horizontalCenter: parent.horizontalCenter theme: root.theme variant: "title" text: "no notifications" } NuguText { anchors.horizontalCenter: parent.horizontalCenter theme: root.theme variant: "meta" text: "you're all caught up" } } } Repeater { model: groupRows delegate: Column { id: groupDelegate required property var rowData width: root.width spacing: 6 PopupButton { id: groupHeader readonly property int hiddenCount: Math.max(1, (groupDelegate.rowData.count || 1) - (groupDelegate.rowData.previewCount || 1)) theme: root.theme width: parent.width height: (groupDelegate.rowData.count || 0) > 1 ? 30 : 0 visible: (groupDelegate.rowData.count || 0) > 1 text: groupDelegate.rowData.expanded ? `${groupDelegate.rowData.appName || "app"} · ${groupDelegate.rowData.count} — show less` : `${groupDelegate.rowData.appName || "app"} · ${groupDelegate.rowData.count} — show ${hiddenCount} more` checked: groupDelegate.rowData.expanded onClicked: root.toggleGroup(groupDelegate.rowData.appName || "") } StableListModel { id: notificationRows rows: groupDelegate.rowData.visibleNotifications || [] } Repeater { model: notificationRows delegate: Item { id: cardWrap required property var rowData width: root.width implicitHeight: cardInner.implicitHeight height: implicitHeight NotificationCard { id: cardInner width: parent.width theme: root.theme modelData: cardWrap.rowData replyDraft: cardWrap.rowData.replyDraft || "" replyError: cardWrap.rowData.replyError || "" onDismiss: id => root.runAction(id, "close") onInvokeAction: (id, actionId) => root.runAction(id, actionId) onReplyEdited: (id, text) => root.editReply(id, text) onSendReply: (id, text) => root.sendReply(id, text) } } } } } } } // slim scroll indicator, overlaid (outside the Flickable so it // doesn't scroll with content); shown only when content overflows. Rectangle { anchors.right: parent.right width: 3 radius: 2 visible: scroll.contentHeight > scroll.height + 1 color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.5) : "transparent" height: Math.max(24, scrollArea.height * scrollArea.height / Math.max(1, scroll.contentHeight)) y: scroll.contentY * scrollArea.height / Math.max(1, scroll.contentHeight) } } }