Twitter X Icon

Change Twitter X Icon

Från och med 2023-07-30. Se den senaste versionen.

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 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 X Icon
// @namespace   TwitterX
// @match       https://twitter.com/*
// @grant       none
// @version     0.1.0
// @author      CY Fung
// @description Change Twitter X Icon
// @run-at      document-start
// @license MIT
// ==/UserScript==


(() => {

  let mIconUrl = '';
  let linkCache = new Map();

  let waa = new WeakSet();

  let mDotUrlMap = new Map();

  const op = {
    radius: (canvas)=>Math.round(canvas.width * 0.14),

    x: (canvas, radius)=> canvas.width - radius * 2 + radius*0.05,

    y: (canvas, radius)=>0 + radius * 2 - radius*0.3,

  };

  function addRedDotToImage(dataUriBase64, op) {
    return new Promise((resolve, reject) => {
      // Create an image element to load the data URI
      const image = new Image();
      image.onload = () => {
        const canvas = document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;

        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0);

        const radius = op.radius(canvas);
        const dotX =  op.x(canvas, radius);
        const dotY =  op.y(canvas, radius);

        // Draw a red dot on the top right corner
        ctx.beginPath();
        ctx.arc(dotX, dotY, radius, 0, 2 * Math.PI);
        ctx.fillStyle = 'red';
        ctx.fill();

        // Convert the canvas back to a data URI base64 string
        const revisedDataUriBase64 = canvas.toDataURL();
        resolve(revisedDataUriBase64);
      };

      // Set the image source to the provided data URI
      image.src = dataUriBase64;
    });
  }


  function myLink(link, dottable) {

    if (waa.has(link)) return;
    waa.add(link);


    let hrefDtor = Object.getOwnPropertyDescriptor(link.constructor.prototype, 'href');

    if (!hrefDtor.set || !hrefDtor.get) {
      return;
    }

    const getHref = () => {
      return hrefDtor.get.call(link)
    }

    let qq = null;


    async function updateURL(hh) {


      console.log('old href', hh, link.getAttribute('has-dot') === 'true')

      let nurl = mIconUrl;

      if(nurl && hh){

          let href = hh;
          let isDotted = link.getAttribute('has-dot') === 'true'

          if (isDotted && !nurl.startsWith('http') ) {
            nurl = await addRedDotToImage(nurl, op);
          }

      }

      if (hh !== nurl && nurl) link.href = nurl;



    }

    function ckk() {
      const hh = getHref();
      if (qq === hh) return;
      qq = hh;
      updateURL(hh);
    }


    function updateDotState(hh2) {

      if (hh2 && typeof hh2 =='string' &&  hh2.startsWith('http')) {
        let href = hh2;
        let isDotted = false;

        if (mDotUrlMap.has(href)) isDotted = mDotUrlMap.get(href);
        else {

          if (href.endsWith('/twitter-pip.3.ico')) isDotted = true;
          else {

            let q = /\?[^?.:\/\\]+/.exec(href);
            q = q ? q[0] : '';

            if (q) {
              isDotted = true;
            }

          }

          mDotUrlMap.set(href, isDotted);


        }


        link.setAttribute('has-dot', isDotted ? 'true' : 'false')
      }

      Promise.resolve().then(ckk)



    }

    let hh2 = null;

    hh2 = getHref();
    updateDotState(hh2);

    Object.defineProperty(link, 'href', {
      get() {
        return hh2;
      },
      set(a) {
        if (!a || a.startsWith('http')) {
          hh2 = a;
          updateDotState(hh2);
        }
        return hrefDtor.set.call(this, a);
      }

    });



    document.addEventListener('my-twitter-icon-has-changed',(evt)=>{

      if(!evt) return;
      let detail = evt.detail;

      if(!detail)return;
      let mIconUrl = detail.mIconUrl;
      if(!mIconUrl) return;


      link.href = mIconUrl;
      console.log('icon changed')

      Promise.resolve().then(ckk);



    },true);

  }

  function mIconFn(iconUrl, rel, dottable) {



    const selector = `link[rel~="${rel}"]`;
    let link = document.querySelector(selector);
    if (!link) {

      /** @type {HTMLLinkElement} */
      link = document.createElement("link");
      link.rel = `${rel}`;
      link.href = iconUrl;
      document.head.appendChild(link);
    }

    for (const link of document.querySelectorAll(selector)) {
      if(waa.has(link))continue;
        myLink(link, dottable);
    }


  }

  function replacePageIcon(iconUrl) {
    mIconFn(iconUrl, 'icon', 1)
  }

  function replaceAppIcon(iconUrl) {

    mIconFn(iconUrl, 'apple-touch-icon', 0);
  }


  const addCSS = (href) => {
    let p = document.querySelector('style#j8d4f');
    if (!p) {
      p = document.createElement('style');
      p.id = 'j8d4f';
      document.head.appendChild(p);
    }

    let newTextContent = `
     a[href="/home"][aria-label="Twitter"] * {
     pointer-events: none;
     }
        a[href="/home"][aria-label="Twitter"] > div::before {

            background-image: url("${href}");

            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            content: '';
            color: #fff;
            display: block;
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            border-radius: 50% / 50%;
        }
        a[href="/home"] svg::before {
            display: block;
            position: absolute;
            content: "";
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
        }


        a[href="/home"] svg path {
            visibility: collapse;
        }

        `;
    newTextContent = newTextContent.trim();

    if (p.textContent !== newTextContent) p.textContent = newTextContent;
  }

  let qdd = 0;

  function sendMessageIconChanged (mIconUrl){
    document.dispatchEvent(new CustomEvent('my-twitter-icon-has-changed', {detail: { mIconUrl }}));
  }

  function changeIconFn(withPageElement) {
    mIconUrl = localStorage.getItem('myCustomTwitterIcon');
    if (!mIconUrl) return;

    let tid = qdd = Date.now();

      if (tid !== qdd) return;

      addCSS(mIconUrl);
      replacePageIcon(mIconUrl);
      replaceAppIcon(mIconUrl);

    sendMessageIconChanged(mIconUrl)


  }


  function onImageLoaded(dataURL) {


    // Save the data URL to localStorage with a specific key
    localStorage.setItem('myCustomTwitterIcon', dataURL);
    console.log('myCustomTwitterIcon - done');
    changeIconFn(1);



  }


  // Function to handle the image drop event
  function handleDrop(event) {
    if (!event) return;

    if (!(event.target instanceof HTMLElement)) return;

    event.preventDefault();
    // Check if the target element is the desired anchor with href="/home"
    const targetElement = event.target.closest('a[href="/home"][aria-label="Twitter"]');
    if (!targetElement) return;

    // Get the dropped file (assuming only one file is dropped)
    const file = event.dataTransfer.files[0];

    // Check if the dropped file is an image
    if (!file || !file.type.startsWith('image/')) return;

    linkCache.clear();

    // Read the image file and convert to base64 data URL
    let reader = new FileReader();
    reader.onload = function () {
      Promise.resolve(reader.result).then(onImageLoaded);
      reader = null;
    };
    reader.readAsDataURL(file);
  }

  // Function to handle the dragover event and allow dropping
  function handleDragOver(event) {
    event.preventDefault();
  }


  if (localStorage.getItem('myCustomTwitterIcon')) {

    changeIconFn(0);
  }

  let observer = null;

  // Function to check if the target element is available and hook the drag and drop functionality
  function hookDragAndDrop() {
    const targetElement = document.querySelector('a[href="/home"]');
    if (targetElement && observer) {
      targetElement.addEventListener('dragover', handleDragOver);
      targetElement.addEventListener('drop', handleDrop);
      console.log('Drag and drop functionality hooked.');



      observer.takeRecords();
      // Stop and disconnect the observer since the targetElement is found
      observer.disconnect();
      observer = null;

      if (localStorage.getItem('myCustomTwitterIcon')) {

        changeIconFn(1);
      }


    }
  }

  // Use MutationObserver to observe changes in the document
  observer = new MutationObserver(function (mutationsList, observer) {
    let p = false;
    for (const mutation of mutationsList) {
      if (mutation.type === 'childList' || mutation.type === 'subtree') {
        p = true;

      }
    }
    if (p) hookDragAndDrop();
  });

  // Start observing the entire document
  observer.observe(document, { childList: true, subtree: true });




})();