Filter X.com Location

Script to filter X content by user location.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Filter X.com Location
// @namespace    mailto:[email protected]
// @version      0.2.2
// @description  Script to filter X content by user location.
// @match        https://x.com/*
// @grant        GM_xmlhttpRequest
// @license AGPL3.0
// ==/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 = 1500;
  const RATE_LIMIT_PAUSE = 60000;
  const MAX_RETRIES = 5;
  const CACHE_TTL = 1000 * 60 * 60 * 24 * 30;

  const csrfToken = decodeURIComponent(document.cookie.match(/(?:^|; )ct0=([^;]+)/)?.[1] || "");
  const cache = new Map();
  const pending = new Map();
  const apiQueue = [];

  let processingApi = false;
  let isRateLimited = false;

  // READ Batching
  let readBuffer = [];
  let readTimeout = null;

  // WRITE Batching
  let writeBuffer = [];
  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 hydrateCache = () =>
    dbPromise.then((db) => {
      if (!db) return;
      return new Promise((resolve) => {
        const tx = db.transaction("countries", "readonly");
        const now = Date.now();
        tx.objectStore("countries").openCursor().onsuccess = (event) => {
          const cursor = event.target.result;
          if (cursor) {
            const val = cursor.value;
            const country = typeof val === 'string' ? val : val.country;
            const time = typeof val === 'string' ? 0 : val.timestamp;
            if (now - time < CACHE_TTL) cache.set(cursor.key, country);
            cursor.continue();
          }
        };
        tx.oncomplete = resolve;
      });
    });

  // --- 2. REMOTE INTERACTION (BATCH READ & WRITE) ---

  // A. BATCH READ
  const flushReadBatch = async () => {
    const batch = [...new Set(readBuffer)];
    readBuffer = [];
    readTimeout = null;
    if (batch.length === 0) return;

    if (REMOTE_DB_URL.includes("YOUR-SUBDOMAIN")) { console.warn("[Filter X] Worker URL not set"); return; }

    try {
      // Use gmFetch to bypass CSP
      const res = await gmFetch(REMOTE_DB_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "batch_read", usernames: batch })
      });

      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 (Likely CSP or Net Error)", e);
      // Fallback: send all to API queue
      batch.forEach(u => apiQueue.push(u));
      processApiQueue();
    }
  };

  const addToReadBatch = (username) => {
    readBuffer.push(username);
    if (!readTimeout) readTimeout = setTimeout(flushReadBatch, READ_BATCH_DELAY);
  };

  // B. BATCH WRITE
  const flushWriteBatch = async () => {
    const batch = [...writeBuffer];
    writeBuffer = []; // Clear buffer immediately
    writeTimeout = null;

    if (batch.length === 0) return;
    if (REMOTE_DB_URL.includes("YOUR-SUBDOMAIN")) return;

    console.log(`[Filter X] Uploading ${batch.length} new locations to Cloudflare...`);

    try {
      // Use gmFetch to bypass CSP
      const res = await gmFetch(REMOTE_DB_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "batch_write", entries: batch })
      });
      if (!res.ok) console.warn("[Filter X] Write Upload Failed", res.status);
    } catch (e) {
      console.warn("[Filter X] Write Error", e);
    }
  };

  const addToWriteBatch = (username, location) => {
    if (!username || !location) return;
    writeBuffer.push({ username, location });

    if (writeBuffer.length >= WRITE_BATCH_SIZE) {
        if (writeTimeout) clearTimeout(writeTimeout);
        flushWriteBatch();
    } else if (!writeTimeout) {
        writeTimeout = setTimeout(flushWriteBatch, WRITE_BATCH_DELAY);
    }
  };

  // --- 3. X API LOGIC (Standard Fetch) ---
  const fetchCountry = async (username) => {
    for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
      try {
        const params = new URLSearchParams({ variables: JSON.stringify({ screenName: username }) });
        // NOTE: We KEEP standard fetch here because X API is Same-Origin (or allowed) and needs browser cookies/headers automatically.
        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 = async () => {
    if (processingApi || isRateLimited) return;
    processingApi = true;

    while (apiQueue.length) {
      if (isRateLimited) break;
      const username = apiQueue.shift();

      if (cache.has(username)) {
         finalizeUser(username, cache.get(username));
         continue;
      }

      const country = await fetchCountry(username);

      if (country === "RATE_LIMIT") {
          isRateLimited = true;
          apiQueue.unshift(username);
          setTimeout(() => { isRateLimited = false; processApiQueue(); }, RATE_LIMIT_PAUSE);
          break;
      }

      if (country) {
        cache.set(username, country);
        saveCountry(username, country);

        // Add to Write Batch
        addToWriteBatch(username, country);

        finalizeUser(username, country);
      } else {
        pending.delete(username);
      }

      if (apiQueue.length > 0) {
        await wait(REQUEST_DELAY + Math.random() * RANDOM_JITTER);
      }
    }
    processingApi = false;
  };

  // --- 4. DOM LOGIC ---
  const finalizeUser = (username, country) => {
      const targets = pending.get(username);
      if (!targets) return;
      targets.forEach(({ container, tweet }) => applyCountry(container, tweet, country));
      pending.delete(username);
  };

  const enqueue = (username, container, tweet) => {
    if (cache.has(username)) {
      applyCountry(container, tweet, cache.get(username));
      return;
    }
    const existingTargets = pending.get(username);
    if (existingTargets) {
      existingTargets.push({ container, tweet });
      return;
    }
    pending.set(username, [{ container, tweet }]);
    addToReadBatch(username);
  };

  const applyCountry = (container, tweet, country) => {
    const flag = countryFlags.get(country) || country;
    const flagText = ` ${flag}`;
    const existing = container.querySelector("[data-account-based-in]");

    if (existing) {
      existing.textContent = flagText;
      existing.title = country;
    } else {
      const node = document.createElement("span");
      node.dataset.accountBasedIn = "true";
      node.textContent = flagText;
      node.style.padding = "0px 10px";
      node.style.cursor = "help";
      node.title = country;
      container.appendChild(node);
    }
    const target = tweet?.parentElement?.parentElement?.parentElement;
    if (target) {
      if (filterEnabled && filterValues.length && country) {
        const lowerCountry = country.toLowerCase();
        const match = filterValues.some((value) => lowerCountry.includes(value.toLowerCase()));
        const show = filterMode === "whitelist" ? match : !match;
        target.style.display = show ? "" : "none";
      } else {
        target.style.display = "";
      }
    }
  };

  const handleTweet = (tweet) => {
    const container = tweet.querySelector('[data-testid="User-Name"]');
    const username = container?.querySelector("a")?.getAttribute("href")?.replace(/^\//, "");
    if (!username) return;
    enqueue(username, container, tweet);
  };

  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) => {
    const tweets = root?.querySelectorAll?.('[data-testid="tweet"]');
    if (tweets) {
        tweets.forEach(tweet => {
            if (!tweet.dataset.filterXObserved) {
                tweet.dataset.filterXObserved = "true";
                tweetObserver.observe(tweet);
            }
        });
    }
    if (root?.matches?.('[data-testid="tweet"]')) {
         if (!root.dataset.filterXObserved) {
             root.dataset.filterXObserved = "true";
             tweetObserver.observe(root);
         }
    }
  };

  const findNav = (root) => {
    const nav = root?.querySelector?.('nav[aria-label="Primary"][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 = `
      <button type="button" aria-haspopup="menu" aria-expanded="false" style="width: 100%; display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: inherit; font-size: 20px; font-weight: 400; line-height: 24px;">
        <svg viewBox="0 0 24 24" aria-hidden="true" style="width: 1.75rem; height: 1.75rem; flex-shrink: 0;"><path d="M4 5h16v2l-6 6v4l-4 2v-6L4 7z" fill="currentColor"></path></svg>
        <span style="white-space: nowrap;">Filters</span>
      </button>
      <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 12px; min-width: 220px; border-radius: 12px; background: #000; display: none; z-index: 1000; border: 1px solid #333; box-shadow: 0 0 10px rgba(0,0,0,0.5);">
        <label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: white;">
          <input type="checkbox" ${filterEnabled ? "checked" : ""}>
          Enable filter
        </label>
        <label style="display: block; color: white;">
          Filter entries (one per line)
          <textarea style="width: 100%; margin-top: 4px; min-height: 80px; background: #222; color: white; border: 1px solid #444;">${filterValues.join("\n")}</textarea>
        </label>
        <label style="display: block; margin-top: 8px; color: white;">
          Mode
          <select style="width: 100%; margin-top: 4px; background: #222; color: white; border: 1px solid #444;">
            <option value="blacklist"${filterMode === "blacklist" ? " selected" : ""}>Blacklist</option>
            <option value="whitelist"${filterMode === "whitelist" ? " selected" : ""}>Whitelist</option>
          </select>
        </label>
      </div>
    `;
    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));
    });
    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));
    });
    select.addEventListener("change", () => {
      filterMode = select.value;
      localStorage.setItem("tweetFilterMode", filterMode);
      document.querySelectorAll('[data-testid="tweet"]').forEach(t => handleTweet(t));
    });
    nav.appendChild(wrapper);
  };

  hydrateCache().then(() => {
    findNav(document);
    findTweets(document);
  });

  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === 1) {
             findTweets(node);
             findNav(node);
        }
      });
    });
  }).observe(document, { childList: true, subtree: true });
})();