feat: queue authored messages for reconnect (#234)
This commit is contained in:
1 parent
8df41e2fe7
commit
d66c1b48ef
8 files changed
+381
-14
No files matched your search
+60
-7
@@ -719,6 +719,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<script src="static/widgets.js"></script>
|
||||
<script src="static/drafts.js"></script>
|
||||
<script src="static/issue-outbox.js"></script>
|
||||
<script src="static/authored-outbox.js"></script>
|
||||
<script src="static/offline-work.js"></script>
|
||||
<script src="static/my-work.js"></script>
|
||||
<script src="static/pick-work.js"></script>
|
||||
@@ -830,6 +831,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
});
|
||||
const issueCapture = createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||
const issueOutbox = createIssueOutbox({ storage: localStorage, fetchJson: fetchReviewJson });
|
||||
const authoredOutbox = createAuthoredOutbox({ storage: localStorage, fetchJson: fetchReviewJson });
|
||||
const shareParams = new URLSearchParams(location.search);
|
||||
const sharedLaunch = {
|
||||
title: shareParams.get('title') || '',
|
||||
@@ -985,6 +987,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
const notificationReplier = createNotificationReplier({
|
||||
post: postNotificationReply,
|
||||
storage: localStorage,
|
||||
authoredOutbox,
|
||||
onStatus: message => { qs('#update-reply-status').textContent = message; },
|
||||
});
|
||||
const notificationReader = createNotificationReader({
|
||||
@@ -1251,13 +1254,18 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
function renderDrafts() {
|
||||
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' ?
|
||||
'<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>' :
|
||||
item.kind === 'authored-outbox' ?
|
||||
'<button class="draft-resume" data-draft-index="' + index + '" type="button">Open message</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>' :
|
||||
'<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 = item.kind === 'issue-outbox' ?
|
||||
const state = isOutbox ?
|
||||
'<span class="pill">' + (item.status === 'attention' ? 'Needs attention' : 'Queued for sync') + '</span>' : '';
|
||||
return '<article class="my-work-card draft-card">' +
|
||||
'<span class="small">' + escapeHtml(item.label) + (item.repository ? ' · ' + escapeHtml(item.repository) : '') + '</span>' +
|
||||
@@ -1290,7 +1298,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
const item = lastDrafts[Number(button.dataset.draftIndex)];
|
||||
if (!item?.outbox_id) return;
|
||||
button.disabled = true;
|
||||
applyOutboxResult(await issueOutbox.retry(item.outbox_id));
|
||||
if (item.kind === 'authored-outbox') applyAuthoredOutboxResult(await authoredOutbox.retry(item.outbox_id));
|
||||
else applyOutboxResult(await issueOutbox.retry(item.outbox_id));
|
||||
});
|
||||
});
|
||||
list.querySelectorAll('.draft-discard').forEach(button => {
|
||||
@@ -1298,6 +1307,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
if (!window.confirm('Discard this unfinished draft?')) return;
|
||||
const item = lastDrafts[Number(button.dataset.draftIndex)];
|
||||
if (item?.kind === 'issue-outbox') issueOutbox.discard(item.outbox_id);
|
||||
else if (item?.kind === 'authored-outbox') authoredOutbox.discard(item.outbox_id);
|
||||
else if (item) draftInbox.discard(item.id);
|
||||
lastDrafts = draftInbox.list();
|
||||
const count = qs('[data-work-count="draft"]');
|
||||
@@ -1970,6 +1980,25 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
applyOutboxResult(await issueOutbox.flush());
|
||||
}
|
||||
|
||||
function applyAuthoredOutboxResult(result) {
|
||||
refreshMyWorkView();
|
||||
const attention = (result.remaining || []).some(item => item.status === 'attention');
|
||||
qs('#my-work-action-status').textContent = result.confirmed?.length ?
|
||||
result.confirmed.length + ' queued message' + (result.confirmed.length === 1 ? '' : 's') + ' sent.' :
|
||||
attention ? 'A queued message needs attention. Open Drafts to edit or discard it.' :
|
||||
'Message queued for sync when the connection returns.';
|
||||
}
|
||||
|
||||
async function flushAuthoredOutbox() {
|
||||
if (!navigator.onLine || !authoredOutbox.list().length) return;
|
||||
applyAuthoredOutboxResult(await authoredOutbox.flush());
|
||||
}
|
||||
|
||||
function canQueueMessage(error) {
|
||||
const status = Number(error?.status || 0);
|
||||
return !status || status >= 500;
|
||||
}
|
||||
|
||||
function inlineAnchor(target) {
|
||||
return {
|
||||
path: target.dataset.reviewFilename,
|
||||
@@ -2802,8 +2831,17 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#issue-comment').value = '';
|
||||
qs('#issue-comment-status').textContent = 'Comment posted.';
|
||||
} catch (error) {
|
||||
qs('#issue-comment-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||
qs('#issue-comment').focus();
|
||||
if (canQueueMessage(error)) {
|
||||
const operationId = localStorage.getItem('stackchain.issue-comment.v1:' + selectedIssue.repository + '#' + selectedIssue.number + ':operation');
|
||||
authoredOutbox.enqueue({ kind:'issue-comment', repository:selectedIssue.repository,
|
||||
number:selectedIssue.number, body, operationId });
|
||||
qs('#issue-comment').value = '';
|
||||
qs('#issue-comment-status').textContent = 'Queued for sync when the connection returns.';
|
||||
refreshMyWorkView();
|
||||
} else {
|
||||
qs('#issue-comment-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||
qs('#issue-comment').focus();
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
@@ -2894,8 +2932,17 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#pull-comment').value = '';
|
||||
qs('#pull-comment-status').textContent = 'Comment posted.';
|
||||
} catch (error) {
|
||||
qs('#pull-comment-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||
qs('#pull-comment').focus();
|
||||
if (canQueueMessage(error)) {
|
||||
const operationId = localStorage.getItem('stackchain.pull-comment.v1:' + selectedPull.repository + '#' + selectedPull.number + ':operation');
|
||||
authoredOutbox.enqueue({ kind:'pull-comment', repository:selectedPull.repository,
|
||||
number:selectedPull.number, body, operationId });
|
||||
qs('#pull-comment').value = '';
|
||||
qs('#pull-comment-status').textContent = 'Queued for sync when the connection returns.';
|
||||
refreshMyWorkView();
|
||||
} else {
|
||||
qs('#pull-comment-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||
qs('#pull-comment').focus();
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
@@ -2948,7 +2995,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#send-update-reply').disabled = true;
|
||||
const result = await notificationReplier.submit(selectedUpdate, body);
|
||||
qs('#send-update-reply').disabled = false;
|
||||
if (result) {
|
||||
if (result?.queued) {
|
||||
qs('#update-reply').value = '';
|
||||
refreshMyWorkView();
|
||||
qs('#my-work-action-status').textContent = 'Reply queued for sync.';
|
||||
} else if (result) {
|
||||
notificationReader.appendReply(result);
|
||||
qs('#update-reply').value = '';
|
||||
qs('#mark-update-read-next').focus();
|
||||
@@ -3152,6 +3203,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
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', () => {
|
||||
@@ -3237,6 +3289,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
contextPoller.start();
|
||||
flushIssueOutbox();
|
||||
flushAuthoredOutbox();
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
contextPoller.setVisible(!document.hidden);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user