function createCommentActions({ fetchJson, getLogin, confirmDelete = () => false }) { const reactionLabels = { '+1': 'Thumbs up', '-1': 'Thumbs down', laugh: 'Laugh', hooray: 'Hooray', confused: 'Confused', heart: 'Heart', rocket: 'Rocket', eyes: 'Eyes', }; const encodedRepository = repository => String(repository || '').split('/') .map(encodeURIComponent).join('/'); function pathFor(context, commentId) { const item = context?.item || {}; if (context?.kind === 'update') { return 'api/v1/notifications/' + encodeURIComponent(item.notification_id) + '/comments/' + encodeURIComponent(commentId); } if (!['issue', 'pull'].includes(context?.kind)) throw new Error('Comment conversation is unavailable.'); return 'api/v1/repos/' + encodedRepository(item.repository) + '/' + (context.kind === 'pull' ? 'pulls/' : 'issues/') + encodeURIComponent(item.number) + '/comments/' + encodeURIComponent(commentId); } function reactionPath(context, commentId, content = '') { return pathFor(context, commentId) + '/reactions' + (content ? '/' + encodeURIComponent(content) : ''); } const pendingReactions = new Set(); const controller = { isOwned(comment) { const login = String(getLogin() || '').trim(); return Boolean(login && comment && comment.author === login); }, async edit(context, pager, commentId, body) { const draft = String(body || '').trim(); if (!draft) throw new Error('Comment must not be blank.'); const comment = await fetchJson(pathFor(context, commentId), { method: 'PATCH', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ body: draft }), }); pager.replace(comment); return pager.snapshot(); }, async remove(context, pager, commentId) { if (!confirmDelete('Delete this comment permanently?')) return null; const result = await fetchJson(pathFor(context, commentId), { method: 'DELETE', headers: { Accept: 'application/json' }, }); if (!result?.deleted || Number(result.id) !== Number(commentId)) { throw new Error('Comment deletion was not confirmed.'); } pager.remove(commentId); return pager.snapshot(); }, async loadReactions(context, commentId) { return fetchJson(reactionPath(context, commentId), { headers: { Accept: 'application/json' }, }); }, async setReaction(context, commentId, content, active) { return fetchJson(reactionPath(context, commentId, content), { method: 'PUT', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ active: Boolean(active) }), }); }, reactionHtml(state) { const current = new Map((state?.reactions || []).map(item => [item.content, item])); return Object.entries(reactionLabels).map(([content, label]) => { const item = current.get(content) || {}; const count = Number.isInteger(item.count) && item.count > 0 ? item.count : 0; const selected = item.selected === true; return ''; }).join('') + ''; }, actionHtml(comment) { const owned = controller.isOwned(comment) ? '
' + '' + '
' : ''; return owned + '
' + '' + '
'; }, wire({ root, getSurface, isOffline, escapeHtml }) { root.addEventListener('click', async event => { const actionButton = event.target.closest('[data-comment-action]'); const reactionClose = actionButton ? null : event.target.closest('[data-comment-reactions-close]'); const reactionButton = actionButton ? null : event.target.closest('[data-comment-reaction]'); const reactionTrigger = actionButton ? null : event.target.closest('[data-comment-reactions-open]'); if (reactionClose) { const card = reactionClose.closest('.issue-comment'); const menu = card?.querySelector('[data-comment-reaction-menu]'); const trigger = card?.querySelector('[data-comment-reactions-open]'); if (menu && trigger) { menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); trigger.focus(); } return; } if (reactionButton || reactionTrigger) { const control = reactionButton || reactionTrigger; const card = control.closest('.issue-comment'); const commentId = Number(card?.dataset.commentId); const surface = getSurface(); const menu = card?.querySelector('[data-comment-reaction-menu]'); const trigger = card?.querySelector('[data-comment-reactions-open]'); if (!commentId || !menu || !trigger) return; if (isOffline()) { surface.status.textContent = 'Reconnect to react.'; return; } if (pendingReactions.has(commentId)) return; pendingReactions.add(commentId); control.disabled = true; try { if (reactionButton) { const content = reactionButton.dataset.commentReaction; const active = reactionButton.getAttribute('aria-pressed') !== 'true'; const state = await controller.setReaction(surface.context, commentId, content, active); menu.innerHTML = controller.reactionHtml(state); menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); trigger.focus(); surface.status.textContent = 'Reaction updated.'; } else { surface.status.textContent = 'Loading reactions…'; const state = await controller.loadReactions(surface.context, commentId); menu.innerHTML = controller.reactionHtml(state); menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); surface.status.textContent = ''; } } catch (error) { surface.status.textContent = error.message || 'Reactions are unavailable. Please retry.'; } finally { pendingReactions.delete(commentId); control.disabled = false; } return; } const button = actionButton; if (!button) return; const card = button.closest('.issue-comment'); const commentId = Number(card?.dataset.commentId); const surface = getSurface(); const comment = surface.pager?.snapshot().comments.find(item => item.id === commentId); if (!comment || !controller.isOwned(comment)) return; if (isOffline()) { surface.status.textContent = 'Reconnect to edit or delete this comment.'; return; } if (button.dataset.commentAction === 'delete') { try { const state = await controller.remove(surface.context, surface.pager, commentId); if (state) { surface.render(state); surface.status.textContent = 'Comment deleted.'; } } catch (error) { surface.status.textContent = error.message || 'Comment deletion failed. Retry or open it in Gitea.'; } return; } card.innerHTML = '
Editing your comment
' + '' + '
' + '
'; const textarea = card.querySelector('.comment-edit-textarea'); textarea.focus(); card.querySelector('[data-comment-edit-cancel]').addEventListener('click', () => surface.render(surface.pager.snapshot())); card.querySelector('[data-comment-edit-save]').addEventListener('click', async saveEvent => { const save = saveEvent.currentTarget; save.disabled = true; surface.status.textContent = 'Saving comment…'; try { const state = await controller.edit(surface.context, surface.pager, commentId, textarea.value); surface.render(state); surface.status.textContent = 'Comment updated.'; } catch (error) { save.disabled = false; surface.status.textContent = error.message || 'Comment update failed. Your edit is safe; retry or open it in Gitea.'; textarea.focus(); } }); }); }, }; return controller; } if (typeof module !== 'undefined' && module.exports) module.exports = createCommentActions;