// ==UserScript==
// @name Twitter X Icon
// @namespace TwitterX
// @match https://twitter.com/*
// @grant none
// @version 0.1.5
// @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: (canvasSize) => Math.round(canvasSize.width * 0.14),
x: (canvasSize, radius) => canvasSize.width - radius * 2 + radius * 0.05,
y: (canvasSize, 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 { width, height } = image;
const canvasSize = {
width, height
}
const radius = op.radius(canvasSize);
const dotX = op.x(canvasSize, radius);
const dotY = op.y(canvasSize, radius);
// Convert the canvas back to a data URI base64 string
let revisedDataUriBase64;
if (dataUriBase64.startsWith('data:image/svg+xml')) {
// For SVG, create a new SVG element and add the circle element
const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgElement.setAttribute('width', width);
svgElement.setAttribute('height', height);
// Create a new image element within the SVG
const svgImageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
svgImageElement.setAttribute('width', width);
svgImageElement.setAttribute('height', height);
svgImageElement.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', dataUriBase64);
svgElement.appendChild(svgImageElement);
// Create a red dot circle element
const circleElement = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circleElement.setAttribute('cx', dotX);
circleElement.setAttribute('cy', dotY);
circleElement.setAttribute('r', radius);
circleElement.setAttribute('fill', 'red');
svgElement.appendChild(circleElement);
if (typeof btoa !== 'function') return reject();
try {
// Convert the modified SVG element back to a data URI base64 string
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgElement);
revisedDataUriBase64 = 'data:image/svg+xml;base64,' + btoa(svgString);
} catch (e) { }
} else {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
// 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();
try {
revisedDataUriBase64 = canvas.toDataURL();
} catch (e) { }
}
if (!revisedDataUriBase64) {
return reject();
}
// 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')) {
console.log('dotting')
nurl = await addRedDotToImage(nurl, op);
console.log('dotted', !!nurl)
}
}
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][my-custom-icon] > div::before {
background-image: url("${href}");
--my-custom-icon-padding: 6px;
position: absolute;
left: var(--my-custom-icon-padding);
right: var(--my-custom-icon-padding);
top: var(--my-custom-icon-padding);
bottom: var(--my-custom-icon-padding);
content: '';
color: #fff;
display: block;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 44% / 44%;
}
a[href][my-custom-icon] svg::before {
display: block;
position: absolute;
content: "";
left: 0;
right: 0;
top: 0;
bottom: 0;
}
a[href][my-custom-icon] 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][my-custom-icon]');
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"][aria-label="Twitter"]');
if (targetElement && observer) {
targetElement.setAttribute('my-custom-icon', '');
targetElement.addEventListener('dragover', handleDragOver);
targetElement.addEventListener('drop', handleDrop);
console.log('Drag and drop functionality hooked.');
document.head.appendChild(document.createElement('style')).textContent = `
a[href="/home"][aria-label="Twitter"][my-custom-icon] * {
pointer-events: none;
}
`;
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 });
})();