Bluesky - Muted Words (BROKEN)

Lets you mute posts with certain words or phrases. Go to Moderation and then "Muted words" to edit your list.

Verze ze dne 09. 02. 2024. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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();
})();