Replace Twitter/X Emojis with System Emojis

Replace all Twitter emoji images (Twemoji) on x.com with native system emojis.

  1. // ==UserScript==
  2. // @name Replace Twitter/X Emojis with System Emojis
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Replace all Twitter emoji images (Twemoji) on x.com with native system emojis.
  6. // @author Your Name (Updated)
  7. // @match https://x.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Selector for Twitter's emoji images (covers SVG and potentially PNG)
  16. const EMOJI_SELECTOR = 'img[src*="/emoji/v2/"][alt]'; // Added alt requirement to selector
  17.  
  18. /**
  19. * Replaces a given Twitter emoji IMG node with a SPAN containing the native system emoji.
  20. * @param {HTMLImageElement} imgNode The emoji image element to replace.
  21. */
  22. function replaceEmojiNode(imgNode) {
  23. // Check if already processed or lacks necessary attributes
  24. if (imgNode.dataset.emojiReplaced || !imgNode.parentNode || !imgNode.alt) {
  25. return;
  26. }
  27.  
  28. const alt = imgNode.alt; // The actual emoji character(s)
  29.  
  30. const span = document.createElement('span');
  31. span.textContent = alt; // Use the exact alt text (handles single/multiple emojis)
  32. span.setAttribute('role', 'img'); // Accessibility: Treat span like an image
  33. span.setAttribute('aria-label', alt); // Accessibility: Provide label
  34.  
  35. // --- Styling ---
  36. // 1. Basic inline behavior and alignment (adjust if needed)
  37. span.style.display = 'inline-block'; // Or 'inline' might work depending on context
  38. span.style.verticalAlign = 'text-bottom'; // Common alignment, adjust based on visual tests
  39.  
  40. // 2. Try to match the size of the original emoji image
  41. try {
  42. const computedStyle = window.getComputedStyle(imgNode);
  43. const height = computedStyle.height;
  44. // const width = computedStyle.width; // Width can be tricky with multiple chars, primarily use height
  45.  
  46. // Use height for font size and line height for better vertical centering and scaling
  47. if (height && height !== 'auto' && parseFloat(height) > 0) {
  48. span.style.fontSize = height;
  49. span.style.lineHeight = height; // Match line height to font size for vertical centering
  50. span.style.height = height; // Explicit height
  51. // Setting width can sometimes be problematic for multi-character emojis or variable-width system emojis.
  52. // Let the browser determine width based on content and font size unless specific issues arise.
  53. // span.style.width = 'auto'; // Allow natural width based on font/content
  54. } else {
  55. // Fallback size if computed style is unavailable or invalid
  56. span.style.fontSize = '1em'; // Inherit from parent or use a reasonable default
  57. span.style.lineHeight = '1'; // Normal line height
  58. }
  59. // Copy other potentially relevant styles (optional, can add complexity)
  60. // span.style.margin = computedStyle.margin;
  61.  
  62. } catch (e) {
  63. console.warn("Replace Emojis Script: Couldn't get computed style for emoji, using default size.", e);
  64. // Apply fallback size on error
  65. span.style.fontSize = '1em';
  66. span.style.lineHeight = '1';
  67. }
  68.  
  69. // Mark the original node as processed BEFORE replacing it
  70. imgNode.dataset.emojiReplaced = 'true';
  71. imgNode.style.display = 'none'; // Hide original immediately to prevent flash
  72.  
  73. // Replace the image node with the new span node
  74. imgNode.parentNode.replaceChild(span, imgNode);
  75. // console.log(`Replaced emoji: ${alt}`); // For debugging
  76. }
  77.  
  78. /**
  79. * Scans a given node and its descendants for emoji images and replaces them.
  80. * @param {Node} targetNode The node to scan.
  81. */
  82. function processNode(targetNode) {
  83. if (!targetNode || !targetNode.querySelectorAll) return;
  84.  
  85. // Find all emoji images within the target node that haven't been replaced
  86. const emojiImages = targetNode.querySelectorAll(EMOJI_SELECTOR + ':not([data-emoji-replaced="true"])');
  87. emojiImages.forEach(replaceEmojiNode);
  88. }
  89.  
  90. // --- Mutation Observer ---
  91. // Observe DOM changes to catch dynamically loaded content
  92. const observer = new MutationObserver(mutations => {
  93. mutations.forEach(mutation => {
  94. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  95. mutation.addedNodes.forEach(node => {
  96. // Check if the added node itself is an emoji
  97. if (node.nodeType === Node.ELEMENT_NODE && node.matches(EMOJI_SELECTOR)) {
  98. replaceEmojiNode(node);
  99. }
  100. // Check if the added node contains emojis
  101. else if (node.nodeType === Node.ELEMENT_NODE && node.querySelector) {
  102. processNode(node);
  103. }
  104. });
  105. }
  106. // Optional: Handle attribute changes if emoji src/alt might change later
  107. // else if (mutation.type === 'attributes' && mutation.attributeName === 'src' && mutation.target.matches(EMOJI_SELECTOR)) {
  108. // // Re-evaluate if needed, but usually replacement is permanent
  109. // // Be careful not to cause infinite loops if you modify attributes observer listens to
  110. // }
  111. });
  112. });
  113.  
  114. // Start observing the document body for additions and subtree modifications
  115. observer.observe(document.body, {
  116. childList: true,
  117. subtree: true
  118. // attributes: true, // Uncomment cautiously if needed
  119. // attributeFilter: ['src', 'alt'] // Specify attributes if uncommenting 'attributes'
  120. });
  121.  
  122. // --- Initial Scan ---
  123. // Process existing emojis present on the page when the script initially runs
  124. console.log("Replace Emojis Script: Running initial scan...");
  125. processNode(document.body);
  126. console.log("Replace Emojis Script: Initial scan complete. Observer active.");
  127.  
  128. })();