Automatic Repost Hider on X

Automatically hides reposts on X with optional smart filtering by keywords

// ==UserScript==
// @name         Automatic Repost Hider on X
// @description  Automatically hides reposts on X with optional smart filtering by keywords
// @namespace    https://github.com/xechostormx/repost-hider
// @version      1.02
// @author       xechostormx, hearing_echoes (enhanced by Grok)
// @match        https://x.com/*
// @match        https://www.x.com/*
// @match        https://mobile.x.com/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

'use strict';

// CSS selectors for tweets and reposts
const selectors = {
  tweet: '[data-testid="cellInnerDiv"]',
  repost: '[data-testid="socialContext"]',
  hidden: '[style*="display: none;"]',
  tweetText: '[data-testid="tweetText"]' // For keyword filtering
};

// Configuration
const config = {
  checkInterval: 750, // Increased for better performance
  debug: false, // Set to true for console logging
  keywordFilter: [], // Add keywords to filter specific reposts, e.g., ['spam', 'ad']
};

// Track hidden reposts for performance monitoring
let hiddenCount = 0;

// Debounce function to limit excessive calls during scrolling
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Main function to hide reposts
function hideReposts() {
  const reposts = document.querySelectorAll(
    `${selectors.tweet}:has(${selectors.repost}):not(${selectors.hidden})`
  );

  reposts.forEach(tweet => {
    // Optional keyword-based filtering
    if (config.keywordFilter.length > 0) {
      const tweetText = tweet.querySelector(selectors.tweetText)?.textContent.toLowerCase() || '';
      if (!config.keywordFilter.some(keyword => tweetText.includes(keyword.toLowerCase()))) {
        return; // Skip if no keywords match
      }
    }

    tweet.style.display = 'none';
    hiddenCount++;
    if (config.debug) {
      console.debug(`Hid repost #${hiddenCount}`);
    }
  });

  // Log performance metrics if enabled
  if (config.debug && reposts.length > 0) {
    console.debug(`Processed ${reposts.length} reposts in this cycle`);
  }
}

// Debounced version of hideReposts
const debouncedHideReposts = debounce(hideReposts, config.checkInterval);

// Initialize observer for dynamic content
function initObserver() {
  if (!document.body) {
    if (config.debug) console.debug('Document body not ready, retrying...');
    setTimeout(initObserver, 100);
    return;
  }

  const observerConfig = { childList: true, subtree: true };
  const observer = new MutationObserver(() => debouncedHideReposts());
  observer.observe(document.body, observerConfig);

  // Clean up observer on page unload
  window.addEventListener('unload', () => observer.disconnect());
}

// Start the script
function initialize() {
  window.addEventListener('scroll', debouncedHideReposts);
  initObserver();
  debouncedHideReposts(); // Initial run
}

// Run when DOM is ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
  initialize();
} else {
  document.addEventListener('DOMContentLoaded', initialize);
}