Lets you mute posts with certain words or phrases. Go to Moderation and then "Muted words" to edit your list.
Fra og med
// ==UserScript==
// @name Bluesky - Muted Words (BROKEN)
// @author @thissteveguy.bsky.app
// @version 24.02.09
// @description Lets you mute posts with certain words or phrases. Go to Moderation and then "Muted words" to edit your list.
// @namespace https://greasyfork.org/en/users/253-lednerg
// @license (CC) Attribution Non-Commercial Share Alike; http://creativecommons.org/licenses/by-nc-sa/3.0/
// @match https://bsky.app/*
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// ==/UserScript==
// This lets you mute posts which contain certain words or phrases in Bluesky. Click Moderation and then
// "Muted words" to edit your list. Place each word or phrase on its own line. Don't use quotation marks
// or commas unless they are part of what you're muting. Capitalization doesn't matter.
//
// Make sure to enter in all forms of a word that you'd like hidden; partial words will not work. "Jump"
// will not block "jumps" or "jumpy" - you'll need to add those yourself. However, words with apostrophes
// do work. So "Mark" will block "Mark's" and "Mark'll" just fine
//
// When a post is hidden in a feed or within a thread, a message will be displayed in its place showing
// you which word was found, along with a button to unhide the post. (Hidden posts within search results
// will not have an "Unhide post" button, but clicking them will take you to their thread.)
//
// This script will break as soon as Bluesky decides to change anything about how they display posts and
// threads, so please be ready for that. This is ultimately just an experiment, and I cannot promise I
// will be fixing issues immediately. There is a debug function and a ridiculous amount of notes within
// the code to help with troubleshooting. It was tested with Chrome in Windows using Violentmonkey, as
// well as with the Kiwi Browser in Android using Tampermonkey.
(function() {
"use strict";
// Set to true to enable debugging in your browser's console (F12)
let debugConsole = false;
function consoleLog(...args) {
if (debugConsole) {
console.log(...args);
}
}
// CSS stylesheet for the options window and muted word posts.
GM_addStyle(`
.varTextBkg {
color: var(--text);
background-color: var(--background); }
.varTextMarg { margin-bottom: 20px;
color: var(--text); }
.textArea { width: 100%;
height: 400px;
font-size: 1.25em;
font-weight: 500;
letter-spacing: 0.25px;
border: 1px solid var(--text);
border-radius: 8px;
resize: none;
text-align: center; }
.mutedWordsBtns {
width: 74px;
padding: 8px 16px;
border-radius: 8px;
text-align: center; }
.showsMutedMessage .showsMutedMessage {
border-top: none !important;
height: auto !important; }
.showsMutedMessage .showsMutedMessage div:not(.mutedMessage) {
display: inherit !important; }
.mutedWordsHide.showsMutedMessage > div:not(.mutedMessage) {
display: none; }
.mutedWordsHide.showsMutedMessage {
height: 50px;
padding-top: 0 !important;
border-top: 1px solid var(--text);
border-bottom: 1px solid var(--text); }
.mutedWordsHide.showsMutedMessage + .mutedWordsHide.showsMutedMessage {
border-top: none; }
.mutedWordsHide.showsMutedMessage .mutedMessage {
color: var(--text) !important;
font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 15px !important;
padding-left: 20px;
position: relative;
top: 14px; }
.mutedWordsHide.showsMutedMessage .mutedTerm {
padding: 0 10px;
margin: 0 5px;
color: #0000;
border: 1px solid var(--text);
opacity: .2; }
.mutedWordsHide.showsMutedMessage .mutedTerm:hover {
color: var(--text);
border: none;
opacity: 1; }
div.showsMutedMessage:not(.mutedWordsHide) .mutedMessage,
div.showsMutedMessage:not(.mutedWordsHide) .mutedMessage + button {
display: none; }
.mutedMessage + button:hover {
text-decoration: underline;
color: var(--text) }
.mutedMessage + button {
background: transparent;
color: var(--text);
border: 1px;
font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
cursor: pointer;
width: 100px;
height: 30px;
position: absolute;
right: 18px;
top: 9px; }
`);
// Function to display the options window
const displayOptionsWindow = () => {
// Create the background div
const backgroundDiv = document.createElement('div');
backgroundDiv.setAttribute('tabindex', '0');
backgroundDiv.className = 'css-175oi2r r-1awozwy r-17c3jg3 r-1pi2tsx r-1777fci r-1d2f490 r-u8s1d r-ipm5af r-13qz1uu';
// Event listener to remove the background div when clicked
backgroundDiv.addEventListener('click', () => {
backgroundDiv.remove();
});
// Create the container div
const containerDiv = document.createElement('div');
// Fetch stored muted words from Tampermonkey's local storage
const storedMutedWords = GM_getValue('mutedWords', []);
// Places each word or phrase on its own line
const mutedWordsText = storedMutedWords.join('\n');
// HTML for the options window
containerDiv.innerHTML = `
<div tabindex="0" class="css-175oi2r r-1xfd6ze r-rs99b7 r-rsyp9y r-7h7f8p r-dd0y9b r-cxgwc0 r-33ulu8 varTextBkg" style="border-color: rgba(128, 128, 128, 0.5);">
<div class="css-146c3p1 r-1x35g6 r-vw2c0b r-6gpygo r-q4m81j varTextMarg" style="text-align: center; font-size: 1.5em;">Muted Words</div>
<div class="css-146c3p1 varTextMarg">Enter the words or phrases you'd like to mute, one per line. Click 'Save' to apply changes.</div>
<textarea id="mutedWordsTextarea" class="css-146c3p1 varTextBkg textArea">${mutedWordsText}</textarea>
<input id="statusMessage" class="css-146c3p1 varTextBkg" type="text" readonly style="width: 100%; border: none; margin-bottom: 20px;" value="">
<div style="display: flex; justify-content: flex-end;">
<button id="mutedWordsCancelBtn" class="css-146c3p1 r-wzwllv r-jwli3a mutedWordsBtns" style="cursor: pointer; margin-right: 24px;">Close</button>
<button id="mutedWordsSaveBtn" class="mutedWordsSaveDisabled css-146c3p1 r-wzwllv r-jwli3a mutedWordsBtns">Save</button>
</div>
</div>
<style>.mutedWordsSaveEnabled{ cursor: pointer; background-color:rgb(0, 133, 255) } .mutedWordsSaveDisabled{ background-color:rgb(113, 113, 113) }</style>
`;
// Event listener to stop propagation of click event to background div
containerDiv.addEventListener('click', (e) => {
e.stopPropagation();
});
// Append the container div to the background div
backgroundDiv.appendChild(containerDiv);
// Find the parent div where the background div should be appended
const parentDiv = document.querySelector('#root .css-175oi2r.r-1pi2tsx');
// Append the background div to the parent div
if (parentDiv) {
parentDiv.appendChild(backgroundDiv);
}
// When the save button is pressed, run this
function saveMutedWords() {
// Get the textarea content
const textareaContent = document.getElementById('mutedWordsTextarea').value;
// Split the content by line into an array
let mutedWordsArray = textareaContent.split('\n');
// Remove empty lines
mutedWordsArray = mutedWordsArray.filter(word => word.trim() !== '');
// Save the array to Tampermonkey's local storage
GM_setValue('mutedWords', mutedWordsArray);
// Update the Save button's classes to indicate it's disabled after saving
const saveButton = document.getElementById('mutedWordsSaveBtn');
saveButton.classList.remove('mutedWordsSaveEnabled');
saveButton.classList.add('mutedWordsSaveDisabled');
// Update the status message
document.getElementById('statusMessage').value = 'Saved. Please refresh your browser!';
}
// Detect changes in the textarea and update the Save button's classes
document.getElementById('mutedWordsTextarea').addEventListener('input', () => {
// Puts current textarea into a variable
const currentTextareaContent = document.getElementById('mutedWordsTextarea').value;
// Puts currenty stored muted words list into a variable
const savedMutedWords = GM_getValue('mutedWords', []).join('\n');
// Finds the Save button
const saveButton = document.getElementById('mutedWordsSaveBtn');
// Swaps the Save button's class if a differnce is found and vice versa
if (currentTextareaContent !== savedMutedWords) {
saveButton.classList.remove('mutedWordsSaveDisabled');
saveButton.classList.add('mutedWordsSaveEnabled');
} else {
saveButton.classList.remove('mutedWordsSaveEnabled');
saveButton.classList.add('mutedWordsSaveDisabled');
}
});
// Attach the function to the Save button
document.getElementById('mutedWordsSaveBtn').addEventListener('click', saveMutedWords);
// Attach the function to remove the background div to the Cancel button
document.getElementById('mutedWordsCancelBtn').addEventListener('click', () => {
backgroundDiv.remove();
});
};
// Flag to check if the button has been added
let buttonAdded = false;
// Function to check if the current page is the Moderation page
const isModerationPage = () => {
return window.location.href.includes("/moderation");
};
// Function to add the Muted Words button (by cloning the Mute Lists button and changing it)
const addMutedWordsButton = () => {
if (buttonAdded) return; // Exit if the button has already been added
// Find the Mute Lists button
let muteListsBtn = document.querySelector('[data-testid="moderationlistsBtn"]');
if (muteListsBtn) {
// Clone the button
let newBtn = muteListsBtn.cloneNode(true);
// Update the cloned button
newBtn.setAttribute('data-testid', 'mutedWordsBtn');
newBtn.querySelector('div[dir="auto"]').textContent = "Muted words";
newBtn.querySelector('svg').remove();
newBtn.querySelector('.css-175oi2r').insertAdjacentHTML('beforeend', '<span style="font-size: .7em; color: var(--text);">$#@!</span>');
// Add event listener to the new button
newBtn.addEventListener('click', () => {
displayOptionsWindow();
consoleLog("Muted Words button clicked.");
});
// Insert the new button before the Mute Lists button
muteListsBtn.parentNode.insertBefore(newBtn, muteListsBtn);
consoleLog("Muted Words button added.");
buttonAdded = true; // Set the flag to true
return true;
} else {
consoleLog("Mute Lists button not found.");
return false;
}
};
// Initialize MutationObserver
const observer = new MutationObserver((mutations) => {
if (isModerationPage()) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
if (addMutedWordsButton()) {
buttonAdded = true;
}
}
}
} else {
buttonAdded = false; // Reset the flag when navigating away from the Moderation page
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// We do this check every time a change to the document is detected
function checkForMutedWords() {
// Load the muted word list
const mutedWords = GM_getValue("mutedWords", []);
// Search these specific elements of the page for the words
// If the script stops working, it's probably because they changed the site so that this points nowhere
const posts = document.querySelectorAll('[data-testid="contentHider-post"]:not(.mutedWordsChecked), div[role="link"] div[dir="auto"]:not(.mutedWordsChecked)');
consoleLog("Text nodes to be checked: ", posts.length);
posts.forEach((post) => {
// Mark the post as checked
post.classList.add('mutedWordsChecked');
// Check for muted words in text nodes only
const walker = document.createTreeWalker(post, NodeFilter.SHOW_TEXT, null, false);
let node;
let foundMutedWord = false;
// Keep track of which specific word it found (first) to show the user
let theWordItFound = '';
while ((node = walker.nextNode())) {
for (const word of mutedWords) {
const regex = new RegExp(`\\b${word}\\b`, 'i'); // Match whole word, case-insensitive
if (regex.test(node.textContent)) {
foundMutedWord = true;
theWordItFound = word;
consoleLog("Found the term: ", theWordItFound);
break;
}
}
if (foundMutedWord) break;
}
// Add the "mutedWordsDetected" class if a muted word is found
if (foundMutedWord) {
post.classList.add('mutedWordsDetected');
let postContainer;
// Posts within search results pages are structured differently
if (window.location.href.includes("/search?q=")) {
// Find the container holding the entire post
postContainer = post.closest('div[role="link"]');
} else {
// Find the container holding the entire post
postContainer = post.closest('div[role="link"]').parentNode;
}
// Make sure it's not null and that we haven't already addressed this post before
if (postContainer !== null && !postContainer.classList.contains('mutedWordsHide')) {
// The CSS at the top of the script will hide any div with a mutedWordsHide class
postContainer.classList.add('mutedWordsHide');
// Create a button to remove the mutedWordsHide class
const removeMutedWordsButton = document.createElement('button');
removeMutedWordsButton.textContent = 'Unhide post';
// What happens when you click "Unhide post"
removeMutedWordsButton.addEventListener('click', () => {
// It removes the mutedWordsHide class from the button's postContainer, it's parent, and grandparent
postContainer.classList.remove('mutedWordsHide');
if (postContainer.parentNode) {
postContainer.parentNode.classList.remove('mutedWordsHide');
}
// Do we really need to do this check?
if (postContainer.parentNode && postContainer.parentNode.parentNode) {
postContainer.parentNode.parentNode.classList.remove('mutedWordsHide');
}
});
// Create muted message
const mutedMessage = document.createElement('div');
mutedMessage.classList.add('mutedMessage');
mutedMessage.innerHTML = `Post containing muted term: <span class="mutedTerm">${theWordItFound}</span>`;
// Append muted message and button to postContainer
postContainer.appendChild(mutedMessage);
postContainer.classList.add('showsMutedMessage');
// Adds the "Unhide post" button at the end (but not in search results, since it won't work there currently)
if (!window.location.href.includes("/search?q=")) postContainer.appendChild(removeMutedWordsButton);
}
}
});
}
// Debounce function to limit the frequency of function execution
let debounceTimer;
function debounce(func, delay) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(func, delay);
}
// MutationObserver is what Javascript uses to detect changes to the page, such as adding posts as you scroll
const mutedWordsObserver = new MutationObserver((mutations) => {
// Use debounce to limit the frequency of checkForMutedWords execution
debounce(checkForMutedWords, 50); // a 50ms delay means it can only check up to 20 times a second, which is plenty
});
// Options for the observer (which mutations to observe)
const config = {
attributes: true,
childList: true,
subtree: true
};
// Target node to observe
const targetNode = document.querySelector('[data-testid="contentHider-post"]') || document.body;
// Start observing the target node for configured mutations
mutedWordsObserver.observe(targetNode, config);
// Run the function initially
checkForMutedWords();
})();