Created
September 6, 2026 13:10
-
-
Save noaione/092678e05054416168c1fd92de3251db to your computer and use it in GitHub Desktop.
[AO3] Mute specific user from the comment sections
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 AO3 Mute Commenters | |
| // @namespace ao3-mute-commenters | |
| // @version 1.0.0 | |
| // @description Replace muted users' comments with placeholder text. | |
| // @match https://archiveofourown.org/* | |
| // @require https://update.greasyfork.org/scripts/552743/1859007/AO3%3A%20Menu%20Helpers%20Library.js?v=2.3.0 | |
| // @icon https://archiveofourown.org/images/logo.png | |
| // @grant none | |
| // @run-at document-idle | |
| // @updateURL https://gist.githubusercontent.com/noaione/092678e05054416168c1fd92de3251db/raw/ao3-mute-commenters.user.js | |
| // @downloadURL https://gist.githubusercontent.com/noaione/092678e05054416168c1fd92de3251db/raw/ao3-mute-commenters.user.js | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const STORAGE_KEY = 'ao3-mute-commenters:v1'; | |
| const COMMENT_SELECTOR = '.comment.group'; | |
| const MUTED_TEXT = 'This comment is hidden because you muted this user.'; | |
| function loadMuted() { | |
| try { | |
| const value = JSON.parse(localStorage.getItem(STORAGE_KEY)); | |
| return new Set( | |
| Array.isArray(value) | |
| ? value.filter(id => typeof id === 'string' && /^\d+$/.test(id)) | |
| : [] | |
| ); | |
| } catch { | |
| return new Set(); | |
| } | |
| } | |
| let muted = loadMuted(); | |
| function getUserId(comment) { | |
| for (const className of comment.classList) { | |
| const match = /^user-(\d+)$/.exec(className); | |
| if (match) return match[1]; | |
| } | |
| return null; | |
| } | |
| // Exclude elements belonging to nested replies. | |
| function ownedElements(comment, selector) { | |
| return [...comment.querySelectorAll(selector)].filter( | |
| element => element.closest(COMMENT_SELECTOR) === comment | |
| ); | |
| } | |
| function updateBody(body, isMuted) { | |
| const hasOriginal = Object.prototype.hasOwnProperty.call( | |
| body, | |
| '__originalCommentHtml' | |
| ); | |
| if (isMuted) { | |
| // Save only once, so we never cache the placeholder as the original. | |
| if (!hasOriginal) { | |
| body.__originalCommentHtml = body.innerHTML; | |
| const placeholder = document.createElement('p'); | |
| const placeholderContent = document.createElement('em'); | |
| placeholderContent.textContent = MUTED_TEXT; | |
| placeholder.appendChild(placeholderContent); | |
| body.replaceChildren(placeholder); | |
| } | |
| body.dataset.ao3CommentMuted = 'true'; | |
| } else { | |
| if (hasOriginal) { | |
| body.innerHTML = body.__originalCommentHtml; | |
| delete body.__originalCommentHtml; | |
| } | |
| body.removeAttribute('data-ao3-comment-muted'); | |
| } | |
| } | |
| function updateButton(button, isMuted) { | |
| const text = isMuted ? 'Unmute' : 'Mute'; | |
| if (button.textContent !== text) { | |
| button.textContent = text; | |
| } | |
| button.setAttribute('aria-pressed', String(isMuted)); | |
| button.title = isMuted | |
| ? 'Show comments from this user' | |
| : 'Hide comments from this user'; | |
| } | |
| function updateUser(userId) { | |
| const isMuted = muted.has(userId); | |
| document.querySelectorAll( | |
| `blockquote.userstuff[data-ao3-comment-user="${userId}"]` | |
| ).forEach(body => updateBody(body, isMuted)); | |
| document.querySelectorAll( | |
| `button[data-ao3-mute-user="${userId}"]` | |
| ).forEach(button => updateButton(button, isMuted)); | |
| } | |
| function saveMuted(next) { | |
| try { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify([...next])); | |
| } catch (error) { | |
| console.warn('[AO3 Mute Commenters]', error); | |
| alert('Unable to save muted users to browser storage.'); | |
| return; | |
| } | |
| const affected = new Set([...muted, ...next]); | |
| muted = next; | |
| affected.forEach(updateUser); | |
| } | |
| function toggleMute(userId) { | |
| // Preserve changes made in another tab. | |
| const next = loadMuted(); | |
| if (next.has(userId)) { | |
| next.delete(userId); | |
| } else { | |
| next.add(userId); | |
| } | |
| saveMuted(next); | |
| } | |
| function processComment(comment) { | |
| const userId = getUserId(comment); | |
| if (!userId) return; | |
| const isMuted = muted.has(userId); | |
| for (const body of ownedElements(comment, 'blockquote.userstuff')) { | |
| body.dataset.ao3CommentUser = userId; | |
| updateBody(body, isMuted); | |
| } | |
| const actions = ownedElements( | |
| comment, | |
| 'ul.actions, ol.actions' | |
| )[0]; | |
| if (!actions) return; | |
| let button = actions.querySelector('button[data-ao3-mute-user]'); | |
| if (!button) { | |
| const item = document.createElement('li'); | |
| button = document.createElement('button'); | |
| button.type = 'button'; | |
| button.dataset.ao3MuteUser = userId; | |
| button.addEventListener('click', event => { | |
| event.preventDefault(); | |
| toggleMute(button.dataset.ao3MuteUser); | |
| }); | |
| item.append(button); | |
| actions.append(item); | |
| } | |
| button.dataset.ao3MuteUser = userId; | |
| updateButton(button, isMuted); | |
| } | |
| function refresh() { | |
| document.querySelectorAll(COMMENT_SELECTOR).forEach(processComment); | |
| } | |
| window.AO3MenuHelpers?.addToSharedMenu({ | |
| id: 'ao3-mute-commenters-reset', | |
| text: 'Unmute all commenters', | |
| onClick(event) { | |
| event.preventDefault(); | |
| const count = loadMuted().size; | |
| if (!count) { | |
| alert('No commenters are muted.'); | |
| return; | |
| } | |
| if (confirm(`Unmute all ${count} muted commenters?`)) { | |
| saveMuted(new Set()); | |
| } | |
| }, | |
| }); | |
| window.addEventListener('storage', event => { | |
| if (event.key !== STORAGE_KEY && event.key !== null) return; | |
| const next = loadMuted(); | |
| const affected = new Set([...muted, ...next]); | |
| muted = next; | |
| affected.forEach(updateUser); | |
| }); | |
| // Handle dynamically added comments without making requests. | |
| let scheduled = false; | |
| const observer = new MutationObserver(records => { | |
| if (scheduled || !records.some(record => record.addedNodes.length)) { | |
| return; | |
| } | |
| scheduled = true; | |
| requestAnimationFrame(() => { | |
| scheduled = false; | |
| refresh(); | |
| }); | |
| }); | |
| observer.observe(document.querySelector('#main') || document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| refresh(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment