feat: bind offline outboxes to Gitea identity (#238)
CI / lint (pull_request) Successful in 19s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-07 22:57:49 +00:00
1 parent d155f0081f
commit a73b3bc773
9 files changed
+237 -63

No files matched your search

+39 -16
View File
@@ -847,6 +847,8 @@ textarea { resize: vertical; min-height: 120px; }
let bulkMarkPending = false;
let reviewHandoffPending = false;
let editingOutboxId = null;
let confirmedOwnerLogin = '';
let activeFlushLogin = '';
async function fetchReviewJson(url, options) {
const response = await fetch(url, options);
@@ -876,8 +878,12 @@ textarea { resize: vertical; min-height: 120px; }
loadMilestones: item => issueController.loadMilestones(item),
});
const issueCapture = createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage });
const issueOutbox = createIssueOutbox({ storage: localStorage, fetchJson: fetchReviewJson });
const authoredOutbox = createAuthoredOutbox({ storage: localStorage, fetchJson: fetchReviewJson });
const issueOutbox = createIssueOutbox({
storage: localStorage, fetchJson: fetchReviewJson, getOwnerLogin: () => confirmedOwnerLogin,
});
const authoredOutbox = createAuthoredOutbox({
storage: localStorage, fetchJson: fetchReviewJson, getOwnerLogin: () => confirmedOwnerLogin,
});
const shareParams = new URLSearchParams(location.search);
const sharedLaunch = {
title: shareParams.get('title') || '',
@@ -887,7 +893,7 @@ textarea { resize: vertical; min-height: 120px; }
let sharedLaunchState = Object.values(sharedLaunch).some(Boolean) ?
issueCapture.stageSharedContent(sharedLaunch) : null;
const pullController = createPullSheet({ fetchJson: fetchReviewJson, storage: localStorage });
const draftInbox = createDraftInbox({ storage: localStorage });
const draftInbox = createDraftInbox({ storage: localStorage, getCurrentLogin: () => activeFlushLogin });
const offlineWorkStore = createOfflineWorkStore({ storage: localStorage });
const findWorkController = createFindWork({
fetchJson: fetchReviewJson,
@@ -1242,6 +1248,7 @@ textarea { resize: vertical; min-height: 120px; }
function handleContextError(e) {
console.error('context failed', e);
liveMode = false;
activeFlushLogin = '';
if (!hasContextSnapshot && hydrateOfflineWork('outage')) return;
setStatus(hasContextSnapshot ? 'Update failed · showing last snapshot' : 'Unavailable');
if (!hasContextSnapshot) {
@@ -1301,7 +1308,10 @@ textarea { resize: vertical; min-height: 120px; }
const list = qs('#my-work-list');
list.innerHTML = lastDrafts.length ? lastDrafts.map((item, index) => {
const isOutbox = item.kind === 'issue-outbox' || item.kind === 'authored-outbox';
const outboxActions = item.kind === 'issue-outbox' ?
const outboxActions = item.quarantined ?
'<button class="draft-copy" data-draft-index="' + index + '" type="button">Copy content</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard</button>' :
item.kind === 'issue-outbox' ?
'<button class="draft-edit" data-draft-index="' + index + '" type="button">Edit</button>' +
'<button class="draft-send" data-draft-index="' + index + '" type="button">Send now</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard</button>' :
@@ -1312,7 +1322,9 @@ textarea { resize: vertical; min-height: 120px; }
'<button class="draft-resume" data-draft-index="' + index + '" type="button">Resume draft</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard draft</button>';
const state = isOutbox ?
'<span class="pill">' + (item.status === 'attention' ? 'Needs attention' : 'Queued for sync') + '</span>' : '';
'<span class="pill">' + (item.quarantined ? 'Identity protected' :
(item.status === 'attention' ? 'Needs attention' : 'Queued for sync')) + '</span>' +
(item.ownership ? '<div class="small">' + escapeHtml(item.ownership) + '</div>' : '') : '';
return '<article class="my-work-card draft-card">' +
'<span class="small">' + escapeHtml(item.label) + (item.repository ? ' · ' + escapeHtml(item.repository) : '') + '</span>' +
'<span class="my-work-card-title">' + escapeHtml(item.title) + '</span>' +
@@ -1344,8 +1356,16 @@ textarea { resize: vertical; min-height: 120px; }
const item = lastDrafts[Number(button.dataset.draftIndex)];
if (!item?.outbox_id) return;
button.disabled = true;
if (item.kind === 'authored-outbox') applyAuthoredOutboxResult(await authoredOutbox.retry(item.outbox_id));
else applyOutboxResult(await issueOutbox.retry(item.outbox_id));
if (item.kind === 'authored-outbox') applyAuthoredOutboxResult(await authoredOutbox.retry(item.outbox_id, activeFlushLogin));
else applyOutboxResult(await issueOutbox.retry(item.outbox_id, activeFlushLogin));
});
});
list.querySelectorAll('.draft-copy').forEach(button => {
button.addEventListener('click', async () => {
const item = lastDrafts[Number(button.dataset.draftIndex)];
if (!item?.copy_text) return;
await navigator.clipboard.writeText(item.copy_text);
qs('#my-work-action-status').textContent = 'Queued content copied without sending it.';
});
});
list.querySelectorAll('.draft-discard').forEach(button => {
@@ -2022,8 +2042,8 @@ textarea { resize: vertical; min-height: 120px; }
}
async function flushIssueOutbox() {
if (!navigator.onLine || !issueOutbox.list().length) return;
applyOutboxResult(await issueOutbox.flush());
if (!navigator.onLine || !activeFlushLogin || !issueOutbox.list().length) return;
applyOutboxResult(await issueOutbox.flush(activeFlushLogin));
}
function applyAuthoredOutboxResult(result) {
@@ -2036,8 +2056,8 @@ textarea { resize: vertical; min-height: 120px; }
}
async function flushAuthoredOutbox() {
if (!navigator.onLine || !authoredOutbox.list().length) return;
applyAuthoredOutboxResult(await authoredOutbox.flush());
if (!navigator.onLine || !activeFlushLogin || !authoredOutbox.list().length) return;
applyAuthoredOutboxResult(await authoredOutbox.flush(activeFlushLogin));
}
function canQueueMessage(error) {
@@ -2259,6 +2279,10 @@ textarea { resize: vertical; min-height: 120px; }
}
if (snapshot.context) {
setOfflineWorkMode(false);
const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale &&
!contextFreshness?.degraded && !contextFreshness?.revalidating;
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
if (activeFlushLogin) confirmedOwnerLogin = activeFlushLogin;
snapshot.context.notifications = lastNotifications;
renderContextSnapshot(snapshot.context);
if (contextFreshness?.stale) markMyWorkStale();
@@ -2271,6 +2295,8 @@ textarea { resize: vertical; min-height: 120px; }
});
updateOfflineWorkControls();
}
flushIssueOutbox();
flushAuthoredOutbox();
} else handleContextError(new Error('Context section unavailable'));
if (Array.isArray(snapshot.events)) paintEventStream(snapshot.events);
if (eventsFreshness?.revalidating) {
@@ -2669,7 +2695,7 @@ textarea { resize: vertical; min-height: 120px; }
closeCreateIssueSheet();
refreshMyWorkView();
qs('#my-work-action-status').textContent = 'Queued for sync.';
if (navigator.onLine) applyOutboxResult(await issueOutbox.retry(queued.id), true);
if (navigator.onLine) applyOutboxResult(await issueOutbox.retry(queued.id, activeFlushLogin), true);
} catch (error) {
qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.';
button.disabled = false;
@@ -3222,6 +3248,7 @@ textarea { resize: vertical; min-height: 120px; }
return true;
}
function showOfflineStatus() {
activeFlushLogin = '';
offlineStatus.hidden = false;
setStatus('Offline');
if (!hasContextSnapshot) hydrateOfflineWork();
@@ -3248,8 +3275,6 @@ textarea { resize: vertical; min-height: 120px; }
if (!navigator.onLine) showOfflineStatus();
window.addEventListener('offline', showOfflineStatus);
window.addEventListener('online', reconnectLiveData);
window.addEventListener('online', flushIssueOutbox);
window.addEventListener('online', flushAuthoredOutbox);
qs('#refresh').addEventListener('click', load);
qs('#start-work-session').addEventListener('click', () => {
@@ -3334,8 +3359,6 @@ textarea { resize: vertical; min-height: 120px; }
);
}
contextPoller.start();
flushIssueOutbox();
flushAuthoredOutbox();
document.addEventListener('visibilitychange', () => {
contextPoller.setVisible(!document.hidden);
});