Last active
November 19, 2025 21:14
-
-
Save dbowling/80baff8bad5950177c894e0e133248db to your computer and use it in GitHub Desktop.
github-issue-comments-hide-by-username
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name GitHub Issue Hide Comments by Username + Slash Commands | |
| // @namespace https://github.com/dbowling | |
| // @version 0.5 | |
| // @description Toggle/hide selected usernames and command-only comments | |
| // @include https://github.com/* | |
| // @match https://github.com/*/*/issues/* | |
| // @run-at document-idle | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @icon https://github.githubassets.com/pinned-octocat.svg | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const DEFAULT_HIDDEN_USERNAMES = ['k8s-triage-robot']; | |
| const processedComments = new WeakSet(); | |
| const hiddenUsernames = new Set(); | |
| // --- Settings helpers ---------------------------------------------------- | |
| function parseHiddenUsernames(raw) { | |
| return String(raw) | |
| .split(',') | |
| .map((s) => s.trim()) | |
| .filter(Boolean); | |
| } | |
| function loadHiddenUsernames() { | |
| const raw = GM_getValue('hiddenUsernames', DEFAULT_HIDDEN_USERNAMES.join(',')); | |
| hiddenUsernames.clear(); | |
| for (const name of parseHiddenUsernames(raw)) { | |
| hiddenUsernames.add(name); | |
| } | |
| console.debug('[GitHub Issue Comment User] Hidden usernames:', [...hiddenUsernames]); | |
| } | |
| function configureHiddenUsernames() { | |
| const current = [...hiddenUsernames].join(', '); | |
| const input = window.prompt( | |
| 'Enter comma-separated GitHub usernames to auto-collapse:', | |
| current | |
| ); | |
| if (input === null) { | |
| return; // user cancelled | |
| } | |
| GM_setValue('hiddenUsernames', input); | |
| loadHiddenUsernames(); | |
| window.alert('Hidden usernames updated. Reload the page to re-process existing comments.'); | |
| } | |
| GM_registerMenuCommand('Configure hidden usernames', configureHiddenUsernames); | |
| loadHiddenUsernames(); | |
| // --- DOM helpers --------------------------------------------------------- | |
| function findBodyContainer(commentEl) { | |
| let bodyContainer = | |
| commentEl.querySelector('.IssueCommentViewer-module__IssueCommentBody--xvkt3') || null; | |
| if (!bodyContainer) { | |
| const markdownBody = commentEl.querySelector('[data-testid="markdown-body"], .markdown-body'); | |
| if (markdownBody) { | |
| bodyContainer = markdownBody.closest('div') || markdownBody; | |
| } | |
| } | |
| return bodyContainer; | |
| } | |
| function extractUsername(commentEl) { | |
| // Primary: explicit author link in the header | |
| const headerAuthorLink = commentEl.querySelector('[data-testid="avatar-link"]'); | |
| if (headerAuthorLink && headerAuthorLink.textContent) { | |
| return headerAuthorLink.textContent.trim(); | |
| } | |
| // Fallback: avatar/profile link | |
| const profileLink = commentEl.querySelector('a[aria-label*="\'s profile"]'); | |
| if (profileLink) { | |
| const img = profileLink.querySelector('img[alt^="@"]'); | |
| if (img) { | |
| return (img.getAttribute('alt') || '').replace(/^@/, '').trim(); | |
| } | |
| const href = profileLink.getAttribute('href') || ''; | |
| const parts = href.split('/').filter(Boolean); | |
| if (parts.length > 0) { | |
| return parts[parts.length - 1]; | |
| } | |
| } | |
| return null; | |
| } | |
| // Detect “command-only” comments like `/remove-lifecycle stale`, `/triage accepted` | |
| function isCommandOnlyComment(commentEl) { | |
| const bodyContainer = findBodyContainer(commentEl); | |
| if (!bodyContainer) return false; | |
| const text = bodyContainer.textContent || ''; | |
| const trimmed = text.trim(); | |
| if (!trimmed) return false; | |
| const lines = trimmed | |
| .split('\n') | |
| .map((l) => l.trim()) | |
| .filter((l) => l.length > 0); | |
| if (!lines.length) return false; | |
| // Heuristic: all non-empty lines must start with '/' | |
| const allCommands = lines.every((line) => line.startsWith('/')); | |
| if (allCommands) { | |
| console.debug('[GitHub Issue Comment User] Detected command-only comment:', lines); | |
| } | |
| return allCommands; | |
| } | |
| // Generic toggle setup for both “hidden users” and “command-only” comments | |
| function setupToggle(commentEl, labelBase) { | |
| if (commentEl.dataset.hiddenCommentToggleInit === '1') return; | |
| commentEl.dataset.hiddenCommentToggleInit = '1'; | |
| const bodyContainer = findBodyContainer(commentEl); | |
| if (!bodyContainer) { | |
| console.debug( | |
| '[GitHub Issue Comment User] Could not find body container to toggle for:', | |
| labelBase | |
| ); | |
| return; | |
| } | |
| const headerLeft = | |
| commentEl.querySelector('[data-testid="comment-header-left-side-items"]') || | |
| commentEl.querySelector('[data-testid="comment-header"]') || | |
| commentEl; | |
| const toggleButton = document.createElement('button'); | |
| toggleButton.type = 'button'; | |
| toggleButton.textContent = `Show ${labelBase} comment`; | |
| toggleButton.setAttribute( | |
| 'aria-label', | |
| `Toggle visibility of ${labelBase} comment` | |
| ); | |
| toggleButton.setAttribute('aria-expanded', 'false'); | |
| // Light GitHub-ish styling | |
| toggleButton.style.marginLeft = '8px'; | |
| toggleButton.style.fontSize = '12px'; | |
| toggleButton.style.border = 'none'; | |
| toggleButton.style.padding = '2px 4px'; | |
| toggleButton.style.background = 'transparent'; | |
| toggleButton.style.cursor = 'pointer'; | |
| toggleButton.style.color = 'var(--fgColor-muted, #57606a)'; | |
| toggleButton.style.textDecoration = 'underline'; | |
| let collapsed = true; | |
| bodyContainer.style.display = 'none'; | |
| toggleButton.addEventListener('click', () => { | |
| collapsed = !collapsed; | |
| bodyContainer.style.display = collapsed ? 'none' : ''; | |
| toggleButton.textContent = collapsed | |
| ? `Show ${labelBase} comment` | |
| : `Hide ${labelBase} comment`; | |
| toggleButton.setAttribute('aria-expanded', String(!collapsed)); | |
| }); | |
| headerLeft.appendChild(toggleButton); | |
| } | |
| // --- Core logic ---------------------------------------------------------- | |
| function handleComment(comment) { | |
| if (processedComments.has(comment)) return; | |
| processedComments.add(comment); | |
| const username = extractUsername(comment); | |
| const commandOnly = isCommandOnlyComment(comment); | |
| if (username) { | |
| console.debug('[GitHub Issue Comment User]', username); | |
| } else { | |
| console.debug('[GitHub Issue Comment User] Username not found for comment:', comment); | |
| } | |
| const shouldHideByUser = username && hiddenUsernames.has(username); | |
| const shouldHideByCommand = commandOnly; | |
| if (!shouldHideByUser && !shouldHideByCommand) { | |
| return; | |
| } | |
| let labelBase; | |
| if (shouldHideByUser && username) { | |
| labelBase = username; | |
| } else if (shouldHideByCommand) { | |
| labelBase = 'command'; | |
| } else { | |
| labelBase = 'hidden'; | |
| } | |
| console.debug( | |
| '[GitHub Issue Comment User] Setting up toggle for comment:', | |
| { username, commandOnly } | |
| ); | |
| setupToggle(comment, labelBase); | |
| } | |
| function scanComments() { | |
| const comments = document.querySelectorAll('div.react-issue-comment'); | |
| comments.forEach(handleComment); | |
| } | |
| // Initial scan | |
| scanComments(); | |
| // Watch for dynamically loaded comments | |
| const observer = new MutationObserver((mutations) => { | |
| let foundAny = false; | |
| for (const m of mutations) { | |
| for (const node of m.addedNodes) { | |
| if (node.nodeType !== Node.ELEMENT_NODE) continue; | |
| const el = /** @type {Element} */ (node); | |
| if (el.matches && el.matches('div.react-issue-comment')) { | |
| handleComment(el); | |
| foundAny = true; | |
| } else if (el.querySelector && el.querySelector('div.react-issue-comment')) { | |
| foundAny = true; | |
| } | |
| } | |
| } | |
| if (foundAny) { | |
| scanComments(); | |
| } | |
| }); | |
| if (document.body) { | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment