Twitter Follower Count

Display the number of followers for Twitter accounts

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Twitter Follower Count
// @namespace    amm1rr.com.twitter.follower.count
// @version      0.3.2
// @homepage     https://github.com/Amm1rr/Twitter-Follower-Count/
// @description  Display the number of followers for Twitter accounts
// @author       Mohammad Khani (@m_khani65)
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  /**
   * Cache to store user data to prevent redundant processing.
   * Key: screen_name, Value: user details object.
   * @type {Map<string, Object>}
   */
  const userCache = new Map();
  /**
   * Store reference to the original XMLHttpRequest.send method.
   */

  const originalSend = XMLHttpRequest.prototype.send;
  /**
   * Override XMLHttpRequest.send to intercept API responses.
   * Filters for responses from Twitter API endpoints and extracts user data,
   * caching the data if it's not already present.
   *
   * @param {...any} args - Arguments passed to the original send method.
   */

  XMLHttpRequest.prototype.send = function (...args) {
    this.addEventListener("load", () => {
      // Process only API responses relevant to Twitter data.
      if (!this.responseURL || !this.responseURL.includes("/i/api/")) return;

      let responseData;
      try {
        responseData = decodeResponse(this);
        if (!responseData) return;
      } catch (e) {
        console.error("Failed to decode response:", e);
        return;
      }

      try {
        const responseJSON = JSON.parse(responseData);
        const users = extractUsers(responseJSON, "screen_name");
        users.forEach((user) => {
          if (!user.screen_name || !user.followers_count) return; // Cache the user data if not already present.
          if (!userCache.has(user.screen_name)) {
            cacheUserData(user);
          }
        });
      } catch (e) {
        // Fail silently if JSON parsing fails.
      }
    });
    originalSend.apply(this, args);
  };
  /**
   * Decodes the XMLHttpRequest response based on its type.
   *
   * @param {XMLHttpRequest} xhr - The XMLHttpRequest object.
   * @returns {string|null} The decoded response data or null if decoding fails.
   */

  const decodeResponse = (xhr) => {
    if (xhr.responseType === "" || xhr.responseType === "text") {
      return xhr.responseText;
    } else if (xhr.responseType === "arraybuffer") {
      return new TextDecoder("utf-8").decode(xhr.response);
    }
    return null;
  };
  /**
   * Caches user data.
   *
   * @param {Object} user - The user data object from the API.
   */

  const cacheUserData = (user) => {
    userCache.set(user.screen_name, {
      name: user.name,
      screen_name: user.screen_name,
      followers_count: user.followers_count,
      formatted_followers_count: formatFollowers(user.followers_count),
      friends_count: user.friends_count,
    });
  };
  /**
   * Recursively traverse an object to extract all sub-objects containing a specific key.
   *
   * @param {Object} obj - The object to traverse.
   * @param {string} key - The key to search for (e.g., "screen_name").
   * @param {Array<Object>} [result=[]] - Array to accumulate found objects.
   * @returns {Array<Object>} Array of objects that contain the specified key.
   */

  const extractUsers = (obj, key, result = []) => {
    for (const value of Object.values(obj)) {
      if (value && typeof value === "object") {
        if (value.hasOwnProperty(key)) {
          result.push(value);
        }
        extractUsers(value, key, result);
      }
    }
    return result;
  };
  /**
   * Format a number into a human-readable string with K/M suffix.
   *
   * @param {number} number - The number of followers.
   * @returns {string} Formatted number string.
   */

  const formatFollowers = (number) => {
    if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`;
    if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
    return number.toString();
  };
  /**
   * Create a DOM element (span) to display the formatted follower count.
   *
   * @param {string} formattedCount - The formatted follower count string.
   * @returns {HTMLElement} The created span element.
   */

  const createFollowerCountElement = (formattedCount) => {
    const span = document.createElement("span");
    span.className = "count-follower";
    span.innerText = formattedCount;
    Object.assign(span.style, {
      position: "absolute",
      bottom: "-2px",
      left: "50%",
      transform: "translate(-50%)",
      fontSize: "8px",
      fontWeight: "bold",
      color: "#ffffff",
      backgroundColor: "rgb(29, 155, 240)",
      border: "1px solid #0867d2",
      borderRadius: "9999px",
      padding: "0px 4px",
      whiteSpace: "nowrap",
    });
    return span;
  };
  /**
   * Update the DOM to display follower counts for all cached users.
   * It searches for profile links in the document and appends the follower count
   * element next to the profile image.
   */

  const updateFollowerCounts = () => {
    userCache.forEach((user, screen_name) => {
      const profileLinks = document.querySelectorAll(
        `a[href*="/${screen_name}"]` // Modified selector to be more robust
      );
      profileLinks.forEach((link) => {
        // Skip if follower count is already appended.
        if (link.querySelector(".count-follower")) return;
        const parent = link.parentNode;
        if (!parent) return;
        const img = parent.querySelector('img[draggable="true"]');
        if (!img) return; // Adjust styles for proper display.

        parent.style.overflow = "inherit";
        parent.style.clipPath = "none";
        const closestUl = parent.closest("ul");
        if (closestUl) {
          closestUl.style.overflow = "inherit";
        }

        const span = createFollowerCountElement(user.formatted_followers_count);
        link.appendChild(span);
      });
    });
  };
  /**
   * Debounce function to limit the rate at which a function is executed.
   *
   * @param {Function} func - The function to debounce.
   * @param {number} delay - The delay in milliseconds.
   * @returns {Function} A debounced version of the given function.
   */

  const debounce = (func, delay) => {
    let timeoutId;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
  }; // Debounced version of updateFollowerCounts to reduce excessive calls.

  const debouncedUpdateFollowerCounts = debounce(updateFollowerCounts, 100); // Update follower counts on initial page load and during scroll events.

  window.addEventListener("load", debouncedUpdateFollowerCounts);
  document.addEventListener("scroll", debouncedUpdateFollowerCounts);
  /**
   * Use MutationObserver to monitor changes in the DOM.
   * This helps in detecting dynamically added elements (e.g., new user profiles)
   * and triggers an update to append follower counts accordingly.
   */

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