Filter X.com Location

Script to filter X content by user location.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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