feat: paginate unread updates inbox (#147)
This commit is contained in:
1 parent
953f59fb19
commit
a86de5cbf4
9 files changed
+350
-31
No files matched your search
+67
-5
@@ -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%; }
|
||||
.load-more-notifications { min-height:44px; width:100%; margin-top:10px; }
|
||||
.load-more-notifications[hidden] { display:none; }
|
||||
.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; }
|
||||
@@ -141,6 +143,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-work-list" id="my-work-list"></div>
|
||||
<div class="small" id="notification-page-status" aria-live="polite"></div>
|
||||
<button class="load-more-notifications" id="load-more-notifications" type="button" hidden>Load older updates</button>
|
||||
<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>
|
||||
@@ -318,6 +322,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
let lastMyWork = [];
|
||||
let lastNotifications = [];
|
||||
let lastContextSnapshot = null;
|
||||
let notificationPagination = { page: 1, total: 0, has_more: false };
|
||||
let hasContextSnapshot = false;
|
||||
let selectedReview = null;
|
||||
let reviewTrigger = null;
|
||||
@@ -367,6 +373,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function fetchNotificationPage(page) {
|
||||
const response = await fetch('api/v1/notifications?page=' + encodeURIComponent(page), {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || 'Loading unread updates failed.');
|
||||
return payload;
|
||||
}
|
||||
|
||||
const notificationAcknowledger = createNotificationAcknowledger({
|
||||
markRead: markNotificationRead,
|
||||
onItems: items => {
|
||||
@@ -383,10 +398,30 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
},
|
||||
onStatus: message => { qs('#my-work-action-status').textContent = message; },
|
||||
});
|
||||
const notificationPager = createNotificationPager({
|
||||
load: fetchNotificationPage,
|
||||
onNotifications: items => {
|
||||
lastNotifications = items;
|
||||
if (lastContextSnapshot) {
|
||||
lastContextSnapshot.notifications = lastNotifications;
|
||||
paintMyWork(lastContextSnapshot);
|
||||
}
|
||||
},
|
||||
onPagination: pagination => {
|
||||
notificationPagination = pagination;
|
||||
const loaded = Math.min(pagination.total, pagination.page * 50);
|
||||
qs('#notification-page-status').textContent = pagination.total ?
|
||||
loaded + ' of ' + pagination.total + ' unread updates loaded.' : '';
|
||||
qs('#load-more-notifications').hidden =
|
||||
selectedWorkFilter !== 'update' || !pagination.has_more;
|
||||
},
|
||||
onStatus: message => { qs('#my-work-action-status').textContent = message; },
|
||||
});
|
||||
|
||||
function renderContextSnapshot(data) {
|
||||
liveMode = true;
|
||||
hasContextSnapshot = true;
|
||||
lastContextSnapshot = data;
|
||||
if (data.error && lastMyWork.length) markMyWorkStale();
|
||||
else paintMyWork(data);
|
||||
qs('#context').innerHTML = '<div class="kv"><div class="label">User</div><div class="value">' + escapeHtml(data.user?.full_name || data.user?.login || '—') + '</div>' +
|
||||
@@ -476,14 +511,20 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
});
|
||||
});
|
||||
const ids = notificationIds(visible);
|
||||
const allIds = notificationIds(visible);
|
||||
const ids = allIds.slice(0, Math.min(lastNotifications.length, 50));
|
||||
const bulkBar = qs('#bulk-mark-read-bar');
|
||||
const bulkButton = qs('#bulk-mark-read');
|
||||
bulkBar.hidden = selectedWorkFilter !== 'update' || ids.length === 0;
|
||||
qs('#load-more-notifications').hidden =
|
||||
selectedWorkFilter !== 'update' || !notificationPagination.has_more;
|
||||
bulkButton.disabled = bulkMarkPending;
|
||||
const bulkLabel = allIds.length > ids.length ?
|
||||
'next ' + ids.length + ' of ' + allIds.length + ' loaded updates' :
|
||||
'all ' + ids.length + ' updates';
|
||||
bulkButton.textContent = bulkConfirmationPending ?
|
||||
'Confirm marking ' + ids.length + ' updates read' :
|
||||
'Mark all ' + ids.length + ' updates read';
|
||||
'Confirm marking ' + bulkLabel + ' read' :
|
||||
'Mark ' + bulkLabel + ' read';
|
||||
}
|
||||
|
||||
function reviewFileElement(filename) {
|
||||
@@ -645,7 +686,24 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
|
||||
function renderLiveSnapshot(snapshot) {
|
||||
const notificationsFresh = Array.isArray(snapshot.notifications);
|
||||
if (notificationsFresh) lastNotifications = snapshot.notifications;
|
||||
if (notificationsFresh) {
|
||||
if (notificationPagination.page > 1) {
|
||||
const byId = new Map(lastNotifications.map(item => [item.id, item]));
|
||||
snapshot.notifications.forEach(item => byId.set(item.id, item));
|
||||
lastNotifications = Array.from(byId.values());
|
||||
} else {
|
||||
lastNotifications = snapshot.notifications;
|
||||
}
|
||||
if (snapshot.notification_pagination) {
|
||||
const page = notificationPagination.page > 1 ? notificationPagination.page :
|
||||
snapshot.notification_pagination.page;
|
||||
notificationPager.reset({
|
||||
page,
|
||||
total: snapshot.notification_pagination.total,
|
||||
has_more: page * 50 < snapshot.notification_pagination.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (snapshot.context) {
|
||||
snapshot.context.notifications = lastNotifications;
|
||||
renderContextSnapshot(snapshot.context);
|
||||
@@ -793,8 +851,12 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
function load() { return contextPoller.refresh(); }
|
||||
|
||||
qs('#refresh').addEventListener('click', load);
|
||||
qs('#load-more-notifications').addEventListener('click', () =>
|
||||
notificationPager.loadMore(lastNotifications)
|
||||
);
|
||||
qs('#bulk-mark-read').addEventListener('click', async () => {
|
||||
const ids = notificationIds(filterMyWork(lastMyWork, 'update'));
|
||||
const allIds = notificationIds(filterMyWork(lastMyWork, 'update'));
|
||||
const ids = allIds.slice(0, 50);
|
||||
if (!ids.length || bulkMarkPending) return;
|
||||
if (!bulkConfirmationPending) {
|
||||
bulkConfirmationPending = true;
|
||||
|
||||
Reference in new issue
Block a user