TeaBag - Threads Filter

Filter out unwanted content on threads: hide verified users with optional whitelist, block users, and filter specific words.

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_link:Tampermonkey}.

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         TeaBag - Threads Filter
// @namespace    https://www.threads.com
// @version      2.0
// @description  Filter out unwanted content on threads: hide verified users with optional whitelist, block users, and filter specific words.
// @author       artificialsweetener.ai, https://artificialsweetener.ai
// @match        https://www.threads.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- Script State ---
    let settings = {};
    const processedPostIds = new Set();
    let filteredCount = 0;
    let normalUserPostCount = 0;
    let verifiedUserPostCount = 0;
    let counterDisplay = null;
    let lastUrl = location.href;
    let debounceTimer;

    // --- Settings Management ---
    function loadSettings() {
        const defaults = {
            enableFilterCount: true,
            enableHideCheckmarks: true,
            enableNoFilterOnProfiles: true,
            hideSuggestions: true,
            verifiedFilterMode: 'filter_all',
            whitelist: '',
            blacklist: '',
            textFilterList: ''
        };
        settings = GM_getValue('teabag_settings', defaults);
        delete settings.ratioValue;

        for (const key in defaults) {
            if (typeof settings[key] === 'undefined') {
                settings[key] = defaults[key];
            }
        }
    }

    function textToSet(text) {
        if (!text) return new Set();
        return new Set(text.split('\n').map(u => u.trim().toLowerCase()).filter(Boolean));
    }

    // --- Core Filtering Logic ---
    function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }

    function toggleGlobalCheckmarkHiding() {
        if (settings.enableHideCheckmarks) {
            document.body.classList.add('teabag-hide-checkmarks');
        } else {
            document.body.classList.remove('teabag-hide-checkmarks');
        }
    }

    function hideSuggestionBox() {
        const previouslyHidden = document.querySelector('[data-teabag-hidden-suggestion]');
        if (previouslyHidden) {
            previouslyHidden.style.display = '';
            delete previouslyHidden.dataset.teabagHiddenSuggestion;
        }
        if (!settings.hideSuggestions) return;

        const allSpans = document.querySelectorAll('span');
        for (const span of allSpans) {
            if (span.textContent.trim() === 'Suggested for you') {
                const titleContainer = span.closest('div > div > div > div');
                if (titleContainer && titleContainer.parentElement) {
                    const moduleContainer = titleContainer.parentElement;
                    moduleContainer.style.display = 'none';
                    moduleContainer.dataset.teabagHiddenSuggestion = 'true';
                    return;
                }
            }
        }
    }

    function processPost(post) {
        const timeLink = post.querySelector('a time');
        if (!timeLink) return;
        const postLink = timeLink.closest('a');
        if (!postLink || !postLink.href.includes('/post/')) return;
        const postId = postLink.href.substring(postLink.href.lastIndexOf('/') + 1);

        if (processedPostIds.has(postId)) return;
        processedPostIds.add(postId);

        const userLink = post.querySelector('a[href^="/@"]');
        if (!userLink) return;

        const username = userLink.getAttribute('href').substring(2).toLowerCase();
        const verifiedBadge = post.querySelector('svg[aria-label="Verified"]');
        const isOnProfilePage = settings.enableNoFilterOnProfiles && location.pathname.startsWith('/@');

        if (verifiedBadge) verifiedUserPostCount++; else normalUserPostCount++;

        let shouldHide = false;
        const whitelistSet = textToSet(settings.whitelist);
        const blacklistSet = textToSet(settings.blacklist);
        const textFilterSet = textToSet(settings.textFilterList);

        if (isOnProfilePage) {
            // No post hiding on profiles.
        } else if (blacklistSet.has(username)) {
            shouldHide = true;
        } else {
            const postText = post.innerText.toLowerCase();
            for (const filterText of textFilterSet) {
                if (postText.includes(filterText)) { shouldHide = true; break; }
            }
        }

        const isVerified = verifiedBadge && !whitelistSet.has(username);
        if (!shouldHide && !isOnProfilePage) {
            if (settings.verifiedFilterMode === 'filter_all' && isVerified) {
                shouldHide = true;
            }
        }

        if (shouldHide) {
            if (post.style.display !== 'none') {
                post.style.display = 'none';
                filteredCount++;
            }
        } else {
            if (post.style.display === 'none') {
                 post.style.display = '';
            }
        }
    }

    function runFilter(forceClear = false) {
        if (forceClear) {
            document.querySelectorAll('div[data-pressable-container="true"], article[role="article"]').forEach(el => el.style.display = '');
            filteredCount = 0;
            normalUserPostCount = 0;
            verifiedUserPostCount = 0;
            processedPostIds.clear();
        }
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            if (!location.pathname.startsWith('/@')) runFilter(true);
        }

        hideSuggestionBox();
        toggleGlobalCheckmarkHiding();

        const postSelector = 'div[data-pressable-container="true"], article[role="article"]';
        document.querySelectorAll(postSelector).forEach(processPost);
        updateCounterDisplay();
    }

    const debouncedRunFilter = () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => runFilter(false), 150); };

    // --- UI & Initialization ---
    function injectGlobalCSS() {
        const css = `
            /* --- Global Filter Styles --- */
            .teabag-hide-checkmarks svg[aria-label="Verified"] { display: none !important; }

            /* --- Fluent Design Settings Panel --- */
            #teabag-settings-panel-overlay {
                --accent-color: #007AFF; --bg-color: rgba(28, 28, 28, 0.7); --border-color: rgba(255, 255, 255, 0.15);
                --text-color-primary: #FFFFFF; --text-color-secondary: #c0c0c0; --input-bg-color: rgba(55, 55, 55, 0.8);
                --hover-bg-color: rgba(70, 70, 70, 0.9); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                color: var(--text-color-primary); position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center;
                background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(16px) saturate(180%); opacity: 0; animation: teabagFadeIn 0.3s ease forwards;
            }
            @keyframes teabagFadeIn { to { opacity: 1; } }
            .teabag-form {
                background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3);
                width: 650px; max-height: 90vh; overflow-y: auto; padding: 24px; transform: scale(0.95); animation: teabagScaleUp 0.3s ease forwards;
            }
            @keyframes teabagScaleUp { to { transform: scale(1); } }
            .teabag-form h2 { margin: 0 0 20px 0; text-align: center; font-size: 22px; font-weight: 600; color: var(--text-color-primary) !important; }
            .teabag-form fieldset { border: none; padding: 0; margin: 0 0 20px 0; }
            .teabag-form legend { font-size: 15px; font-weight: 600; margin-bottom: 12px; color: var(--text-color-primary) !important; }
            .teabag-form .description { font-size: 12px; color: var(--text-color-secondary); margin-top: -6px; margin-left: 28px; }
            .teabag-form .radio-label, .teabag-form .checkbox-label {
                display: flex; align-items: center; padding: 10px; border-radius: 8px; cursor: pointer;
                transition: background-color 0.2s ease; margin-bottom: 4px; color: var(--text-color-primary);
            }
            .teabag-form .radio-label:hover, .teabag-form .checkbox-label:hover { background: var(--hover-bg-color); }
            .teabag-form input[type="radio"], .teabag-form input[type="checkbox"] { margin-right: 12px; accent-color: var(--accent-color); width: 16px; height: 16px; }
            .teabag-form textarea { width: 100%; background: var(--input-bg-color); color: var(--text-color-primary); border: 1px solid var(--border-color); border-radius: 6px; padding: 8px; font-family: "SF Mono", "Consolas", monospace; box-sizing: border-box; resize: vertical; transition: border-color 0.2s ease, box-shadow 0.2s ease; }
            .teabag-form textarea:focus { outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 2px var(--accent-color); }
            .teabag-form .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
            .teabag-form button { border-radius: 8px; font-weight: 600; font-size: 14px; padding: 10px 20px; cursor: pointer; border: 1px solid transparent; transition: all 0.2s ease; }
            .teabag-form button#teabag-cancel { background: var(--input-bg-color); border-color: var(--border-color); color: var(--text-color-primary); }
            .teabag-form button#teabag-cancel:hover { background: var(--hover-bg-color); }
            .teabag-form button#teabag-save { background: var(--accent-color); color: white; }
            .teabag-form button#teabag-save:hover { filter: brightness(1.1); }
        `;
        const styleSheet = document.createElement("style");
        styleSheet.innerText = css;
        document.head.appendChild(styleSheet);
    }

    function createSettingsPanel() {
        if (document.getElementById('teabag-settings-panel-overlay')) return;
        const overlay = document.createElement('div');
        overlay.id = 'teabag-settings-panel-overlay';
        const form = document.createElement('div');
        form.className = 'teabag-form';
        overlay.appendChild(form);

        const createElement = (tag, { className = '', textContent = '', ...props }, children = []) => {
            const el = document.createElement(tag);
            if (className) el.className = className;
            if (textContent) el.textContent = textContent;
            Object.assign(el, props);
            children.forEach(child => el.appendChild(child));
            return el;
        };

        const createFieldset = (legend, children) => createElement('fieldset', {}, [createElement('legend', { textContent: legend }), ...children]);
        const createTextareaGroup = (id, label, rows) => createElement('div', {}, [
            createElement('label', { htmlFor: id, textContent: label, style: { display: 'block', marginBottom: '8px' } }),
            createElement('textarea', { id, rows, value: settings[id.replace('teabag-', '')] })
        ]);
        const createInput = (type, name, { id, value, label, description, checked }) => {
            const input = createElement('input', { type, name, id, value, checked });
            const labelEl = createElement('label', { className: `${type}-label`, htmlFor: id }, [input]);
            const textWrapper = createElement('div', {});
            textWrapper.appendChild(createElement('span', { textContent: label }));
            if (description) {
                textWrapper.appendChild(createElement('p', { className: 'description', textContent: description }));
            }
            labelEl.appendChild(textWrapper);
            return labelEl;
        };

        const generalOptions = createFieldset('General Options', [
            createInput('checkbox', '', { id: 'teabag-enableFilterCount', label: 'Enable Filter Counter', checked: settings.enableFilterCount }),
            createInput('checkbox', '', { id: 'teabag-enableHideCheckmarks', label: 'Hide All Verified Checkmarks', checked: settings.enableHideCheckmarks }),
            createInput('checkbox', '', { id: 'teabag-enableNoFilterOnProfiles', label: 'Disable Filtering on Profile Pages', checked: settings.enableNoFilterOnProfiles }),
            createInput('checkbox', '', { id: 'teabag-hideSuggestions', label: 'Hide \'Suggested for you\' Box', checked: settings.hideSuggestions })
        ]);

        const verifiedModeOptions = createFieldset('Verified User Filtering', [
            createInput('radio', 'verifiedFilterMode', { id: 'teabag-mode-filter', value: 'filter_all', label: 'Filter All', description: 'Hide all posts from verified users not on your whitelist.', checked: settings.verifiedFilterMode === 'filter_all' }),
            createInput('radio', 'verifiedFilterMode', { id: 'teabag-mode-show', value: 'show_all', label: 'Show All', description: 'Do not filter users based on their verified status.', checked: settings.verifiedFilterMode === 'show_all' })
        ]);

        const listsContainer = createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginBottom: '20px' } }, [
            createTextareaGroup('teabag-whitelist', 'Whitelist (one per line)', 10),
            createTextareaGroup('teabag-blacklist', 'Blacklist (one per line)', 10)
        ]);
        const wordFilter = createTextareaGroup('teabag-textFilterList', 'Filtered Words (one per line)', 5);

        const saveButton = createElement('button', { id: 'teabag-save', textContent: 'Save & Apply' });
        const cancelButton = createElement('button', { id: 'teabag-cancel', textContent: 'Cancel' });
        const actions = createElement('div', { className: 'form-actions' }, [cancelButton, saveButton]);

        form.append(createElement('h2', { textContent: 'TeaBag Filter Settings' }), generalOptions, verifiedModeOptions, listsContainer, wordFilter, actions);
        document.body.appendChild(overlay);

        saveButton.onclick = () => {
            settings = {
                enableFilterCount: form.querySelector('#teabag-enableFilterCount').checked,
                enableHideCheckmarks: form.querySelector('#teabag-enableHideCheckmarks').checked,
                enableNoFilterOnProfiles: form.querySelector('#teabag-enableNoFilterOnProfiles').checked,
                hideSuggestions: form.querySelector('#teabag-hideSuggestions').checked,
                verifiedFilterMode: form.querySelector('input[name="verifiedFilterMode"]:checked').value,
                whitelist: form.querySelector('#teabag-whitelist').value,
                blacklist: form.querySelector('#teabag-blacklist').value,
                textFilterList: form.querySelector('#teabag-textFilterList').value
            };
            GM_setValue('teabag_settings', settings);
            overlay.remove();
            runFilter(true);
        };
        cancelButton.onclick = () => overlay.remove();
        overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
    }

    function updateCounterDisplay() {
        if (!settings.enableFilterCount) { if (counterDisplay) counterDisplay.style.display = 'none'; return; }
        if (!counterDisplay) {
            counterDisplay = document.createElement('div');
            Object.assign(counterDisplay.style, {
                position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white',
                padding: '5px 10px', borderRadius: '5px', zIndex: '100001', fontSize: '14px', fontFamily: 'sans-serif'
            });
            document.body.appendChild(counterDisplay);
        }
        counterDisplay.style.display = 'block';
        const isOnProfilePage = location.pathname.startsWith('/@');
        if (settings.enableNoFilterOnProfiles && isOnProfilePage) {
            counterDisplay.innerText = `[Filter paused on profiles]`;
        } else {
            let ratioText = '0:0';
            if (normalUserPostCount > 0 || verifiedUserPostCount > 0) {
                const divisor = gcd(normalUserPostCount, verifiedUserPostCount);
                ratioText = `${(normalUserPostCount / divisor)}:${(verifiedUserPostCount / divisor)}`;
            }
            counterDisplay.innerText = `[Filtered: ${filteredCount} | Ratio (N:V): ${ratioText}]`;
        }
    }

    // --- Initial Load ---
    loadSettings();
    GM_registerMenuCommand('TeaBag Filter Settings', createSettingsPanel);

    window.addEventListener('DOMContentLoaded', () => {
        injectGlobalCSS();
        const observer = new MutationObserver(debouncedRunFilter);
        observer.observe(document.body, { childList: true, subtree: true });
        runFilter();
    });

})();