PayCheck for X (Formerly Twitter)

See (a very VERY rough idea of) how much money a post is worth.

// ==UserScript==
// @name         PayCheck for X (Formerly Twitter)
// @description  See (a very VERY rough idea of) how much money a post is worth.
// @version      0.0.5
// @author       yungsamd17 & Theo @t3dotgg
// @namespace    https://github.com/yungsamd17/paycheck-userscript
// @icon         https://raw.githubusercontent.com/yungsamd17/paycheck-userscript/main/assets/paycheck-for-twitter.png
// @match        https://twitter.com/*
// @match        https://mobile.twitter.com/*
// @match        https://tweetdeck.twitter.com/*
// @match        https://x.com/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

function convertToRawCount(internationalInputString) {
  const numberPattern = /([\d,.]+)([kmb]*)/i;
  const matches = internationalInputString.match(numberPattern);

  if (!matches) {
    return NaN; // Return NaN if the input doesn't match the expected pattern
  }

  const numericPart = matches[1];
  const multiplier = matches[2].toLowerCase();

  let numericValue;

  const lastChars = [
    numericPart.slice(-1),
    numericPart.slice(-2, -1),
    numericPart.slice(-3, -2),
  ];

  // Check if second or third to last character are , or . to handle international numbers
  if (lastChars.includes(".") || lastChars.includes(",")) {
    const parts = numericPart.replace(",", ".").split(".");
    const integerPart = parts[0].replace(/[,]/g, "");
    const decimalPart = parts[1] ? parts[1] : "0";
    numericValue = parseFloat(integerPart + "." + decimalPart);
  } else {
    numericValue = parseFloat(numericPart.replaceAll(",", ""));
  }

  let factor = 1;

  switch (multiplier) {
    case "k":
      factor = 1000;
      break;
    case "m":
      factor = 1000000;
      break;
    case "b":
      factor = 1000000000;
      break;
  }

  return Math.round(numericValue * factor);
}

function convertToDollars(number) {
  const rawCount = convertToRawCount(number);

  const processed = rawCount * 0.000026;
  if (processed < 0.1) return processed.toFixed(5);
  return processed.toFixed(2);
}

const globalSelectors = {};
globalSelectors.postCounts = `[role="group"][id*="id__"]:only-child`;
globalSelectors.articleDate = `[role="article"][aria-labelledby*="id__"][tabindex="-1"] time`;
globalSelectors.analyticsLink = " :not(.dollarBox)>a[href*='/analytics']";
globalSelectors.viewCount =
  globalSelectors.postCounts + globalSelectors.analyticsLink;

const innerSelectors = {};
innerSelectors.dollarSpot = "div div:first-child";
innerSelectors.viewSVG = "div div:first-child svg";
innerSelectors.viewAmount = "div div:last-child span span span";
innerSelectors.articleViewAmount = "span div:first-child span span span";

function doWork() {
  const viewCounts = Array.from(
    document.querySelectorAll(globalSelectors.viewCount)
  );

  const articleViewDateSections = document.querySelectorAll(globalSelectors.articleDate);

  if (articleViewDateSections.length) {
    // the rootDateViewsSection will always be the parent->parent->parent of the last element of the articleDate querySelectorAll result
    let rootDateViewsSection = articleViewDateSections[articleViewDateSections.length - 1].parentElement.parentElement.parentElement;

    // if there is one child, that means it's an old tweet with no viewcount
    // if there are more than 4, we already added the paycheck value
    if (rootDateViewsSection?.children?.length !== 1 && rootDateViewsSection?.children.length < 4) {
      // clone 2nd and 3rd child of rootDateViewsSection
      const clonedDateViewSeparator =
        rootDateViewsSection?.children[1].cloneNode(true);
      const clonedDateView = rootDateViewsSection?.children[2].cloneNode(true);

      // insert clonedDateViews and clonedDateViewsTwo after the 3rd child we just cloned
      rootDateViewsSection?.insertBefore(
        clonedDateViewSeparator,
        rootDateViewsSection?.children[2].nextSibling
      );
      rootDateViewsSection?.insertBefore(
        clonedDateView,
        rootDateViewsSection?.children[3].nextSibling
      );

      // get view count value from 'clonedDateViewsTwo'
      const viewCountValue = clonedDateView?.querySelector(
        innerSelectors.articleViewAmount
      )?.textContent;
      const dollarAmount = convertToDollars(viewCountValue);

      // replace textContent in cloned clonedDateViews (now 4th child) with converted view count value
      clonedDateView.querySelector(
        innerSelectors.articleViewAmount
      ).textContent = "$" + dollarAmount;

      // remove 'views' label
      clonedDateView.querySelector(`span`).children[1].remove();
    }
  }

  for (const view of viewCounts) {
    // only add the dollar box once
    if (!view.classList.contains("replaced")) {
      // make sure we don't touch this one again
      view.classList.add("replaced");

      // get parent and clone to make dollarBox
      const parent = view.parentElement;
      const dollarBox = parent.cloneNode(true);
      dollarBox.classList.add("dollarBox");

      // insert dollarBox after view count
      parent.parentElement.insertBefore(dollarBox, parent.nextSibling);

      // remove view count icon
      const oldIcon = dollarBox.querySelector(innerSelectors.viewSVG);
      oldIcon?.remove();

      // swap the svg for a dollar sign
      const dollarSpot = dollarBox.querySelector(innerSelectors.dollarSpot)
        ?.firstChild?.firstChild;
      dollarSpot.textContent = "$";

      // magic alignment value
      dollarSpot.style.marginTop = "-0.6rem";
    }

    // get the number of views and calculate & set the dollar amount
    const dollarBox = view.parentElement.nextSibling.firstChild;
    const viewCount = view.querySelector(
      innerSelectors.viewAmount
    )?.textContent;
    if (viewCount == undefined) continue;
    const dollarAmountArea = dollarBox.querySelector(innerSelectors.viewAmount);
    dollarAmountArea.textContent = convertToDollars(viewCount);
  }
}

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function () {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function () {
        if (Date.now() - lastRan >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

// Function to start MutationObserver
const observe = () => {
  const runDocumentMutations = throttle(() => {
    requestAnimationFrame(doWork);
  }, 1000);

  const observer = new MutationObserver((mutationsList) => {
    if (!mutationsList.length) return;
    runDocumentMutations();
  });

  observer.observe(document, {
    childList: true,
    subtree: true,
  });
};

observe();