Twitter X Title

Change Twitter X Title

// ==UserScript==
// @name        Twitter X Title
// @namespace   TwitterX
// @match       https://twitter.com/*
// @grant       none
// @version     0.2.3
// @author      CY Fung
// @description Change Twitter X Title
// @run-at      document-start
// @license     MIT
// @unwrap
// @inject-into page
// ==/UserScript==


(function () {
  'use strict';

  let i18nXs = null;

  const i18nCache = new Set();

  function customTitle(p) {
    if (typeof p !== 'string' || !p.includes('X')) return p;

    if (!i18nXs && window.webpackChunk_twitter_responsive_web) {
      i18nXs = generateI18nXs();
      console.log('i18nXs', i18nXs)
    }

    let q = p.replace(/[\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]/g, ' ');
    const mxLen = q.length;
    let skipI18N = false;
    if (q.replace(/[\(\d\)]/g, '').trim() === 'X') {
      q = q.replace('X', 'Twitter');
      skipI18N = true;
    } else if (q.endsWith(' / X')) {
      q = q.substring(0, q.length - ' / X'.length) + ' / Twitter';
    }


    if (!skipI18N) {
      let uv = null;
      for (const i18nX of i18nXs) {
        const { s, l, y, m } = i18nX;
        if (uv !== null && uv !== m) break;
        if (mxLen >= l && q.includes(s)) {
          const xc = s;
          const idx = q.indexOf(xc);
          if (idx >= 0 && idx >= y) {
            const tc = xc.replace(/\bX\b/g, 'Twitter');
            q = q.substring(0, idx) + tc + q.substring(idx + xc.length);
            uv = m;
          }
        }
      }
      if (typeof uv === 'string' && !i18nCache.has(uv)) {
        if (i18nCache.size > 12) i18nCache.clear();
        i18nCache.add(uv);
        moveToFront(i18nXs, uv);
      }
    }
    return q;
  }



  function generateI18nXs() {
    let i18nXs = [];
    let i18nFunction = null;
    for (const s of window.webpackChunk_twitter_responsive_web) {
      if (s && s[0] && s[0][0]) {
        const tag = s[0][0]
        if (typeof tag === 'string' && tag.startsWith('i18n/')) {
          if (s[1] && typeof s[1] === 'object') {
            let entries = Object.entries(s[1]);
            if (entries.length === 1 && typeof entries[0][1] === 'function') {
              i18nFunction = entries[0][1];
            }
          }
          break;
        }
      }
    }

    let i18nFunctionString = i18nFunction + "";
    let mFuncs = [...i18nFunctionString.matchAll(/function\([,a-zA-Z0-9$_]*\)\{return([^\{\}]+\bX\b[^\{\}]+)\}/g)].map(c => c[1]);
    for (const mfString of mFuncs) {

      let rk1 = mfString.includes('"');
      let rk2 = mfString.includes("'");
      let rt1 = rk1 && !rk2 ? '"' : !rk1 && rk2 ? "'" : '';
      let rt2 = rk1 && !rk2 ? "'" : !rk1 && rk2 ? '"' : '';
      if (!rt1 || !rt2) continue;
      if (mfString.includes(rt2) || mfString.includes("\\") || mfString.includes("&#")) continue;
      const p = mfString.split(rt1)
      if ((p.length % 2) !== 1) continue;

      let uLen = 0;
      let jLen = 0;

      const qm = [];

      for (let i = 1; i < p.length; i += 2) {
        p[i] = p[i].length > 0 ? p[i].replace(/[\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]/g, ' ') : '';
        qm[i] = p[i].replace(/\bX\b/g, '_').replace(/\bTwitter\b/g, '_').length;
        uLen += qm[i];
      }

      for (let i = 1; i < p.length; i += 2) {
        if (/\bX\b/.test(p[i]) && !/\bTwitter\b/.test(p[i])) {

          i18nXs.push({
            s: p[i],
            y: jLen,
            l: uLen,
            m: mfString
          })
        }

        jLen += qm[i];
      }

    }

    return i18nXs;

  }

  function moveToFront(arr, value) {
    let insertAt = 0;

    for (let i = 0; i < arr.length;) {
      if (arr[i].m === value) {
        let start = i;
        do {
          i++;
        } while (i < arr.length && arr[i].m === value)
        let end = i;

        // Remove the segment from the array
        const matchedItems = arr.splice(start, end - start);

        // Insert the segment to the front at the offset
        arr.splice(insertAt, 0, ...matchedItems);

        insertAt += matchedItems.length;
      } else {
        i++;
      }
    }

    return arr;
  }

  const map = new Map();

  function fixTitle() {

    let p = document.title;
    if (!p) return;

    let q = map.get(p)
    if (q) {
      if (p !== q) {
        document.title = q;
      }
      return;
    }

    q = customTitle(p);

    if (map.size > 24 && p !== q) map.clear();
    map.set(p, q)

    if (p !== q) {
      map.set(q, q)
      document.title = q;
    }

  }


  function handleTitleChange(mutationsList) {
    console.log(document.title)

    let b = false;
    for (const mutation of mutationsList) {
      if (mutation.type === 'childList') {
        // Title has changed, do something here
        b = true;
        break;
      }
    }

    if (b) {
      // console.log('Title changed:', document.title);
      fixTitle();
    }
  }

  let observer = null;

  function doActionFn() {

    // Check if the title element has appeared
    const titleElement = document.querySelector('title');
    if (titleElement) {
      // Title element found, stop observing
      if (observer) observer.disconnect();

      // Now, create a new observer to monitor the title changes
      const titleObserver = new MutationObserver(handleTitleChange);

      // Configure the observer to monitor the title text changes
      const config = { childList: true, subtree: true };

      // Start observing the title element
      titleObserver.observe(titleElement, config);

      // Log the initial title
      // console.log('Initial Title:', titleElement.textContent);
      fixTitle()
    }

  }

  // Function to handle the title changes
  function mutCallback(mutationsList, observer) {
    let doAction = false;
    for (const mutation of mutationsList) {
      if (mutation.type === 'childList') {
        doAction = true;
        break;
      }
    }
    if (doAction) doActionFn();
  }

  if (document.querySelector('title')) {

    doActionFn();

  } else {


    // Create a new MutationObserver to monitor the document for the title element
    observer = new MutationObserver(mutCallback);

    // Configure the observer to monitor childList changes (new elements)
    const config = { childList: true, subtree: true };

    // Start observing the document
    observer.observe(document, config);

  }

})();