Skip to content

Instantly share code, notes, and snippets.

@noaione
Created September 6, 2026 13:10
Show Gist options
  • Select an option

  • Save noaione/092678e05054416168c1fd92de3251db to your computer and use it in GitHub Desktop.

Select an option

Save noaione/092678e05054416168c1fd92de3251db to your computer and use it in GitHub Desktop.
[AO3] Mute specific user from the comment sections
// ==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