YT Comment Filter (Improved)

Automatically hide YouTube comments based on username and spam text patterns.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YT Comment Filter (Improved)
// @supportURL   https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @namespace    https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @homepageURL  https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @version      4.0
// @description  Automatically hide YouTube comments based on username and spam text patterns.
// @author       Mochamad Adi MR (adimuham.mad)
// @match        *://www.youtube.com/watch?v=*
// @match        *://www.youtube.com/shorts/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// @compatible   chrome
// @compatible   firefox
// @compatible   edge
// @compatible   opera
// @compatible   safari
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    let filterToggleButton = null;
    let isShowingHidden = false;
    const STORAGE_KEY = "yt_filter_custom_words";
    const builtInForbiddenUsernames = ["vip"];

    function loadCustomWords() {
        const savedWords = GM_getValue(STORAGE_KEY, "");

        if (savedWords) {
            return savedWords.split(',').map(word => word.trim()).filter(Boolean);
        }

        return [];
    }

    function showSettingsPrompt() {
        const currentWords = GM_getValue(STORAGE_KEY, "");
        const newWords = prompt("Enter custom username (separate with commas)):\nno need to enter a username where the vowels\nare replaced with numbers", currentWords);
        if (newWords === null) return;
        GM_setValue(STORAGE_KEY, newWords);
        alert("Custom usernames has saved!\nRefresh the page to apply the changes and see the results.");
    }

    function registerMenu() {
        GM_registerMenuCommand("List Blocked Username", showSettingsPrompt);
    }

    const customForbiddenUsernames = loadCustomWords();
    const forbiddenUsernames = Array.from(new Set([...builtInForbiddenUsernames, ...customForbiddenUsernames]));

    const forbiddenRegex = /[^\u0000-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u08A0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u125F\u1280-\u12BF\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1A20-\u1A5F\u1A80-\u1AFF\u1B00-\u1B7F\u1B80-\u1BBF\u1BC0-\u1BFF\u1C00-\u1C4F\u1C50-\u1C7F\u1C90-\u1CBF\u1CC0-\u1CCF\u1CD0-\u1CFF\u1E00-\u1EFF\u1F00-\u1FFF\u2000-\u206F\u2070-\u20CF\u20D0-\u20FF\u2150-\u218F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2DE0-\u2DFF\u2E00-\u2E7F\u2E80-\u2EFF\u2F00-\u2FDF\u2FF0-\u2FFF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31A0-\u31BF\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA6A0-\uA6FF\uA700-\uA71F\uA720-\uA7FF\uA800-\uA82F\uA830-\uA83F\uA840-\uA87F\uA880-\uA8DF\uA8E0-\uA8FF\uA900-\uA92F\uA930-\uA95F\uA960-\uA97F\uA980-\uA9DF\uA9E0-\uA9FF\uAA00-\uAA3F\uAA40-\uAA6F\uAA70-\uAAAB\uAAAC-\uAAAF\uAAB0-\uAABF\uAAC0-\uAADF\uAAE0-\uAAEF\uAAF0-\uAAFF\uAB00-\uAB2F\uAB30-\uAB6F\uAB70-\uABBF\uABC0-\uABFF\uAC00-\uD7AF\uD7B0-\uD7FF\uF900-\uFAFF\uFB00-\uFB4F\uFB50-\uFDFF\uFE00-\uFE0F\uFE10-\uFE1F\uFE20-\uFE2F\uFE30-\uFE4F\uFE50-\uFE6F\uFE70-\uFEFF]/;

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    function createLeetPattern(word) {
        let escapedWord = escapeRegExp(word);
        return escapedWord.replace(/a/gi, "[a4]").replace(/i/gi, "[i1]").replace(/e/gi, "[e3]").replace(/o/gi, "[o0]").replace(/s/gi, "[s5]").replace(/t/gi, "[t7]").replace(/g/gi, "[g9]");
    }

    const forbiddenPatterns = forbiddenUsernames.map(createLeetPattern);
    const forbiddenUserRegex = new RegExp(forbiddenPatterns.join("|"), "i");

    function addGlobalStyles() {
        GM_addStyle(`
            .yt-comment-filter-hidden { display: none; }
            body.yt-filter-showing-hidden .yt-comment-filter-hidden {
                display: block !important;
                opacity: 0.6;
                border: 1px dashed rgba(255, 0, 0, 0.5);
                border-radius: 8px;
                margin-bottom: 8px !important;
            }

            #yt-filter-toggle-container {
                display: inline-flex;
                align-items: center;
                vertical-align: middle;
                cursor: pointer;
                font-family: "Roboto","Arial",sans-serif;
                font-size: 14px;
                font-weight: 500;
                color: var(--yt-spec-text-secondary);
                margin-left: 32px;
                position: relative;
                bottom: 3px;
            }

            #yt-filter-toggle-container > div {
                display: inline-flex;
                align-items: center;
            }

            #yt-filter-toggle-container svg {
                width: 24px;
                height: 24px;
                fill: var(--yt-spec-text-secondary);
                margin-right: 4px;
            }

            #yt-filter-toggle-container .yt-filter-label,
            #yt-filter-toggle-container .yt-filter-status {
                text-transform: uppercase;
                margin-right: 4px;
            }

            #yt-filter-toggle-container .icon-visibility-off { display: none; }
            #yt-filter-toggle-container .icon-visibility-on { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-off { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-on { display: none; }
        `);
    }

    function updateHiddenCount() {
        if (!filterToggleButton) return;
        const count = document.querySelectorAll(".yt-comment-filter-hidden").length;
        const label = filterToggleButton.querySelector(".yt-filter-label");
        const status = filterToggleButton.querySelector(".yt-filter-status");
        const info = filterToggleButton.querySelector(".yt-filter-info");
        if (!label || !status || !info) return;

        label.textContent = "Hidden";

        if (isShowingHidden) {
            status.textContent = "OFF";
            info.style.display = "none";
        } else {
            status.textContent = "ON";
            info.style.display = "";
            info.textContent = `(${count})`;
        }
    }

    function injectFilterButton() {
        if (document.getElementById("yt-filter-toggle-container")) return;
        const targetElement = document.querySelector("ytd-comments-header-renderer #additional-section");
        if (!targetElement) return;
        filterToggleButton = document.createElement("span");
        filterToggleButton.id = "yt-filter-toggle-container";
        filterToggleButton.className = "style-scope ytd-comments-header-renderer";
        const visibilityIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5C21.27 7.61 17 4.5 12 4.5zm0 13c-3.04 0-5.5-2.46-5.5-5.5S8.96 6.5 12 6.5s5.5 2.46 5.5 5.5-2.46 5.5-5.5 5.5zm0-9c-1.93 0-3.5 1.57-3.5 3.5s1.57 3.5 3.5 3.5 3.5-1.57 3.5-3.5-1.57-3.5-3.5-3.5z"></path></svg>`;
        const visibilityOffIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L21.73 23 23 21.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.93 1.57 3.5 3.5 3.5.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-3.04 0-5.5-2.46-5.5-5.5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.93-1.57-3.5-3.5-3.5l-.16.02z"></path></svg>`;
        filterToggleButton.innerHTML = `<div><svg class="icon-visibility-on" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityIconSVG}</svg><svg class="icon-visibility-off" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityOffIconSVG}</svg><span class="yt-filter-label">Hidden</span><span class="yt-filter-status">OFF</span><span class="yt-filter-info">(0)</span></div>`;
        filterToggleButton.addEventListener("click", () => {
            isShowingHidden = !isShowingHidden;
            document.body.classList.toggle("yt-filter-showing-hidden", isShowingHidden);
            updateHiddenCount();
        });
        targetElement.after(filterToggleButton);
        return true;
    }

    let throttleCooldown = false;
    const throttleDelay = 1000;

    function removeBadComments() {
        const commentContainers = document.querySelectorAll("ytd-comment-view-model:not(.yt-comment-filter-hidden)");

        commentContainers.forEach((commentContainer) => {
            const commentTextElement = commentContainer.querySelector("#content-text");
            const usernameElement = commentContainer.querySelector("#author-text span");
            if (!commentTextElement) return;
            let isSpam = false;
            if (forbiddenRegex.test(commentTextElement.innerText)) isSpam = true;
            if (!isSpam && usernameElement && forbiddenUserRegex.test(usernameElement.innerText)) isSpam = true;

            if (isSpam) {
                commentContainer.style.display = "none";
                commentContainer.classList.add("yt-comment-filter-hidden");

                if (commentContainer.id === "comment") {
                    const thread = commentContainer.closest("ytd-comment-thread-renderer");
                    if (thread) {
                        const repliesSection = thread.querySelector("div#replies");
                        if (repliesSection) {
                            repliesSection.style.display = "none";
                        }
                    }
                }
            }
        });

        updateHiddenCount();
    }

    function throttledRemoveBadComments() {
        if (throttleCooldown) return;
        removeBadComments();
        throttleCooldown = true;
        setTimeout(() => {
            throttleCooldown = false;
        }, throttleDelay);
    }

    registerMenu();
    addGlobalStyles();
    setTimeout(removeBadComments, 3000);

    const observer = new MutationObserver(() => {
        throttledRemoveBadComments();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    const buttonInjectInterval = setInterval(() => {
        if (injectFilterButton()) {
            clearInterval(buttonInjectInterval);
            updateHiddenCount();
        }
    }, 1000);
})();