diff --git a/frontend/conversation-action-hydrator.js b/frontend/conversation-action-hydrator.js
new file mode 100644
index 0000000..1b22d6e
--- /dev/null
+++ b/frontend/conversation-action-hydrator.js
@@ -0,0 +1,49 @@
+function createConversationActionHydrator({ load, activate }) {
+ let actions = null;
+ let pending = null;
+ const wiredRoots = new WeakSet();
+
+ function ensure() {
+ if (actions) return Promise.resolve(actions);
+ if (!pending) {
+ pending = load().then(() => {
+ actions = activate();
+ return actions;
+ }).catch(error => {
+ pending = null;
+ throw error;
+ });
+ }
+ return pending;
+ }
+
+ function show({ root, state, paint, wire, retry }) {
+ if (actions) {
+ retry.hidden = true;
+ if (!wiredRoots.has(root)) {
+ wire(actions);
+ wiredRoots.add(root);
+ }
+ paint(state, actions);
+ return Promise.resolve(true);
+ }
+
+ paint(state, null);
+ retry.hidden = true;
+ return ensure().then(controller => {
+ if (!wiredRoots.has(root)) {
+ wire(controller);
+ wiredRoots.add(root);
+ }
+ paint(state, controller);
+ return true;
+ }).catch(() => {
+ retry.hidden = false;
+ return false;
+ });
+ }
+
+ return { show, ready: () => Boolean(actions) };
+}
+
+if (typeof module !== 'undefined' && module.exports) module.exports = createConversationActionHydrator;
diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index 61d669e..b06d92a 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -696,6 +696,7 @@ textarea { resize: vertical; min-height: 120px; }
.pull-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
.pull-file, .pull-comment-card { margin:8px 0; padding:10px; border:1px solid #203a5c; border-radius:10px; }
.conversation-more { min-height:44px; width:100%; margin:8px 0; }
+.conversation-actions-retry { min-height:44px; max-width:100%; margin:8px 0; }
.pull-file-toggle, .pull-review-file { min-height:44px; width:100%; }
.pull-file-toggle { display:flex; justify-content:space-between; align-items:center; gap:8px; text-align:left; }
.pull-review-file { margin-top:8px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index c6e2fc9..99f9cd7 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -614,14 +614,16 @@
'comment-actions': document.querySelector('meta[name="stackchain-feature-comment-actions"]')?.content || '',
},
});
- await commentActionFeatures.run('comment-actions', {
- status: qs('#my-work-action-status'), retryLabel:'Reload to retry comment actions.',
- }, () => {
- commentActions = createCommentActions({
- fetchJson: fetchReviewJson,
- getLogin: () => confirmedOwnerLogin,
- confirmDelete: message => window.confirm(message),
- });
+ const commentActionHydrator = createConversationActionHydrator({
+ load: () => commentActionFeatures.load('comment-actions'),
+ activate: () => {
+ commentActions = createCommentActions({
+ fetchJson: fetchReviewJson,
+ getLogin: () => confirmedOwnerLogin,
+ confirmDelete: message => window.confirm(message),
+ });
+ return commentActions;
+ },
});
const issueCaptureFeatures = createFeatureLoader({
document,
@@ -3100,8 +3102,8 @@
toggle?.focus();
}
- function renderIssueComment(comment) {
- const actions = commentActions.actionHtml?.(comment) || '';
+ function renderIssueComment(comment, controller = commentActions) {
+ const actions = controller?.actionHtml?.(comment) || '';
return '
';
}
- function renderIssueConversation(state) {
+ function paintIssueConversation(state, controller) {
const comments = state?.comments || [];
qs('#issue-comments').innerHTML = comments.length ?
- comments.map(renderIssueComment).join('') : 'No comments yet.
';
+ comments.map(comment => renderIssueComment(comment, controller)).join('') : 'No comments yet.
';
qs('#load-older-issue-comments').hidden = !Number.isInteger(state?.older_page);
qs('#issue-conversation-status').textContent = comments.length ?
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
}
- function renderUpdateConversation(state) {
+ function paintUpdateConversation(state, controller) {
const comments = state?.comments || [];
qs('#update-comments').innerHTML = comments.length ?
- comments.map(renderIssueComment).join('') : 'No comments yet.
';
+ comments.map(comment => renderIssueComment(comment, controller)).join('') : 'No comments yet.
';
qs('#load-older-update-comments').hidden = !Number.isInteger(state?.older_page);
qs('#update-conversation-status').textContent = comments.length ?
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
@@ -3129,10 +3131,10 @@
updateReadPosition.ready(String(selectedUpdate?.notification_id || ''), newest);
}
- function renderPullConversation(state) {
+ function paintPullConversation(state, controller) {
const comments = state?.comments || [];
qs('#pull-comments').innerHTML = comments.length ? comments.map(comment =>
- ''
+ ''
).join('') : 'No comments yet.
';
qs('#load-older-pull-comments').hidden = !Number.isInteger(state?.older_page);
qs('#pull-conversation-status').textContent = comments.length ?
@@ -3154,16 +3156,47 @@
};
}
- function wireCommentActions(selector) {
- commentActions.wire({
+ function wireCommentActions(selector, controller = commentActions) {
+ controller.wire({
root:qs(selector), getSurface:()=>commentSurface(selector),
isOffline:()=>offlineWorkMode || navigator.onLine === false, escapeHtml,
});
}
- wireCommentActions('#issue-comments');
- wireCommentActions('#pull-comments');
- wireCommentActions('#update-comments');
+ const conversationActionSurfaces = {
+ issue: { selector:'#issue-comments', paint:paintIssueConversation },
+ pull: { selector:'#pull-comments', paint:paintPullConversation },
+ update: { selector:'#update-comments', paint:paintUpdateConversation },
+ };
+ const latestConversationStates = {};
+
+ function wireConversationActions(kind) {
+ if (kind === 'issue') wireCommentActions('#issue-comments');
+ if (kind === 'pull') wireCommentActions('#pull-comments');
+ if (kind === 'update') wireCommentActions('#update-comments');
+ }
+
+ function showConversationWithActions(kind, state) {
+ const surface = conversationActionSurfaces[kind];
+ latestConversationStates[kind] = state;
+ return commentActionHydrator.show({
+ root:qs(surface.selector), state, paint:surface.paint,
+ retry:qs('#retry-' + kind + '-comment-actions'),
+ wire:controller => wireConversationActions(kind, controller),
+ });
+ }
+
+ function renderIssueConversation(state) { void showConversationWithActions('issue', state); }
+ function renderPullConversation(state) { void showConversationWithActions('pull', state); }
+ function renderUpdateConversation(state) { void showConversationWithActions('update', state); }
+
+ function retryConversationActions(kind) {
+ const state = latestConversationStates[kind];
+ if (state) void showConversationWithActions(kind, state);
+ }
+ qs('#retry-issue-comment-actions').addEventListener('click', () => retryConversationActions('issue'));
+ qs('#retry-pull-comment-actions').addEventListener('click', () => retryConversationActions('pull'));
+ qs('#retry-update-comment-actions').addEventListener('click', () => retryConversationActions('update'));
function renderIssueLabelEditor(item, confirmedNames, labels) {
const list = qs('#issue-label-list');
diff --git a/frontend/index.html b/frontend/index.html
index 85efcdc..c4be4d2 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -595,6 +595,7 @@
+