From 77cbee78ee899261bc6d10ebf35a7ad852295ee7 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 09:56:42 -0400 Subject: [PATCH 1/8] Add buttons to hide comments (off-topic, outdated, resolved) --- src/js/lib/api.js | 66 +++++++++++++++++++++++++++++++- src/js/lib/pages/github/issue.js | 2 + src/js/lib/pages/github/pr.js | 3 +- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/js/lib/api.js b/src/js/lib/api.js index 81c95f6c..02abe86c 100644 --- a/src/js/lib/api.js +++ b/src/js/lib/api.js @@ -40,7 +40,7 @@ function getCurrentUser() { */ function getRequestParams() { const url = window.location.href; - const regex = /github.com\/(?\w*)\/(?\w*)(?:\/(?:issues|pull)\/(?\d*))?/; + const regex = /github.com\/(?[\w-]+)\/(?[\w.-]+)(?:\/(?:issues|pull)\/(?\d+))?/; const matches = url.match(regex); return { @@ -609,6 +609,66 @@ function updateComment(commentId, body) { }); } +/** + * Look up an issue comment's GraphQL node ID by its numeric REST ID. + * @param {Number|String} commentId + * @returns {Promise} + */ +function getIssueCommentNodeId(commentId) { + const {owner, repo} = getRequestParams(); + return getOctokit().rest.issues.getComment({owner, repo, comment_id: Number(commentId)}) + .then(response => response.data.node_id); +} + +/** + * Look up a pull-request review's GraphQL node ID by its numeric REST ID. + * @param {Number|String} reviewId + * @returns {Promise} + */ +function getPullRequestReviewNodeId(reviewId) { + const {owner, repo, issue_number} = getRequestParams(); + return getOctokit().rest.pulls.getReview({ + owner, + repo, + pull_number: Number(issue_number), + review_id: Number(reviewId), + }).then(response => response.data.node_id); +} + +/** + * Look up an inline PR review-thread comment's GraphQL node ID by its numeric REST ID. + * @param {Number|String} commentId + * @returns {Promise} + */ +function getPullRequestReviewCommentNodeId(commentId) { + const {owner, repo} = getRequestParams(); + return getOctokit().rest.pulls.getReviewComment({ + owner, + repo, + comment_id: Number(commentId), + }).then(response => response.data.node_id); +} + +/** + * Minimize a comment with a GitHub reported-content classifier. + * @param {String} nodeId GraphQL node id of the comment subject + * @param {String} classifier ReportedContentClassifiers enum value + * @returns {Promise} + */ +function minimizeComment(nodeId, classifier) { + const mutation = ` + mutation MinimizeComment($id: ID!, $classifier: ReportedContentClassifiers!) { + minimizeComment(input: {subjectId: $id, classifier: $classifier}) { + minimizedComment { + isMinimized + minimizedReason + } + } + } + `; + return getOctokit().graphql(mutation, {id: nodeId, classifier}); +} + /** * Get recent workflow runs for a specific workflow * @param {String} workflowId @@ -658,6 +718,10 @@ export { updateComment, getWorkflowRuns, getWorkflowRun, + getIssueCommentNodeId, + getPullRequestReviewNodeId, + getPullRequestReviewCommentNodeId, + minimizeComment, getStatusCheckRollup, getPullRequestHeadRefOid, }; diff --git a/src/js/lib/pages/github/issue.js b/src/js/lib/pages/github/issue.js index 7076739f..1023f6f6 100644 --- a/src/js/lib/pages/github/issue.js +++ b/src/js/lib/pages/github/issue.js @@ -14,6 +14,7 @@ import K2previousissues from '../../../module/K2previousissues/K2previousissues' import ONYXKEYS from '../../../ONYXKEYS'; import * as API from '../../api'; import * as autoLoadMoreComments from '../../autoLoadMoreComments'; +import * as hideCommentButtons from '../../hideCommentButtons'; let clearErrorTimeoutID; function catchError(e) { @@ -231,6 +232,7 @@ export default function () { setInterval(() => IssuePage.renderPaymentDetailsButton(), 2000); autoLoadMoreComments.initAutoLoadMoreComments(); + hideCommentButtons.initHideCommentButtons(); }; return IssuePage; diff --git a/src/js/lib/pages/github/pr.js b/src/js/lib/pages/github/pr.js index 1cc7b031..afa0250a 100644 --- a/src/js/lib/pages/github/pr.js +++ b/src/js/lib/pages/github/pr.js @@ -3,6 +3,7 @@ import Base from './_base'; import ToggleTimestamps from '../../../module/ToggleTimestamps/ToggleTimestamps'; import ToggleAutoLoadMore from '../../../module/ToggleAutoLoadMore/ToggleAutoLoadMore'; import * as autoLoadMoreComments from '../../autoLoadMoreComments'; +import * as hideCommentButtons from '../../hideCommentButtons'; import * as commitCheckStatuses from '../../commitCheckStatuses'; import * as prFavicon from '../../prFavicon'; @@ -146,10 +147,10 @@ export default function () { setInterval(() => PrPage.renderTranslationWorkflowButtons(), 2000); autoLoadMoreComments.initAutoLoadMoreComments(); + hideCommentButtons.initHideCommentButtons(); commitCheckStatuses.initCommitCheckStatuses(); prFavicon.initPrFavicon(); }; return PrPage; } - From 40ac159fbfa6e15e11f9b2486cfb4c2289f8ba49 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 10:11:51 -0400 Subject: [PATCH 2/8] Add misisng import --- src/js/lib/hideCommentButtons.js | 223 +++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/js/lib/hideCommentButtons.js diff --git a/src/js/lib/hideCommentButtons.js b/src/js/lib/hideCommentButtons.js new file mode 100644 index 00000000..d2f36ef3 --- /dev/null +++ b/src/js/lib/hideCommentButtons.js @@ -0,0 +1,223 @@ +/* eslint-disable rulesdir/prefer-underscore-method */ +import $ from 'jquery'; +import * as API from './api'; + +const ACTIONS = [ + {classifier: 'OFF_TOPIC', label: 'Off-topic'}, + {classifier: 'OUTDATED', label: 'Outdated'}, + {classifier: 'RESOLVED', label: 'Resolved'}, +]; + +const BUTTONS_CLASS = 'k2-hide-comment-buttons'; + +// Author profile links in comment headers use hovercards across GitHub UIs. +// GitHub App bots use an /apps/ URL instead. +const AUTHOR_SELECTOR = [ + 'a[data-hovercard-url*="/users/"]', + 'a[data-hovercard-type="user"]', + 'a[href*="/apps/"]', +].join(', '); +const COMMENT_BODY_SELECTOR = '.markdown-body, .comment-body, [data-testid="markdown-body"], [data-testid="issue-comment-body"]'; + +// Match the comment type needed by the REST endpoint that returns its GraphQL node ID. +// Check review-thread comments before review comments because their URLs overlap. +const PERMALINK_TYPES = [ + {type: 'pullrequestreviewcomment', pattern: /pullrequestreviewcomment-(\d+)/}, + {type: 'pullrequestreviewcomment', pattern: /discussion_r(\d+)/}, + {type: 'pullrequestreview', pattern: /pullrequestreview-(\d+)/}, + {type: 'issuecomment', pattern: /issuecomment-(\d+)/}, +]; +const PERMALINK_SELECTOR = [ + 'a[href*="#issuecomment-"]', + 'a[href*="#pullrequestreview-"]', + 'a[href*="#pullrequestreviewcomment-"]', + 'a[href*="#discussion_r"]', +].join(', '); + +let observer = null; +let scanScheduled = false; + +function isOptionsButton(btn) { + const label = (btn.getAttribute('aria-label') || '').toLowerCase(); + if (label.includes('options') || label.includes('show menu') || label === 'more') { + return true; + } + + return !!btn.querySelector('.octicon-kebab-horizontal, [class*="KebabHorizontal"]'); +} + +// The React UI uses a button. The legacy review UI uses a summary element. +function findOptionsButton(rootEl) { + return $(rootEl).find('button, summary').filter((i, btn) => isOptionsButton(btn)).first(); +} + +// Ignore user mentions inside comment bodies because they are not comment authors. +function isHeaderAuthorLink(link) { + const text = (link.textContent || '').trim(); + if (text.startsWith('@')) { + return false; + } + if (link.classList.contains('user-mention')) { + return false; + } + if (link.closest('.markdown-body, .comment-body, [data-testid="markdown-body"]')) { + return false; + } + return true; +} + +function parsePermalink(permalink) { + const href = permalink.getAttribute('href') || ''; + for (let i = 0; i < PERMALINK_TYPES.length; i++) { + const {type, pattern} = PERMALINK_TYPES[i]; + const match = href.match(pattern); + if (match) { + return {type, id: match[1]}; + } + } + return null; +} + +function getPermalinkHash(permalink) { + const href = permalink.getAttribute('href') || ''; + const hashIdx = href.indexOf('#'); + return hashIdx >= 0 ? href.slice(hashIdx + 1) : ''; +} + +// Find the smallest ancestor that contains the author, permalink, and comment body. +function findCommentHeader(authorLink) { + let el = authorLink.parentElement; + while (el && el !== document.body) { + const permalink = el.querySelector(PERMALINK_SELECTOR); + if (permalink) { + const parsed = parsePermalink(permalink); + if (parsed) { + const hash = getPermalinkHash(permalink); + const idMatches = hash && (el.id === hash || !!el.querySelector(`[id="${CSS.escape(hash)}"]`)); + const hasBody = !!el.querySelector(COMMENT_BODY_SELECTOR); + if (idMatches || hasBody) { + return { + container: el, permalink, parsed, optionsBtn: findOptionsButton(el), + }; + } + } + } + el = el.parentElement; + } + return null; +} + +// Hide the comment locally after GitHub accepts the minimize mutation. +function collapseCommentBox(wrapper) { + let el = wrapper.parentElement; + while (el && el !== document.body) { + if (el.querySelector(COMMENT_BODY_SELECTOR)) { + el.style.display = 'none'; + return; + } + el = el.parentElement; + } +} + +function lookupNodeId(commentType, commentId) { + if (commentType === 'pullrequestreview') { + return API.getPullRequestReviewNodeId(commentId); + } + if (commentType === 'pullrequestreviewcomment') { + return API.getPullRequestReviewCommentNodeId(commentId); + } + return API.getIssueCommentNodeId(commentId); +} + +async function handleClick(event) { + const button = event.currentTarget; + const wrapper = button.closest(`.${BUTTONS_CLASS}`); + const commentId = wrapper && wrapper.dataset.commentId; + const commentType = wrapper && wrapper.dataset.commentType; + const classifier = button.dataset.classifier; + if (!commentId || !commentType || !classifier) { + return; + } + $(wrapper).find('button').prop('disabled', true); + try { + const nodeId = await lookupNodeId(commentType, commentId); + await API.minimizeComment(nodeId, classifier); + collapseCommentBox(wrapper); + } catch (e) { + /* eslint-disable-next-line no-console */ + console.error('Failed to hide comment', e); + $(wrapper).find('button').prop('disabled', false); + } +} + +function addButtons({ + container, permalink, parsed, optionsBtn, +}) { + if ($(container).find(`.${BUTTONS_CLASS}`).length) { + return; + } + + // A review can render more than one author link. Prevent duplicate button groups. + if (document.querySelector(`.${BUTTONS_CLASS}[data-comment-id="${CSS.escape(parsed.id)}"][data-comment-type="${CSS.escape(parsed.type)}"]`)) { + return; + } + + const wrapper = document.createElement('span'); + wrapper.className = `${BUTTONS_CLASS} k2-element`; + wrapper.dataset.commentId = parsed.id; + wrapper.dataset.commentType = parsed.type; + ACTIONS.forEach((action) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn btn-sm k2-hide-comment-button'; + btn.dataset.classifier = action.classifier; + btn.textContent = action.label; + btn.addEventListener('click', handleClick); + wrapper.appendChild(btn); + }); + + // Place buttons before the kebab when GitHub exposes the action menu. + if (optionsBtn && optionsBtn.length) { + const anchor = optionsBtn[0]; + const target = anchor.tagName === 'SUMMARY' ? (anchor.closest('details') || anchor) : anchor; + target.parentNode.insertBefore(wrapper, target); + } else { + permalink.parentNode.insertBefore(wrapper, permalink.nextSibling); + } +} + +function scan() { + $(AUTHOR_SELECTOR).each((i, authorLink) => { + if (!isHeaderAuthorLink(authorLink)) { + return; + } + const header = findCommentHeader(authorLink); + if (!header) { + return; + } + addButtons(header); + }); +} + +function scheduleScan() { + if (scanScheduled) { + return; + } + scanScheduled = true; + requestAnimationFrame(() => { + scanScheduled = false; + scan(); + }); +} + +function initHideCommentButtons() { + if (observer) { + return; + } + scheduleScan(); + observer = new MutationObserver(scheduleScan); + observer.observe(document.body, {childList: true, subtree: true}); +} + +// eslint-disable-next-line import/prefer-default-export +export {initHideCommentButtons}; From 370287e3b246b7c98719ef21607cef3ef3cb0f41 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 10:31:28 -0400 Subject: [PATCH 3/8] Remove eslint-disable --- src/js/lib/hideCommentButtons.js | 35 +++++++++++++++++++------------- src/js/lib/pages/github/issue.js | 2 +- src/js/lib/pages/github/pr.js | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/js/lib/hideCommentButtons.js b/src/js/lib/hideCommentButtons.js index d2f36ef3..f81c2555 100644 --- a/src/js/lib/hideCommentButtons.js +++ b/src/js/lib/hideCommentButtons.js @@ -1,5 +1,4 @@ -/* eslint-disable rulesdir/prefer-underscore-method */ -import $ from 'jquery'; +import _ from 'underscore'; import * as API from './api'; const ACTIONS = [ @@ -48,7 +47,7 @@ function isOptionsButton(btn) { // The React UI uses a button. The legacy review UI uses a summary element. function findOptionsButton(rootEl) { - return $(rootEl).find('button, summary').filter((i, btn) => isOptionsButton(btn)).first(); + return _.find(rootEl.querySelectorAll('button, summary'), btn => isOptionsButton(btn)) || null; } // Ignore user mentions inside comment bodies because they are not comment authors. @@ -129,6 +128,16 @@ function lookupNodeId(commentType, commentId) { return API.getIssueCommentNodeId(commentId); } +function setButtonsDisabled(wrapper, disabled) { + _.each(wrapper.querySelectorAll('button'), (button) => { + if (disabled) { + button.setAttribute('disabled', 'disabled'); + } else { + button.removeAttribute('disabled'); + } + }); +} + async function handleClick(event) { const button = event.currentTarget; const wrapper = button.closest(`.${BUTTONS_CLASS}`); @@ -138,22 +147,21 @@ async function handleClick(event) { if (!commentId || !commentType || !classifier) { return; } - $(wrapper).find('button').prop('disabled', true); + setButtonsDisabled(wrapper, true); try { const nodeId = await lookupNodeId(commentType, commentId); await API.minimizeComment(nodeId, classifier); collapseCommentBox(wrapper); - } catch (e) { - /* eslint-disable-next-line no-console */ - console.error('Failed to hide comment', e); - $(wrapper).find('button').prop('disabled', false); + } catch (error) { + setButtonsDisabled(wrapper, false); + wrapper.title = error instanceof Error ? error.message : 'Failed to hide comment'; } } function addButtons({ container, permalink, parsed, optionsBtn, }) { - if ($(container).find(`.${BUTTONS_CLASS}`).length) { + if (container.querySelector(`.${BUTTONS_CLASS}`)) { return; } @@ -177,8 +185,8 @@ function addButtons({ }); // Place buttons before the kebab when GitHub exposes the action menu. - if (optionsBtn && optionsBtn.length) { - const anchor = optionsBtn[0]; + if (optionsBtn) { + const anchor = optionsBtn; const target = anchor.tagName === 'SUMMARY' ? (anchor.closest('details') || anchor) : anchor; target.parentNode.insertBefore(wrapper, target); } else { @@ -187,7 +195,7 @@ function addButtons({ } function scan() { - $(AUTHOR_SELECTOR).each((i, authorLink) => { + _.each(document.querySelectorAll(AUTHOR_SELECTOR), (authorLink) => { if (!isHeaderAuthorLink(authorLink)) { return; } @@ -219,5 +227,4 @@ function initHideCommentButtons() { observer.observe(document.body, {childList: true, subtree: true}); } -// eslint-disable-next-line import/prefer-default-export -export {initHideCommentButtons}; +export default initHideCommentButtons; diff --git a/src/js/lib/pages/github/issue.js b/src/js/lib/pages/github/issue.js index 1023f6f6..7c4c1471 100644 --- a/src/js/lib/pages/github/issue.js +++ b/src/js/lib/pages/github/issue.js @@ -14,7 +14,7 @@ import K2previousissues from '../../../module/K2previousissues/K2previousissues' import ONYXKEYS from '../../../ONYXKEYS'; import * as API from '../../api'; import * as autoLoadMoreComments from '../../autoLoadMoreComments'; -import * as hideCommentButtons from '../../hideCommentButtons'; +import hideCommentButtons from '../../hideCommentButtons'; let clearErrorTimeoutID; function catchError(e) { diff --git a/src/js/lib/pages/github/pr.js b/src/js/lib/pages/github/pr.js index afa0250a..58b7ffcb 100644 --- a/src/js/lib/pages/github/pr.js +++ b/src/js/lib/pages/github/pr.js @@ -3,7 +3,7 @@ import Base from './_base'; import ToggleTimestamps from '../../../module/ToggleTimestamps/ToggleTimestamps'; import ToggleAutoLoadMore from '../../../module/ToggleAutoLoadMore/ToggleAutoLoadMore'; import * as autoLoadMoreComments from '../../autoLoadMoreComments'; -import * as hideCommentButtons from '../../hideCommentButtons'; +import hideCommentButtons from '../../hideCommentButtons'; import * as commitCheckStatuses from '../../commitCheckStatuses'; import * as prFavicon from '../../prFavicon'; From e2b48774663f5513c4d3c0ddc67c52161019e806 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 11:05:41 -0400 Subject: [PATCH 4/8] Fix bug --- src/js/lib/pages/github/issue.js | 2 +- src/js/lib/pages/github/pr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/lib/pages/github/issue.js b/src/js/lib/pages/github/issue.js index 7c4c1471..8e308c8c 100644 --- a/src/js/lib/pages/github/issue.js +++ b/src/js/lib/pages/github/issue.js @@ -232,7 +232,7 @@ export default function () { setInterval(() => IssuePage.renderPaymentDetailsButton(), 2000); autoLoadMoreComments.initAutoLoadMoreComments(); - hideCommentButtons.initHideCommentButtons(); + hideCommentButtons(); }; return IssuePage; diff --git a/src/js/lib/pages/github/pr.js b/src/js/lib/pages/github/pr.js index 58b7ffcb..fd03ecce 100644 --- a/src/js/lib/pages/github/pr.js +++ b/src/js/lib/pages/github/pr.js @@ -147,7 +147,7 @@ export default function () { setInterval(() => PrPage.renderTranslationWorkflowButtons(), 2000); autoLoadMoreComments.initAutoLoadMoreComments(); - hideCommentButtons.initHideCommentButtons(); + hideCommentButtons(); commitCheckStatuses.initCommitCheckStatuses(); prFavicon.initPrFavicon(); }; From 350fa0709666a36525772db4a8ee1c2e73c5e7d5 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 11:08:45 -0400 Subject: [PATCH 5/8] Better looking buttons --- src/css/content.scss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/css/content.scss b/src/css/content.scss index 6e63d43b..3258ec53 100644 --- a/src/css/content.scss +++ b/src/css/content.scss @@ -39,6 +39,22 @@ $color-dark-yellow: #DAA520; .almost-transparent { opacity: 0.1; } + +.k2-hide-comment-buttons { + display: inline-flex; + gap: 4px; + margin-right: 8px; + align-items: center; + vertical-align: middle; + + .k2-hide-comment-button { + height: 22px; + padding: 0 6px; + font-size: 12px; + line-height: 20px; + } +} + .k2-inactive { opacity: 0.2; } From bfabf9a2d45ccae2f05f61c1d2bbd63a2042ec65 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 15:44:34 -0400 Subject: [PATCH 6/8] Address review comments --- src/js/lib/api.js | 30 +++++++++++++++--------------- src/js/lib/hideCommentButtons.js | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/js/lib/api.js b/src/js/lib/api.js index 02abe86c..15619136 100644 --- a/src/js/lib/api.js +++ b/src/js/lib/api.js @@ -611,51 +611,51 @@ function updateComment(commentId, body) { /** * Look up an issue comment's GraphQL node ID by its numeric REST ID. - * @param {Number|String} commentId + * @param {Number|String} commentID * @returns {Promise} */ -function getIssueCommentNodeId(commentId) { +function getIssueCommentNodeID(commentID) { const {owner, repo} = getRequestParams(); - return getOctokit().rest.issues.getComment({owner, repo, comment_id: Number(commentId)}) + return getOctokit().rest.issues.getComment({owner, repo, comment_id: Number(commentID)}) .then(response => response.data.node_id); } /** * Look up a pull-request review's GraphQL node ID by its numeric REST ID. - * @param {Number|String} reviewId + * @param {Number|String} reviewID * @returns {Promise} */ -function getPullRequestReviewNodeId(reviewId) { +function getPullRequestReviewNodeID(reviewID) { const {owner, repo, issue_number} = getRequestParams(); return getOctokit().rest.pulls.getReview({ owner, repo, pull_number: Number(issue_number), - review_id: Number(reviewId), + review_id: Number(reviewID), }).then(response => response.data.node_id); } /** * Look up an inline PR review-thread comment's GraphQL node ID by its numeric REST ID. - * @param {Number|String} commentId + * @param {Number|String} commentID * @returns {Promise} */ -function getPullRequestReviewCommentNodeId(commentId) { +function getPullRequestReviewCommentNodeID(commentID) { const {owner, repo} = getRequestParams(); return getOctokit().rest.pulls.getReviewComment({ owner, repo, - comment_id: Number(commentId), + comment_id: Number(commentID), }).then(response => response.data.node_id); } /** * Minimize a comment with a GitHub reported-content classifier. - * @param {String} nodeId GraphQL node id of the comment subject + * @param {String} nodeID GraphQL node ID of the comment subject * @param {String} classifier ReportedContentClassifiers enum value * @returns {Promise} */ -function minimizeComment(nodeId, classifier) { +function minimizeComment(nodeID, classifier) { const mutation = ` mutation MinimizeComment($id: ID!, $classifier: ReportedContentClassifiers!) { minimizeComment(input: {subjectId: $id, classifier: $classifier}) { @@ -666,7 +666,7 @@ function minimizeComment(nodeId, classifier) { } } `; - return getOctokit().graphql(mutation, {id: nodeId, classifier}); + return getOctokit().graphql(mutation, {id: nodeID, classifier}); } /** @@ -718,9 +718,9 @@ export { updateComment, getWorkflowRuns, getWorkflowRun, - getIssueCommentNodeId, - getPullRequestReviewNodeId, - getPullRequestReviewCommentNodeId, + getIssueCommentNodeID, + getPullRequestReviewNodeID, + getPullRequestReviewCommentNodeID, minimizeComment, getStatusCheckRollup, getPullRequestHeadRefOid, diff --git a/src/js/lib/hideCommentButtons.js b/src/js/lib/hideCommentButtons.js index f81c2555..8959efa5 100644 --- a/src/js/lib/hideCommentButtons.js +++ b/src/js/lib/hideCommentButtons.js @@ -118,14 +118,14 @@ function collapseCommentBox(wrapper) { } } -function lookupNodeId(commentType, commentId) { +function lookupNodeID(commentType, commentID) { if (commentType === 'pullrequestreview') { - return API.getPullRequestReviewNodeId(commentId); + return API.getPullRequestReviewNodeID(commentID); } if (commentType === 'pullrequestreviewcomment') { - return API.getPullRequestReviewCommentNodeId(commentId); + return API.getPullRequestReviewCommentNodeID(commentID); } - return API.getIssueCommentNodeId(commentId); + return API.getIssueCommentNodeID(commentID); } function setButtonsDisabled(wrapper, disabled) { @@ -138,19 +138,19 @@ function setButtonsDisabled(wrapper, disabled) { }); } -async function handleClick(event) { +async function minimizeComment(event) { const button = event.currentTarget; const wrapper = button.closest(`.${BUTTONS_CLASS}`); - const commentId = wrapper && wrapper.dataset.commentId; + const commentID = wrapper && wrapper.dataset.commentId; const commentType = wrapper && wrapper.dataset.commentType; const classifier = button.dataset.classifier; - if (!commentId || !commentType || !classifier) { + if (!commentID || !commentType || !classifier) { return; } setButtonsDisabled(wrapper, true); try { - const nodeId = await lookupNodeId(commentType, commentId); - await API.minimizeComment(nodeId, classifier); + const nodeID = await lookupNodeID(commentType, commentID); + await API.minimizeComment(nodeID, classifier); collapseCommentBox(wrapper); } catch (error) { setButtonsDisabled(wrapper, false); @@ -174,13 +174,13 @@ function addButtons({ wrapper.className = `${BUTTONS_CLASS} k2-element`; wrapper.dataset.commentId = parsed.id; wrapper.dataset.commentType = parsed.type; - ACTIONS.forEach((action) => { + _.each(ACTIONS, (action) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm k2-hide-comment-button'; btn.dataset.classifier = action.classifier; btn.textContent = action.label; - btn.addEventListener('click', handleClick); + btn.addEventListener('click', minimizeComment); wrapper.appendChild(btn); }); From dd491523185fbc8defb3db31ad2c4e9dfd5c3ccc Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 17:22:22 -0400 Subject: [PATCH 7/8] Address review comments --- src/css/content.scss | 7 ++++ src/js/lib/hideCommentButtons.js | 61 ++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/css/content.scss b/src/css/content.scss index 3258ec53..60d0a41c 100644 --- a/src/css/content.scss +++ b/src/css/content.scss @@ -55,6 +55,13 @@ $color-dark-yellow: #DAA520; } } +.k2-minimized-comment { + padding: 8px 12px; + border: 1px solid $color-neutral-light; + color: $color-neutral; + font-style: italic; +} + .k2-inactive { opacity: 0.2; } diff --git a/src/js/lib/hideCommentButtons.js b/src/js/lib/hideCommentButtons.js index 8959efa5..5abd0e72 100644 --- a/src/js/lib/hideCommentButtons.js +++ b/src/js/lib/hideCommentButtons.js @@ -12,11 +12,11 @@ const BUTTONS_CLASS = 'k2-hide-comment-buttons'; // Author profile links in comment headers use hovercards across GitHub UIs. // GitHub App bots use an /apps/ URL instead. const AUTHOR_SELECTOR = [ - 'a[data-hovercard-url*="/users/"]', - 'a[data-hovercard-type="user"]', - 'a[href*="/apps/"]', + '.timeline-comment-header a.author[data-hovercard-type="user"]', + '.timeline-comment-header a.author[data-hovercard-url*="/users/"]', + '.timeline-comment-header a.author[href*="/apps/"]', ].join(', '); -const COMMENT_BODY_SELECTOR = '.markdown-body, .comment-body, [data-testid="markdown-body"], [data-testid="issue-comment-body"]'; +const COMMENT_BODY_SELECTOR = '.comment-body.js-comment-body'; // Match the comment type needed by the REST endpoint that returns its GraphQL node ID. // Check review-thread comments before review comments because their URLs overlap. @@ -59,7 +59,10 @@ function isHeaderAuthorLink(link) { if (link.classList.contains('user-mention')) { return false; } - if (link.closest('.markdown-body, .comment-body, [data-testid="markdown-body"]')) { + if (link.closest(COMMENT_BODY_SELECTOR)) { + return false; + } + if (link.closest('.timeline-comment[id^="pullrequest-"]')) { return false; } return true; @@ -106,16 +109,35 @@ function findCommentHeader(authorLink) { return null; } -// Hide the comment locally after GitHub accepts the minimize mutation. -function collapseCommentBox(wrapper) { - let el = wrapper.parentElement; - while (el && el !== document.body) { - if (el.querySelector(COMMENT_BODY_SELECTOR)) { - el.style.display = 'none'; - return; - } - el = el.parentElement; +function getMinimizeForm(wrapper) { + const comment = wrapper.closest('.timeline-comment-group'); + return comment && comment.querySelector('form.js-timeline-comment-minimize'); +} + +function getActionLabel(classifier) { + const action = _.find(ACTIONS, item => item.classifier === classifier); + return action ? action.label.toLowerCase() : classifier.toLowerCase().replace('_', '-'); +} + +// Render the same state that GitHub uses when the native form is unavailable. +function showMinimizedComment(wrapper, classifier) { + const comment = wrapper.closest('.timeline-comment-group'); + const body = comment && comment.querySelector(COMMENT_BODY_SELECTOR); + if (!comment || !body) { + return; + } + + const header = comment.querySelector('.timeline-comment-header'); + const bodyContainer = body.closest('.edit-comment-hide') || body; + if (header) { + header.style.display = 'none'; } + bodyContainer.style.display = 'none'; + + const minimized = document.createElement('div'); + minimized.className = 'k2-minimized-comment'; + minimized.textContent = `This comment has been minimized as ${getActionLabel(classifier)}.`; + comment.appendChild(minimized); } function lookupNodeID(commentType, commentID) { @@ -148,10 +170,19 @@ async function minimizeComment(event) { return; } setButtonsDisabled(wrapper, true); + const form = getMinimizeForm(wrapper); + if (form) { + const select = form.querySelector('select[name="classifier"]'); + if (select) { + select.value = classifier; + form.requestSubmit(); + return; + } + } try { const nodeID = await lookupNodeID(commentType, commentID); await API.minimizeComment(nodeID, classifier); - collapseCommentBox(wrapper); + showMinimizedComment(wrapper, classifier); } catch (error) { setButtonsDisabled(wrapper, false); wrapper.title = error instanceof Error ? error.message : 'Failed to hide comment'; From 54051a12df62a59ebb754f70c2978ca9d559f067 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 14 Aug 2026 17:25:22 -0400 Subject: [PATCH 8/8] Better collapsing --- src/js/lib/hideCommentButtons.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/js/lib/hideCommentButtons.js b/src/js/lib/hideCommentButtons.js index 5abd0e72..1b0d5f84 100644 --- a/src/js/lib/hideCommentButtons.js +++ b/src/js/lib/hideCommentButtons.js @@ -114,14 +114,9 @@ function getMinimizeForm(wrapper) { return comment && comment.querySelector('form.js-timeline-comment-minimize'); } -function getActionLabel(classifier) { - const action = _.find(ACTIONS, item => item.classifier === classifier); - return action ? action.label.toLowerCase() : classifier.toLowerCase().replace('_', '-'); -} - // Render the same state that GitHub uses when the native form is unavailable. -function showMinimizedComment(wrapper, classifier) { - const comment = wrapper.closest('.timeline-comment-group'); +function showMinimizedComment(wrapper) { + const comment = wrapper.closest('.timeline-comment, .timeline-comment-group'); const body = comment && comment.querySelector(COMMENT_BODY_SELECTOR); if (!comment || !body) { return; @@ -133,10 +128,12 @@ function showMinimizedComment(wrapper, classifier) { header.style.display = 'none'; } bodyContainer.style.display = 'none'; + comment.classList.remove('unminimized-comment'); + comment.classList.add('minimized-comment', 'position-relative'); const minimized = document.createElement('div'); minimized.className = 'k2-minimized-comment'; - minimized.textContent = `This comment has been minimized as ${getActionLabel(classifier)}.`; + minimized.textContent = body.textContent.trim() || 'This comment has been minimized.'; comment.appendChild(minimized); } @@ -182,7 +179,7 @@ async function minimizeComment(event) { try { const nodeID = await lookupNodeID(commentType, commentID); await API.minimizeComment(nodeID, classifier); - showMinimizedComment(wrapper, classifier); + showMinimizedComment(wrapper); } catch (error) { setButtonsDisabled(wrapper, false); wrapper.title = error instanceof Error ? error.message : 'Failed to hide comment';