feat: prefetch the next mobile update (Closes #757)
CI / lint (pull_request) Successful in 1m33s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-13 17:31:20 +00:00
1 parent 1a3df5d7a7
commit db08c7e75f
7 files changed
+141 -9

No files matched your search

+5
View File
@@ -912,6 +912,7 @@
});
const notificationReader = createNotificationReader({
load: fetchNotificationDetail,
getScope: () => confirmedOwnerLogin,
loadConversation: fetchNotificationConversation,
markRead: markNotificationRead,
acknowledge: acknowledgeNotification,
@@ -978,6 +979,9 @@
},
onStatus: message => {
qs('#update-sheet-status').textContent = message;
if (message === 'Update ready.' && updateTriage.active() && !offlineWorkMode) {
notificationReader.prefetch(updateTriage.next());
}
qs('#retry-update-load').hidden = !message.startsWith('Could not load update.');
if (message === 'Inbox cleared.') {
qs('#my-work-action-status').textContent = message;
@@ -1001,6 +1005,7 @@
progress.textContent = 'Update ' + state.index + ' of ' + state.total;
},
onFinish: () => {
notificationReader.prefetch();
qs('#update-triage-progress').hidden = true;
showMobileQueueCompletion('Updates');
},
+31 -6
View File
@@ -380,12 +380,27 @@ function createNotificationReader({
loadConversation = null,
onConversation = () => {},
createPager = typeof createConversationPager === 'function' ? createConversationPager : null,
getScope = () => '',
}) {
let selected = null;
let loadVersion = 0;
let marking = false;
let conversationPager = null;
let offlineHydrated = false;
let prefetched = null;
let prefetchKey = '';
function prefetch(item) {
if (!item) { prefetched = null; prefetchKey = ''; return false; }
const notificationId = item?.notification_id;
if (!Number.isInteger(notificationId) || offlineHydrated) return false;
const key = getScope() + notificationId;
if (prefetchKey === key) return prefetched;
if (prefetched) return false;
prefetchKey = key;
prefetched = load(notificationId);
return prefetched;
}
async function advanceAfterRead(items, current, queueing = false) {
const updated = acknowledgeNotification(items, current.notification_id);
@@ -407,10 +422,19 @@ function createNotificationReader({
selected = item;
offlineHydrated = Boolean(savedDetail && !Array.isArray(savedDetail));
const version = ++loadVersion;
const preload = !offlineHydrated && prefetchKey === getScope() + item.notification_id ? prefetched : null;
prefetched = null;
prefetchKey = '';
onOpen(item);
onStatus('Loading update…');
if (!preload) onStatus('Loading update…');
try {
const detail = offlineHydrated ? savedDetail : await load(item.notification_id);
let detail;
if (offlineHydrated) detail = savedDetail;
else if (preload) detail = await preload.catch(() => {
onStatus('Loading update…');
return load(item.notification_id);
});
else detail = await load(item.notification_id);
if (selected !== item || version !== loadVersion) return false;
onDetail(detail);
if (createPager && loadConversation && detail.conversation) {
@@ -433,6 +457,7 @@ function createNotificationReader({
return {
open,
prefetch,
commentPager() {
return conversationPager;
},
@@ -516,7 +541,7 @@ function createNotificationReplier({
if ((storage.getItem(keyFor(item)) || '') !== body) storage.removeItem(operationKeyFor(item));
storage.setItem(keyFor(item), body);
}
catch (_error) { /* Keep the editable textarea as the fallback. */ }
catch (_error) {}
},
async submit(item, body, attachment = null) {
if (pending) return false;
@@ -542,15 +567,15 @@ function createNotificationReplier({
return { queued:true };
}
try { storage.removeItem(keyFor(item)); storage.removeItem(operationKeyFor(item)); }
catch (_error) { /* Confirmed delivery is authoritative. */ }
catch (_error) {}
onStatus('Reply posted. You can mark this update read when ready.');
return delivery.confirmed[0];
}
const result = await post(item.notification_id, body, operationId);
try { storage.removeItem(keyFor(item)); }
catch (_error) { /* The posted reply is still authoritative. */ }
catch (_error) {}
try { storage.removeItem(operationKeyFor(item)); }
catch (_error) { /* A confirmed result no longer needs replay identity. */ }
catch (_error) {}
onStatus('Reply posted. You can mark this update read when ready.');
return result;
} catch (error) {
+7
View File
@@ -35,6 +35,12 @@
.map(id => byIdentity.get(id));
}
function nextAvailable() {
if (!running || !state) return null;
const candidates = available().filter(item => identity(item) !== state.current);
return candidates.find(item => state.identities.indexOf(identity(item)) > state.identities.indexOf(state.current)) || candidates[0] || null;
}
function finish() {
running = false;
state = null;
@@ -94,6 +100,7 @@
completeAndNext: advance,
acceptCompleted: () => advance(false),
keepUnreadAndNext: advance,
next: nextAvailable,
items: () => state ? available().slice() : [],
end: finish,
};