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