Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 107 additions & 16 deletions Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Panel {
property string sortMode: "updated"
property bool cursorActive: false
property int cursorIndex: 0
property bool notificationsExpanded: false
property int notificationsPage: 0
property bool reviewsExpanded: false
property bool myPullsExpanded: false
property bool issuesExpanded: false
Expand Down Expand Up @@ -70,12 +70,23 @@ Panel {
return rows.slice(0, expanded ? activityExpandedCount : activityPreviewCount)
}

function notificationPageCount() {
return Math.max(1, Math.ceil(github.notifications.length / activityPreviewCount))
}

function notificationRows() {
var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))
if (page !== notificationsPage) notificationsPage = page
var start = page * activityPreviewCount
return github.notifications.slice(start, start + activityPreviewCount)
}

function buildCursorTargets() {
var targets = []
function add(kind, rows) {
for (var i = 0; i < rows.length; i++) targets.push({ key: kind + ":" + String(rows[i].id || rows[i].url || i), kind: kind, row: rows[i] })
}
add("notification", sectionRows(github.notifications, notificationsExpanded))
add("notification", notificationRows())
add("review", sectionRows(github.reviewRequests, reviewsExpanded))
add("mypull", sectionRows(github.myPullRequests, myPullsExpanded))
add("issue", sectionRows(github.assignedIssues, issuesExpanded))
Expand Down Expand Up @@ -456,10 +467,14 @@ Panel {
title: "UNREAD NOTIFICATIONS"
count: github.notifications.length
emptyText: github.state === "ready" ? "You're all caught up." : "No notifications loaded."
model: root.sectionRows(github.notifications, root.notificationsExpanded)
expanded: root.notificationsExpanded
model: root.notificationRows()
Comment thread
AnthonyPoschen marked this conversation as resolved.
showExpansionControl: false
footerButtonsBordered: true
page: root.notificationsPage
pageCount: root.notificationPageCount()
openUrl: "https://github.com/notifications"
onToggleExpanded: root.notificationsExpanded = !root.notificationsExpanded
onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)
onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)
delegateComponent: notificationDelegate
actionText: "Mark all read"
actionBusyText: "Marking…"
Expand Down Expand Up @@ -501,6 +516,7 @@ Panel {
count: github.assignedIssues.length
model: root.sectionRows(github.assignedIssues, root.issuesExpanded)
expanded: root.issuesExpanded
footerButtonsBordered: true
openUrl: "https://github.com/issues/assigned"
onToggleExpanded: root.issuesExpanded = !root.issuesExpanded
delegateComponent: issueDelegate
Expand Down Expand Up @@ -863,6 +879,7 @@ Panel {
detail: modelData.repository + " · " + modelData.reason + " · " + root.relativeTime(modelData.updatedAt)
url: modelData.url
showReadAction: true
showTrailingIndicator: false
notificationId: String(modelData.id || "")
}
}
Expand Down Expand Up @@ -967,7 +984,11 @@ Panel {
property var model: []
property Component delegateComponent: null
property bool expanded: false
property bool showExpansionControl: true
property bool footerButtonsBordered: false
property string openUrl: ""
property int page: 0
property int pageCount: 1
// Optional destructive action. It arms on the first click and only runs on
// the second, so a stray click cannot clear the section.
property string actionText: ""
Expand All @@ -980,6 +1001,8 @@ Panel {
property var actionPrepare: null
property string preparedAction: ""
signal toggleExpanded()
signal previousPage()
signal nextPage()
signal actionTriggered(string prepared)

function disarmAction() {
Expand Down Expand Up @@ -1028,10 +1051,11 @@ Panel {
id: sectionFooter
// Expanding is only offered once the section is truncated; below that
// threshold the remaining controls render unbordered on their own line.
readonly property bool expandable: section.count > root.activityPreviewCount
readonly property bool expandable: section.showExpansionControl && section.count > root.activityPreviewCount
readonly property bool paginated: section.pageCount > 1
readonly property bool showOpen: section.count > 0 && section.openUrl !== ""
readonly property bool showAction: section.count > 0 && section.actionEnabled && section.actionText !== ""
visible: expandable || showOpen || showAction
readonly property bool showAction: section.count > 0 && section.actionText !== ""
visible: expandable || paginated || showOpen || showAction
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.space(12)
Button {
Expand All @@ -1053,9 +1077,9 @@ Panel {
onImplicitWidthChanged: reservedWidth = Math.max(reservedWidth, implicitWidth)
width: Math.max(reservedWidth, implicitWidth)
visible: sectionFooter.showAction
enabled: !section.actionBusy
enabled: section.actionEnabled && !section.actionBusy
text: section.actionBusy ? section.actionBusyText : (section.actionArmed ? section.actionConfirmText : section.actionText)
bordered: sectionFooter.expandable
bordered: sectionFooter.expandable || section.footerButtonsBordered
foreground: section.actionArmed ? root.urgent : root.foreground
fontFamily: root.fontFamily
fontSize: Style.font.caption
Expand All @@ -1075,10 +1099,44 @@ Panel {
section.actionTriggered(confirmed)
}
}
Button {
id: previousPageButton
visible: sectionFooter.paginated
text: "󰅁"
tooltipText: "Previous notifications"
bordered: true
foreground: root.foreground
fontFamily: root.fontFamily
fontSize: Style.font.caption
verticalPadding: Style.spacing.controlPaddingY
enabled: section.page > 0
onClicked: section.previousPage()
}
Text {
visible: sectionFooter.paginated
text: (section.page + 1) + " / " + section.pageCount
height: previousPageButton.height
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
verticalAlignment: Text.AlignVCenter
}
Button {
visible: sectionFooter.paginated
text: "󰅂"
tooltipText: "Next notifications"
bordered: true
foreground: root.foreground
fontFamily: root.fontFamily
fontSize: Style.font.caption
verticalPadding: Style.spacing.controlPaddingY
enabled: section.page + 1 < section.pageCount
onClicked: section.nextPage()
}
Button {
visible: sectionFooter.showOpen
text: "Open in GitHub 󰅂"
bordered: sectionFooter.expandable
bordered: sectionFooter.expandable || section.footerButtonsBordered
foreground: root.foreground
fontFamily: root.fontFamily
fontSize: Style.font.caption
Expand All @@ -1097,6 +1155,7 @@ Panel {
property bool pulse: false
property bool danger: false
property bool showReadAction: false
property bool showTrailingIndicator: true
property string notificationId: ""
property string rowKind: ""
property int rowIndex: 0
Expand All @@ -1117,10 +1176,10 @@ Panel {
RowLayout {
id: row
anchors.left: parent.left
anchors.right: parent.right
anchors.right: readActionStrip.visible ? readActionStrip.left : parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.space(9)
anchors.rightMargin: Style.space(9)
anchors.rightMargin: readActionStrip.visible ? 0 : Style.space(9)
spacing: Style.space(9)
Text {
text: linkRow.glyph
Expand Down Expand Up @@ -1157,17 +1216,49 @@ Panel {
elide: Text.ElideRight
}
}
Text {
visible: linkRow.showTrailingIndicator
text: "󰅂"
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.body
}
}
BorderSurface {
id: readActionStrip
visible: linkRow.showReadAction
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Style.space(32)
radius: 0
color: "transparent"
borderSpec: Border.none()

HoverHandler {
onHoveredChanged: if (hovered) root.selectKey(linkRow.cursorKey)
}

Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Style.normalBorderWidth
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.16)
}

PanelActionButton {
visible: linkRow.showReadAction
id: readAction
anchors.fill: parent
enabled: github.markingNotificationId !== linkRow.notificationId
iconText: github.markingNotificationId === linkRow.notificationId ? "󰑐" : "󰄬"
tooltipText: "Mark this notification read (M)"
foreground: root.foreground
hoverColor: Color.accent
fontFamily: root.fontFamily
Layout.alignment: Qt.AlignVCenter
bordered: false
onClicked: github.markNotificationRead(linkRow.notificationId)
}
Text { text: "󰅂"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.body }
}
}

Expand Down
35 changes: 34 additions & 1 deletion Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Item {
property bool refreshQueued: false
property string markingNotificationId: ""
property bool markingAllNotifications: false
property var markingAllNotificationIds: []
property string notificationActionStatus: ""
property string _markStdout: ""
property string _markStderr: ""
Expand Down Expand Up @@ -171,6 +172,34 @@ Item {
notificationsRevision++;
}

function hideAllNotifications() {
var ids = [];
var hidden = copyMap(hiddenNotifications);
var remaining = [];
for (var i = 0; i < notifications.length; i++) {
var item = notifications[i];
var id = String(item.id || "");
if (id !== "") {
ids.push(id);
hidden[id] = item;
} else {
remaining.push(item);
}
}
if (ids.length > 0) {
hiddenNotifications = hidden;
notifications = remaining;
notificationsRevision++;
}
return ids;
}

function restoreHiddenNotifications(ids) {
var values = Array.isArray(ids) ? ids : [];
for (var i = values.length - 1; i >= 0; i--)
restoreHiddenNotification(values[i]);
Comment thread
AnthonyPoschen marked this conversation as resolved.
}

function visibleNotifications(rows) {
var incoming = Array.isArray(rows) ? rows : [];
var hidden = hiddenNotifications || {};
Expand Down Expand Up @@ -341,6 +370,7 @@ Item {

actionStatusTimer.stop();
markingAllNotifications = true;
markingAllNotificationIds = hideAllNotifications();
notificationActionStatus = "Marking all notifications read…";
_markStdout = "";
_markStderr = "";
Expand Down Expand Up @@ -427,11 +457,14 @@ Item {
} else {
var fallback = all ? "Could not mark all notifications read." : "Could not mark notification read.";
root.notificationActionStatus = response && response.message ? String(response.message) : String(markErrors.text || root._markStderr || fallback).trim();
if (!all && markedId !== "")
if (all)
root.restoreHiddenNotifications(root.markingAllNotificationIds);
else if (markedId !== "")
root.restoreHiddenNotification(markedId);
}
root.markingNotificationId = "";
root.markingAllNotifications = false;
root.markingAllNotificationIds = [];
actionStatusTimer.restart();
if (root.startQueuedMark())
return ;
Expand Down
30 changes: 28 additions & 2 deletions tests/panel-source-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,34 @@ assert_contains 'visible: root.settingsOpen' \
"the settings page is always visible"
assert_contains $'pageFlip.stop()\n settingsOpen = false' \
"closing the panel leaves it on the settings page"
assert_contains $'PanelActionButton {\n visible: linkRow.showReadAction\n enabled: github.markingNotificationId !== linkRow.notificationId' \
"notification row marking is disabled during refresh"
assert_contains $'id: readActionStrip\n visible: linkRow.showReadAction\n anchors.right: parent.right\n anchors.top: parent.top\n anchors.bottom: parent.bottom\n width: Style.space(32)' \
"notification read target does not fill the row height at its right edge"
assert_contains $'anchors.right: readActionStrip.visible ? readActionStrip.left : parent.right\n anchors.verticalCenter: parent.verticalCenter\n anchors.leftMargin: Style.space(9)\n anchors.rightMargin: readActionStrip.visible ? 0 : Style.space(9)' \
"notification content does not meet the full-height read target"
assert_contains $'borderSpec: Border.none()\n\n HoverHandler {\n onHoveredChanged: if (hovered) root.selectKey(linkRow.cursorKey)\n }\n\n Rectangle {\n anchors.left: parent.left\n anchors.top: parent.top\n anchors.bottom: parent.bottom' \
"notification read target does not use a left-only divider"
assert_contains $'anchors.fill: parent\n enabled: github.markingNotificationId !== linkRow.notificationId' \
"notification read target does not fill its action strip"
assert_contains $'function notificationRows() {\n var page = Math.max(0, Math.min(notificationsPage, notificationPageCount() - 1))' \
"notifications are not paged in five-item windows"
assert_contains $'onPreviousPage: root.notificationsPage = Math.max(0, root.notificationsPage - 1)\n onNextPage: root.notificationsPage = Math.min(root.notificationPageCount() - 1, root.notificationsPage + 1)' \
"notification page controls do not clamp their range"
assert_contains $'model: root.notificationRows()\n showExpansionControl: false\n footerButtonsBordered: true\n page: root.notificationsPage' \
"notification pagination still shows an inactive expansion control"
assert_contains $'showReadAction: true\n showTrailingIndicator: false\n notificationId: String(modelData.id || "")' \
"notification rows retain an open-link indicator beside their read action"
assert_contains $'title: "ASSIGNED ISSUES"\n count: github.assignedIssues.length\n model: root.sectionRows(github.assignedIssues, root.issuesExpanded)\n expanded: root.issuesExpanded\n footerButtonsBordered: true\n openUrl: "https://github.com/issues/assigned"' \
"assigned-issues open control does not retain its matching border"
assert_contains 'readonly property bool showAction: section.count > 0 && section.actionText !== ""' \
"bulk notification action disappears while data is loading"
assert_contains 'enabled: section.actionEnabled && !section.actionBusy' \
"bulk notification action is not disabled until it is ready"
assert_contains $'text: "󰅁"\n tooltipText: "Previous notifications"' \
"previous notification page control is missing"
assert_contains $'text: "󰅂"\n tooltipText: "Next notifications"' \
"next notification page control is missing"
assert_contains $'text: (section.page + 1) + " / " + section.pageCount\n height: previousPageButton.height\n color: root.dim' \
"notification page number is not vertically centered with its controls"

assert_contains $'function applyPanelWheel(event) {\n if (!panelFlick || (sortPicker && sortPicker.popup.visible)) return false' \
"the panel still uses Flickable's default wheel distance"
Expand Down
10 changes: 10 additions & 0 deletions tests/service-source-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ assert_contains $'if (confirmed !== prepareMarkAllNotificationsRead()) {\n
"bulk marking does not verify the confirmed snapshot"
assert_contains $'var commandLine = [helperPath(), "--mark-all-read-before", String(snapshot.boundary || "")];\n for (var i = 0; i < snapshot.boundaryIds.length; i++)\n commandLine.push("--mark-boundary-notification", String(snapshot.boundaryIds[i]));' \
"bulk marking does not protect same-second arrivals"
assert_contains $'function hideAllNotifications() {\n var ids = [];\n var hidden = copyMap(hiddenNotifications);\n var remaining = [];\n for (var i = 0; i < notifications.length; i++) {' \
"bulk marking does not batch its optimistic removal"
assert_contains $'hiddenNotifications = hidden;\n notifications = remaining;\n notificationsRevision++;' \
"bulk marking repeatedly updates the notification model"
assert_contains $'function restoreHiddenNotifications(ids) {\n var values = Array.isArray(ids) ? ids : [];\n for (var i = values.length - 1; i >= 0; i--)' \
"failed bulk marking does not preserve notification order"
assert_contains $'markingAllNotifications = true;\n markingAllNotificationIds = hideAllNotifications();\n notificationActionStatus = "Marking all notifications read…";' \
"bulk marking does not provide immediate visible feedback"
assert_contains $'if (all)\n root.restoreHiddenNotifications(root.markingAllNotificationIds);\n else if (markedId !== "")' \
"failed bulk marking does not restore its displayed notifications"
assert_contains $'// GitHub is authoritative after every attempt. This reconciles\n // successful, failed, and partially completed bulk operations.\n root.refreshQueued = false;\n Qt.callLater(root.refresh);' \
"notification marking does not reconcile every result with an authoritative refresh"
assert_not_contains 'root.notifications = root.notifications.filter' \
Expand Down