feat: bulk acknowledge unread updates (#141)
CI / lint (pull_request) Successful in 11s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-06 21:55:32 +00:00
1 parent cafc062b9f
commit db9e51e683
6 files changed
+246

No files matched your search

+55
View File
@@ -66,6 +66,8 @@ textarea { resize: vertical; min-height: 120px; }
.my-work-card:hover { border-color:var(--accent); }
.my-work-card-title { display:block; margin:5px 0; font-weight:650; }
.mark-update-read { min-height:44px; width:100%; }
.my-work-bulk { position:sticky; bottom:0; z-index:4; margin:10px -4px -12px; padding:10px 4px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
.my-work-bulk button { min-height:44px; width:100%; }
.my-work[data-stale="true"] { border-color:#fcd34d; }
.review-sheet { position:fixed; inset:0; z-index:50; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
.review-sheet.open { display:flex; }
@@ -138,6 +140,9 @@ textarea { resize: vertical; min-height: 120px; }
</div>
<div class="my-work-list" id="my-work-list"></div>
<div class="small" id="my-work-action-status" aria-live="assertive"></div>
<div class="my-work-bulk" id="bulk-mark-read-bar" hidden>
<button id="bulk-mark-read" type="button"></button>
</div>
</section>
<aside class="sidebar">
<details class="panel stack" data-panel-key="context" open>
@@ -316,6 +321,8 @@ textarea { resize: vertical; min-height: 120px; }
let progress = null;
let draft = null;
let reviewFiles = [];
let bulkConfirmationPending = false;
let bulkMarkPending = false;
async function fetchReviewJson(url, options) {
const response = await fetch(url, options);
@@ -345,6 +352,17 @@ textarea { resize: vertical; min-height: 120px; }
return payload;
}
async function markNotificationsRead(ids) {
const response = await fetch('api/v1/notifications/read', {
method: 'PATCH',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || 'Bulk mark read failed.');
return payload;
}
const notificationAcknowledger = createNotificationAcknowledger({
markRead: markNotificationRead,
onItems: items => {
@@ -353,6 +371,14 @@ textarea { resize: vertical; min-height: 120px; }
},
onStatus: message => { qs('#my-work-action-status').textContent = message; },
});
const bulkNotificationAcknowledger = createBulkNotificationAcknowledger({
markRead: markNotificationsRead,
onItems: items => {
lastMyWork = items;
refreshMyWorkView();
},
onStatus: message => { qs('#my-work-action-status').textContent = message; },
});
function renderContextSnapshot(data) {
liveMode = true;
@@ -446,6 +472,14 @@ textarea { resize: vertical; min-height: 120px; }
}
});
});
const ids = notificationIds(visible);
const bulkBar = qs('#bulk-mark-read-bar');
const bulkButton = qs('#bulk-mark-read');
bulkBar.hidden = selectedWorkFilter !== 'update' || ids.length === 0;
bulkButton.disabled = bulkMarkPending;
bulkButton.textContent = bulkConfirmationPending ?
'Confirm marking ' + ids.length + ' updates read' :
'Mark all ' + ids.length + ' updates read';
}
function reviewFileElement(filename) {
@@ -734,6 +768,27 @@ textarea { resize: vertical; min-height: 120px; }
function load() { return contextPoller.refresh(); }
qs('#refresh').addEventListener('click', load);
qs('#bulk-mark-read').addEventListener('click', async () => {
const ids = notificationIds(filterMyWork(lastMyWork, 'update'));
if (!ids.length || bulkMarkPending) return;
if (!bulkConfirmationPending) {
bulkConfirmationPending = true;
qs('#my-work-action-status').textContent = 'Confirm to mark all visible updates read.';
renderMyWork();
return;
}
bulkConfirmationPending = false;
bulkMarkPending = true;
renderMyWork();
const result = await bulkNotificationAcknowledger.acknowledge(lastMyWork, ids);
if (result) {
const marked = new Set(result.marked);
lastNotifications = lastNotifications.filter(item => !marked.has(item.id));
}
bulkMarkPending = false;
renderMyWork();
(document.querySelector('[data-notification-id]') || qs('[data-work-filter="update"]'))?.focus();
});
document.querySelectorAll('[data-work-filter]').forEach(button => {
button.setAttribute('aria-pressed', String(button.dataset.workFilter === selectedWorkFilter));
button.addEventListener('click', () => {
+40
View File
@@ -91,6 +91,44 @@ function createNotificationAcknowledger({ markRead, onItems, onStatus }) {
};
}
function notificationIds(items) {
return Array.from(new Set((items || [])
.filter((item) => item && item.has_update && Number.isInteger(item.notification_id))
.map((item) => item.notification_id)));
}
function createBulkNotificationAcknowledger({ markRead, onItems, onStatus }) {
let pending = false;
return {
async acknowledge(items, requestedIds) {
if (pending) return false;
const ids = Array.from(new Set((requestedIds || []).filter(Number.isInteger)));
if (!ids.length) return false;
pending = true;
onStatus('Marking ' + ids.length + ' updates read…');
try {
const result = await markRead(ids);
const marked = (result.marked || []).filter(Number.isInteger);
const failed = (result.failed || []).filter(Number.isInteger);
const updated = marked.reduce(
(current, notificationId) => acknowledgeNotification(current, notificationId),
items
);
onItems(updated);
onStatus(failed.length ?
marked.length + ' marked read · ' + failed.length + ' could not be updated — retry.' :
marked.length + ' updates marked read.');
return { marked, failed };
} catch (_error) {
onStatus('Could not mark updates read. Retry.');
return false;
} finally {
pending = false;
}
},
};
}
function filterMyWork(items, selectedFilter) {
if (selectedFilter === 'all') return items;
if (selectedFilter === 'review') return items.filter((item) => item.is_review);
@@ -124,5 +162,7 @@ if (typeof module !== 'undefined' && module.exports) {
buildMyWork.countMyWork = countMyWork;
buildMyWork.acknowledgeNotification = acknowledgeNotification;
buildMyWork.createNotificationAcknowledger = createNotificationAcknowledger;
buildMyWork.notificationIds = notificationIds;
buildMyWork.createBulkNotificationAcknowledger = createBulkNotificationAcknowledger;
module.exports = buildMyWork;
}