// ==UserScript== // @name Filter X.com Location // @namespace mailto:arabalika@noreplay.codeberg.org // @version 0.2.6 // @description Script to filter X content by user location. // @match https://x.com/* // @grant GM_xmlhttpRequest // @connect filter-x-api.phdogee.workers.dev // @license AGPL3.0 // @downloadURL https://update.greasyfork.icu/scripts/557600/Filter%20Xcom%20Location.user.js // @updateURL https://update.greasyfork.icu/scripts/557600/Filter%20Xcom%20Location.meta.js // ==/UserScript== (() => { // --- CONFIGURATION --- const queryUrl = "https://x.com/i/api/graphql/XRqGa7EeokUU5kppkh13EA/AboutAccountQuery"; const authToken = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; const REMOTE_DB_URL = "https://filter-x-api.phdogee.workers.dev"; // SETTINGS const READ_BATCH_DELAY = 75; // Wait 75ms to collect users for Reading const WRITE_BATCH_DELAY = 2000; // Wait 2s to collect users for Writing (Uploading) const WRITE_BATCH_SIZE = 5; // Or upload immediately if we have 5 users waiting const REQUEST_DELAY = 2000; const RANDOM_JITTER = 500; const RATE_LIMIT_PAUSE = 60000; const MAX_RETRIES = 5; const CACHE_TTL = 1000 * 60 * 60 * 24 * 30; const CONCURRENT_API_LIMIT = 2; const csrfToken = decodeURIComponent(document.cookie.match(/(?:^|; )ct0=([^;]+)/)?.[1] || ""); const cache = new Map(); const pending = new Map(); const apiQueue = []; let activeRequests = 0; // concurrency control let isRateLimited = false; // READ Batching let readBuffer = []; let readTimeout = null; // WRITE Batching let writeBuffer = new Map(); let writeTimeout = null; const storedValues = localStorage.getItem("tweetFilterValues"); let filterValues = storedValues ? storedValues.split("\n").filter(Boolean) : []; let filterMode = localStorage.getItem("tweetFilterMode") || "blacklist"; let filterEnabled = localStorage.getItem("tweetFilterEnabled") === "true"; const countryFlags = new Map([ ["Afghanistan", "๐ฆ๐ซ"], ["ร land Islands", "๐ฆ๐ฝ"], ["Albania", "๐ฆ๐ฑ"], ["Algeria", "๐ฉ๐ฟ"], ["American Samoa", "๐ฆ๐ธ"], ["Andorra", "๐ฆ๐ฉ"], ["Angola", "๐ฆ๐ด"], ["Anguilla", "๐ฆ๐ฎ"], ["Antarctica", "๐ฆ๐ถ"], ["Antigua and Barbuda", "๐ฆ๐ฌ"], ["Argentina", "๐ฆ๐ท"], ["Armenia", "๐ฆ๐ฒ"], ["Aruba", "๐ฆ๐ผ"], ["Australia", "๐ฆ๐บ"], ["Austria", "๐ฆ๐น"], ["Azerbaijan", "๐ฆ๐ฟ"], ["Bahamas", "๐ง๐ธ"], ["Bahrain", "๐ง๐ญ"], ["Bangladesh", "๐ง๐ฉ"], ["Barbados", "๐ง๐ง"], ["Belarus", "๐ง๐พ"], ["Belgium", "๐ง๐ช"], ["Belize", "๐ง๐ฟ"], ["Benin", "๐ง๐ฏ"], ["Bermuda", "๐ง๐ฒ"], ["Bhutan", "๐ง๐น"], ["Bolivia, Plurinational State of", "๐ง๐ด"], ["Bonaire, Sint Eustatius and Saba", "๐ง๐ถ"], ["Bosnia and Herzegovina", "๐ง๐ฆ"], ["Botswana", "๐ง๐ผ"], ["Bouvet Island", "๐ง๐ป"], ["Brazil", "๐ง๐ท"], ["British Indian Ocean Territory", "๐ฎ๐ด"], ["Brunei Darussalam", "๐ง๐ณ"], ["Bulgaria", "๐ง๐ฌ"], ["Burkina Faso", "๐ง๐ซ"], ["Burundi", "๐ง๐ฎ"], ["Cambodia", "๐ฐ๐ญ"], ["Cameroon", "๐จ๐ฒ"], ["Canada", "๐จ๐ฆ"], ["Cape Verde", "๐จ๐ป"], ["Cayman Islands", "๐ฐ๐พ"], ["Central African Republic", "๐จ๐ซ"], ["Chad", "๐น๐ฉ"], ["Chile", "๐จ๐ฑ"], ["China", "๐จ๐ณ"], ["Christmas Island", "๐จ๐ฝ"], ["Cocos (Keeling) Islands", "๐จ๐จ"], ["Colombia", "๐จ๐ด"], ["Comoros", "๐ฐ๐ฒ"], ["Congo", "๐จ๐ฌ"], ["Congo, the Democratic Republic of the", "๐จ๐ฉ"], ["Cook Islands", "๐จ๐ฐ"], ["Costa Rica", "๐จ๐ท"], ["Cรดte d'Ivoire", "๐จ๐ฎ"], ["Croatia", "๐ญ๐ท"], ["Cuba", "๐จ๐บ"], ["Curaรงao", "๐จ๐ผ"], ["Cyprus", "๐จ๐พ"], ["Czech Republic", "๐จ๐ฟ"], ["Denmark", "๐ฉ๐ฐ"], ["Djibouti", "๐ฉ๐ฏ"], ["Dominica", "๐ฉ๐ฒ"], ["Dominican Republic", "๐ฉ๐ด"], ["Ecuador", "๐ช๐จ"], ["Egypt", "๐ช๐ฌ"], ["El Salvador", "๐ธ๐ป"], ["Equatorial Guinea", "๐ฌ๐ถ"], ["Eritrea", "๐ช๐ท"], ["Estonia", "๐ช๐ช"], ["Ethiopia", "๐ช๐น"], ["Falkland Islands (Malvinas)", "๐ซ๐ฐ"], ["Faroe Islands", "๐ซ๐ด"], ["Fiji", "๐ซ๐ฏ"], ["Finland", "๐ซ๐ฎ"], ["France", "๐ซ๐ท"], ["French Guiana", "๐ฌ๐ซ"], ["French Polynesia", "๐ต๐ซ"], ["French Southern Territories", "๐น๐ซ"], ["Gabon", "๐ฌ๐ฆ"], ["Gambia", "๐ฌ๐ฒ"], ["Georgia", "๐ฌ๐ช"], ["Germany", "๐ฉ๐ช"], ["Ghana", "๐ฌ๐ญ"], ["Gibraltar", "๐ฌ๐ฎ"], ["Greece", "๐ฌ๐ท"], ["Greenland", "๐ฌ๐ฑ"], ["Grenada", "๐ฌ๐ฉ"], ["Guadeloupe", "๐ฌ๐ต"], ["Guam", "๐ฌ๐บ"], ["Guatemala", "๐ฌ๐น"], ["Guernsey", "๐ฌ๐ฌ"], ["Guinea", "๐ฌ๐ณ"], ["Guinea-Bissau", "๐ฌ๐ผ"], ["Guyana", "๐ฌ๐พ"], ["Haiti", "๐ญ๐น"], ["Heard Island and McDonald Islands", "๐ญ๐ฒ"], ["Holy See (Vatican City State)", "๐ป๐ฆ"], ["Honduras", "๐ญ๐ณ"], ["Hong Kong", "๐ญ๐ฐ"], ["Hungary", "๐ญ๐บ"], ["Iceland", "๐ฎ๐ธ"], ["India", "๐ฎ๐ณ"], ["Indonesia", "๐ฎ๐ฉ"], ["Iran, Islamic Republic of", "๐ฎ๐ท"], ["Iraq", "๐ฎ๐ถ"], ["Ireland", "๐ฎ๐ช"], ["Isle of Man", "๐ฎ๐ฒ"], ["Israel", "๐ฎ๐ฑ"], ["Italy", "๐ฎ๐น"], ["Jamaica", "๐ฏ๐ฒ"], ["Japan", "๐ฏ๐ต"], ["Jersey", "๐ฏ๐ช"], ["Jordan", "๐ฏ๐ด"], ["Kazakhstan", "๐ฐ๐ฟ"], ["Kenya", "๐ฐ๐ช"], ["Kiribati", "๐ฐ๐ฎ"], ["Korea, Democratic People's Republic of", "๐ฐ๐ต"], ["Korea, Republic of", "๐ฐ๐ท"], ["Kuwait", "๐ฐ๐ผ"], ["Kyrgyzstan", "๐ฐ๐ฌ"], ["Lao People's Democratic Republic", "๐ฑ๐ฆ"], ["Latvia", "๐ฑ๐ป"], ["Lebanon", "๐ฑ๐ง"], ["Lesotho", "๐ฑ๐ธ"], ["Liberia", "๐ฑ๐ท"], ["Libya", "๐ฑ๐พ"], ["Liechtenstein", "๐ฑ๐ฎ"], ["Lithuania", "๐ฑ๐น"], ["Luxembourg", "๐ฑ๐บ"], ["Macao", "๐ฒ๐ด"], ["Macedonia, the Former Yugoslav Republic of", "๐ฒ๐ฐ"], ["Madagascar", "๐ฒ๐ฌ"], ["Malawi", "๐ฒ๐ผ"], ["Malaysia", "๐ฒ๐พ"], ["Maldives", "๐ฒ๐ป"], ["Mali", "๐ฒ๐ฑ"], ["Malta", "๐ฒ๐น"], ["Marshall Islands", "๐ฒ๐ญ"], ["Martinique", "๐ฒ๐ถ"], ["Mauritania", "๐ฒ๐ท"], ["Mauritius", "๐ฒ๐บ"], ["Mayotte", "๐พ๐น"], ["Mexico", "๐ฒ๐ฝ"], ["Micronesia, Federated States of", "๐ซ๐ฒ"], ["Moldova, Republic of", "๐ฒ๐ฉ"], ["Monaco", "๐ฒ๐จ"], ["Mongolia", "๐ฒ๐ณ"], ["Montenegro", "๐ฒ๐ช"], ["Montserrat", "๐ฒ๐ธ"], ["Morocco", "๐ฒ๐ฆ"], ["Mozambique", "๐ฒ๐ฟ"], ["Myanmar", "๐ฒ๐ฒ"], ["Namibia", "๐ณ๐ฆ"], ["Nauru", "๐ณ๐ท"], ["Nepal", "๐ณ๐ต"], ["Netherlands", "๐ณ๐ฑ"], ["New Caledonia", "๐ณ๐จ"], ["New Zealand", "๐ณ๐ฟ"], ["Nicaragua", "๐ณ๐ฎ"], ["Niger", "๐ณ๐ช"], ["Nigeria", "๐ณ๐ฌ"], ["Niue", "๐ณ๐บ"], ["Norfolk Island", "๐ณ๐ซ"], ["Northern Mariana Islands", "๐ฒ๐ต"], ["Norway", "๐ณ๐ด"], ["Oman", "๐ด๐ฒ"], ["Pakistan", "๐ต๐ฐ"], ["Palau", "๐ต๐ผ"], ["Palestine, State of", "๐ต๐ธ"], ["Panama", "๐ต๐ฆ"], ["Papua New Guinea", "๐ต๐ฌ"], ["Paraguay", "๐ต๐พ"], ["Peru", "๐ต๐ช"], ["Philippines", "๐ต๐ญ"], ["Pitcairn", "๐ต๐ณ"], ["Poland", "๐ต๐ฑ"], ["Portugal", "๐ต๐น"], ["Puerto Rico", "๐ต๐ท"], ["Qatar", "๐ถ๐ฆ"], ["Rรฉunion", "๐ท๐ช"], ["Romania", "๐ท๐ด"], ["Russian Federation", "๐ท๐บ"], ["Rwanda", "๐ท๐ผ"], ["Saint Barthรฉlemy", "๐ง๐ฑ"], ["Saint Helena, Ascension and Tristan da Cunha", "๐ธ๐ญ"], ["Saint Kitts and Nevis", "๐ฐ๐ณ"], ["Saint Lucia", "๐ฑ๐จ"], ["Saint Martin (French part)", "๐ฒ๐ซ"], ["Saint Pierre and Miquelon", "๐ต๐ฒ"], ["Saint Vincent and the Grenadines", "๐ป๐จ"], ["Samoa", "๐ผ๐ธ"], ["San Marino", "๐ธ๐ฒ"], ["Sao Tome and Principe", "๐ธ๐น"], ["Saudi Arabia", "๐ธ๐ฆ"], ["Senegal", "๐ธ๐ณ"], ["Serbia", "๐ท๐ธ"], ["Seychelles", "๐ธ๐จ"], ["Sierra Leone", "๐ธ๐ฑ"], ["Singapore", "๐ธ๐ฌ"], ["Sint Maarten (Dutch part)", "๐ธ๐ฝ"], ["Slovakia", "๐ธ๐ฐ"], ["Slovenia", "๐ธ๐ฎ"], ["Solomon Islands", "๐ธ๐ง"], ["Somalia", "๐ธ๐ด"], ["South Africa", "๐ฟ๐ฆ"], ["South Georgia and the South Sandwich Islands", "๐ฌ๐ธ"], ["South Sudan", "๐ธ๐ธ"], ["Spain", "๐ช๐ธ"], ["Sri Lanka", "๐ฑ๐ฐ"], ["Sudan", "๐ธ๐ฉ"], ["Suriname", "๐ธ๐ท"], ["Svalbard and Jan Mayen", "๐ธ๐ฏ"], ["Eswatini", "๐ธ๐ฟ"], ["Sweden", "๐ธ๐ช"], ["Switzerland", "๐จ๐ญ"], ["Syrian Arab Republic", "๐ธ๐พ"], ["Taiwan", "๐น๐ผ"], ["Tajikistan", "๐น๐ฏ"], ["Tanzania, United Republic of", "๐น๐ฟ"], ["Thailand", "๐น๐ญ"], ["Timor-Leste", "๐น๐ฑ"], ["Togo", "๐น๐ฌ"], ["Tokelau", "๐น๐ฐ"], ["Tonga", "๐น๐ด"], ["Trinidad and Tobago", "๐น๐น"], ["Tunisia", "๐น๐ณ"], ["Turkey", "๐น๐ท"], ["Turkmenistan", "๐น๐ฒ"], ["Turks and Caicos Islands", "๐น๐จ"], ["Tuvalu", "๐น๐ป"], ["Uganda", "๐บ๐ฌ"], ["Ukraine", "๐บ๐ฆ"], ["United Arab Emirates", "๐ฆ๐ช"], ["United Kingdom", "๐ฌ๐ง"], ["United States", "๐บ๐ธ"], ["United States Minor Outlying Islands", "๐บ๐ฒ"], ["Uruguay", "๐บ๐พ"], ["Uzbekistan", "๐บ๐ฟ"], ["Vanuatu", "๐ป๐บ"], ["Venezuela, Bolivarian Republic of", "๐ป๐ช"], ["Viet Nam", "๐ป๐ณ"], ["Virgin Islands, British", "๐ป๐ฌ"], ["Virgin Islands, U.S.", "๐ป๐ฎ"], ["Wallis and Futuna", "๐ผ๐ซ"], ["Western Sahara", "๐ช๐ญ"], ["Yemen", "๐พ๐ช"], ["Zambia", "๐ฟ๐ฒ"], ["Zimbabwe", "๐ฟ๐ผ"], ["Europe", "๐"], ["East Asia & Pacific", "๐"], ["North America", "๐"], ["South America", "๐"], ["Eastern Europe (Non-EU)", "๐"], ["West Asia", "๐"], ["South Asia", "๐"], ["Australasia", "๐"], ]); const dbPromise = new Promise((resolve) => { const request = indexedDB.open("aboutAccountCache", 1); request.onupgradeneeded = () => request.result.createObjectStore("countries"); request.onsuccess = () => resolve(request.result); request.onerror = () => resolve(null); }); if (!csrfToken) return; const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // --- HELPER: CSP BYPASS --- const gmFetch = (url, options) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || "GET", url: url, headers: options.headers, data: options.body, onload: (res) => { resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, json: () => Promise.resolve(JSON.parse(res.responseText)) }); }, onerror: (e) => reject(e) }); }); }; // --- 1. LOCAL STORAGE LOGIC --- const saveCountry = (username, country) => { dbPromise.then((db) => { if (!db) return; const tx = db.transaction("countries", "readwrite"); tx.objectStore("countries").put({ country, timestamp: Date.now() }, username); }); }; const getFromDb = (username) => { return dbPromise.then((db) => { if (!db) return null; return new Promise((resolve) => { const tx = db.transaction("countries", "readonly"); const req = tx.objectStore("countries").get(username); req.onsuccess = () => resolve(req.result); req.onerror = () => resolve(null); }); }); }; // --- 2. REMOTE INTERACTION (BATCH READ & WRITE) --- // A. BATCH READ let readBlocked = false; const flushReadBatch = async () => { const batch = [...new Set(readBuffer)]; readBuffer = []; readTimeout = null; if (batch.length === 0) return; if (readBlocked || REMOTE_DB_URL.includes("YOUR-SUBDOMAIN")) { batch.forEach(u => apiQueue.push(u)); processApiQueue(); return; } try { const res = await gmFetch(REMOTE_DB_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "batch_read", usernames: batch }) }); if (!res.ok) { if (res.status === 429 || res.status >= 500) { console.warn(`[Filter X] Cloudflare Read Failed (${res.status}). Switching to local-only mode.`); readBlocked = true; } throw new Error(`Worker returned ${res.status}`); } const results = await res.json(); batch.forEach(username => { if (results[username]) { const country = results[username]; cache.set(username, country); saveCountry(username, country); finalizeUser(username, country); } else { apiQueue.push(username); processApiQueue(); } }); } catch (e) { console.warn("[Filter X] Read Batch Failed", e); batch.forEach(u => apiQueue.push(u)); processApiQueue(); } }; const addToReadBatch = (username) => { readBuffer.push(username); if (!readTimeout) readTimeout = setTimeout(flushReadBatch, READ_BATCH_DELAY); }; // B. BATCH WRITE let writeBlocked = false; const flushWriteBatch = async () => { if (writeBlocked) { writeBuffer.clear(); return; } const batch = Array.from(writeBuffer, ([username, location]) => ({ username, location })); writeBuffer.clear(); writeTimeout = null; if (batch.length === 0) return; if (REMOTE_DB_URL.includes("YOUR-SUBDOMAIN")) return; try { const res = await gmFetch(REMOTE_DB_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "batch_write", entries: batch }) }); if (res.status === 429) { writeBlocked = true; return; } } catch (e) { console.warn("[Filter X] Write Error", e); } }; const addToWriteBatch = (username, location) => { if (writeBlocked) return; if (!username || !location) return; writeBuffer.set(username, location); if (writeBuffer.size >= WRITE_BATCH_SIZE) { if (writeTimeout) clearTimeout(writeTimeout); flushWriteBatch(); } else if (!writeTimeout) { writeTimeout = setTimeout(flushWriteBatch, WRITE_BATCH_DELAY); } }; // --- 3. X API LOGIC --- const fetchCountry = async (username) => { for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) { try { const params = new URLSearchParams({ variables: JSON.stringify({ screenName: username }) }); const res = await fetch(`${queryUrl}?${params.toString()}`, { method: "GET", headers: { authorization: authToken, "x-csrf-token": csrfToken }, credentials: "include", }); if (res.status === 429) return "RATE_LIMIT"; if (!res.ok) throw new Error(`HTTP ${res.status}`); const about = (await res.json())?.data?.user_result_by_screen_name?.result?.about_profile; return about?.account_based_in; } catch (e) { await wait(1000 * (attempt + 1) + Math.random() * 500); } } return null; }; const processApiQueue = () => { if (isRateLimited || activeRequests >= CONCURRENT_API_LIMIT || apiQueue.length === 0) return; while (activeRequests < CONCURRENT_API_LIMIT && apiQueue.length > 0) { const username = apiQueue.shift(); if (cache.has(username)) { finalizeUser(username, cache.get(username)); continue; } activeRequests++; (async () => { try { const country = await fetchCountry(username); if (country === "RATE_LIMIT") { isRateLimited = true; apiQueue.unshift(username); setTimeout(() => { isRateLimited = false; processApiQueue(); }, RATE_LIMIT_PAUSE); return; } if (country) { cache.set(username, country); saveCountry(username, country); addToWriteBatch(username, country); finalizeUser(username, country); } else { pending.delete(username); } } finally { activeRequests--; const delay = (REQUEST_DELAY / CONCURRENT_API_LIMIT) + (Math.random() * RANDOM_JITTER); setTimeout(processApiQueue, delay); } })(); } }; // --- 4. DOM LOGIC --- const finalizeUser = (username, country) => { const targets = pending.get(username); if (!targets) return; targets.forEach(({ container, tweet, type }) => applyCountry(username, container, tweet, country, type)); pending.delete(username); }; const enqueue = (username, container, tweet, type = "tweet") => { if (cache.has(username)) { applyCountry(username, container, tweet, cache.get(username), type); return; } const existingTargets = pending.get(username); if (existingTargets) { existingTargets.push({ container, tweet, type }); return; } pending.set(username, [{ container, tweet, type }]); getFromDb(username).then((record) => { if (!pending.has(username)) return; if (record && (Date.now() - record.timestamp < CACHE_TTL)) { cache.set(username, record.country); finalizeUser(username, record.country); } else { addToReadBatch(username); } }); }; const applyCountry = (username, container, targetElement, country, type = "tweet") => { const flag = countryFlags.get(country) || country; const flagText = ` ${flag}`; let matchesFilter = false; if (filterEnabled && filterValues.length && country) { const lowerCountry = country.toLowerCase(); const match = filterValues.some((value) => lowerCountry.includes(value.toLowerCase())); matchesFilter = filterMode === "whitelist" ? !match : match; } const shouldBlock = matchesFilter; if (container) { const existing = container.querySelector(".filter-x-flag"); let flagNode; if (existing) { flagNode = existing; flagNode.textContent = flagText; } else { flagNode = document.createElement("span"); flagNode.className = "filter-x-flag"; flagNode.dataset.accountBasedIn = "true"; flagNode.textContent = flagText; flagNode.style.padding = type === "profile" ? "0px" : "0px 10px"; flagNode.style.cursor = "help"; container.appendChild(flagNode); } flagNode.title = country; } if (type === "tweet" && targetElement) { const tweetWrapper = targetElement.closest?.('[data-testid="tweet"]') || targetElement.parentElement?.parentElement?.parentElement; if (tweetWrapper) { tweetWrapper.style.display = shouldBlock ? "none" : ""; } } }; const handleTweet = (tweet) => { const userNameRoot = tweet.querySelector('[data-testid="User-Name"]'); if (!userNameRoot) return; const anchor = userNameRoot.querySelector("a"); const username = anchor?.getAttribute("href")?.replace(/^\//, ""); if (!username) return; let container = anchor; while (container.parentElement && container.parentElement !== userNameRoot) { container = container.parentElement; } enqueue(username, container, tweet, "tweet"); }; const handleProfile = (headerStats) => { const pathParts = window.location.pathname.split('/'); const username = pathParts[1]; if (!username) return; const ignoreList = ["home", "explore", "notifications", "messages", "settings", "search", "bookmarks"]; if (ignoreList.includes(username)) return; const primaryCol = document.querySelector('[data-testid="primaryColumn"]'); const nameContainer = primaryCol?.querySelector('[data-testid="UserName"]'); enqueue(username, nameContainer, null, "profile"); }; const onUrlChange = () => { const profileHeader = document.querySelector('[data-testid="UserProfileHeader_Items"]'); if (profileHeader) { delete profileHeader.dataset.filterXObserved; profileHeader.querySelector('.filter-x-flag')?.remove(); handleProfile(profileHeader); } }; // Hook into history API const originalPushState = history.pushState; history.pushState = function() { const res = originalPushState.apply(this, arguments); onUrlChange(); return res; }; const originalReplaceState = history.replaceState; history.replaceState = function() { const res = originalReplaceState.apply(this, arguments); onUrlChange(); return res; }; window.addEventListener('popstate', onUrlChange); const observerCallback = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { handleTweet(entry.target); observer.unobserve(entry.target); } }); }; const tweetObserver = new IntersectionObserver(observerCallback, { rootMargin: "200px 0px" }); const findTweets = (root) => { if (root.matches?.('[data-testid="tweet"]') && !root.dataset.filterXObserved) { root.dataset.filterXObserved = "true"; tweetObserver.observe(root); return; } if (root.querySelectorAll) { const tweets = root.querySelectorAll('[data-testid="tweet"]:not([data-filter-x-observed])'); for (let i = 0; i < tweets.length; i++) { const tweet = tweets[i]; tweet.dataset.filterXObserved = "true"; tweetObserver.observe(tweet); } } }; const findProfile = (root) => { const profileHeader = root.matches?.('[data-testid="UserProfileHeader_Items"]') ? root : root.querySelector?.('[data-testid="UserProfileHeader_Items"]'); if (!profileHeader) return; const currentUsername = window.location.pathname.split('/')[1]; const observedUsername = profileHeader.dataset.filterXObserved; if (observedUsername && observedUsername !== currentUsername) { profileHeader.querySelector('.filter-x-flag')?.remove(); profileHeader.dataset.filterXObserved = ""; // Force reset } if (!profileHeader.dataset.filterXObserved && currentUsername) { profileHeader.dataset.filterXObserved = currentUsername; handleProfile(profileHeader); } }; const findNav = (root) => { const nav = root.querySelector?.('nav[data-testid="AppTabBar"]') || root.querySelector?.('nav[role="navigation"]'); if (!nav || nav.querySelector("[data-cutoff-config]")) return; const wrapper = document.createElement("div"); wrapper.dataset.cutoffConfig = "true"; wrapper.style.margin = "8px 0"; wrapper.style.fontFamily = 'TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'; wrapper.innerHTML = `
`; const button = wrapper.querySelector("button"); const menu = wrapper.querySelector("div"); const checkbox = menu?.querySelector('input[type="checkbox"]'); const textarea = menu?.querySelector("textarea"); const select = menu?.querySelector("select"); if (!button || !menu || !checkbox || !textarea || !select) return; button.addEventListener("click", () => { menu.style.display = "block"; button.setAttribute("aria-expanded", "true"); }); document.addEventListener("click", (event) => { if (!wrapper.contains(event.target)) { menu.style.display = "none"; button.setAttribute("aria-expanded", "false"); } }); checkbox.addEventListener("change", () => { filterEnabled = checkbox.checked; localStorage.setItem("tweetFilterEnabled", filterEnabled.toString()); document.querySelectorAll('[data-testid="tweet"]').forEach(t => handleTweet(t)); const profileH = document.querySelector('[data-testid="UserProfileHeader_Items"]'); if(profileH) handleProfile(profileH); }); textarea.addEventListener("change", () => { filterValues = textarea.value .split("\n") .map((value) => value.trim()) .filter(Boolean); localStorage.setItem("tweetFilterValues", filterValues.join("\n")); document.querySelectorAll('[data-testid="tweet"]').forEach(t => handleTweet(t)); const profileH = document.querySelector('[data-testid="UserProfileHeader_Items"]'); if(profileH) handleProfile(profileH); }); select.addEventListener("change", () => { filterMode = select.value; localStorage.setItem("tweetFilterMode", filterMode); document.querySelectorAll('[data-testid="tweet"]').forEach(t => handleTweet(t)); const profileH = document.querySelector('[data-testid="UserProfileHeader_Items"]'); if(profileH) handleProfile(profileH); }); nav.appendChild(wrapper); }; findNav(document); findTweets(document); findProfile(document); new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { findTweets(node); if (node.tagName === 'NAV' || node.querySelector?.('nav')) findNav(node); if (node.querySelector?.('[data-testid="UserProfileHeader_Items"]')) findProfile(node); } } } }).observe(document, { childList: true, subtree: true }); })();