YT Comment Filter (Improved)

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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