Twitter/X Emoji Fix

Replace Twitter web's image emoji with native emoji for easier tweet content copying

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitter/X Emoji Fix
// @icon         https://twitter.com/favicon.ico
// @namespace    https://github.com/1zero224
// @version      1.0
// @description  Replace Twitter web's image emoji with native emoji for easier tweet content copying
// @description:zh-CN  将 Twitter 网页端的图片 emoji 替换为原生 emoji,方便复制推文内容
// @author       1zero
// @include      https://x.com/*
// @include      https://twitter.com/*
// @include      https://mobile.twitter.com/*
// @license      MIT
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    /**
     * 从 Twitter emoji SVG URL 中提取 Unicode 码点并转换为 emoji 字符
     * @param {string} src - SVG 图片的 URL
     * @returns {string|null} - 转换后的 emoji 字符,失败返回 null
     */
    function extractEmojiFromUrl(src) {
        // 匹配类似 "/emoji/v2/svg/1f3ac.svg" 的 URL
        const match = src.match(/\/emoji\/v2\/svg\/([0-9a-f-]+)\.svg/i);
        if (!match) return null;

        const codePoints = match[1].split('-');
        const emojiChar = String.fromCodePoint(
            ...codePoints.map(cp => parseInt(cp, 16))
        );
        return emojiChar;
    }

    /**
     * 替换单个 emoji 图片元素
     * @param {HTMLImageElement} img - emoji 图片元素
     */
    function replaceEmojiImage(img) {
        // 检查是否已经处理过
        if (img.dataset.emojiReplaced) return;

        const src = img.src || img.getAttribute('src');
        if (!src) return;

        const emoji = extractEmojiFromUrl(src);
        if (!emoji) return;

        // 创建 span 元素替代 img
        const span = document.createElement('span');
        span.textContent = emoji;
        span.className = img.className;
        span.style.fontSize = 'inherit';
        span.style.lineHeight = 'inherit';
        span.style.verticalAlign = 'baseline';
        span.title = img.title || img.alt;
        span.dataset.originalEmoji = emoji;

        // 替换元素
        img.replaceWith(span);
    }

    /**
     * 扫描并替换页面中所有的 emoji 图片
     */
    function replaceAllEmojis() {
        // 查找所有 Twitter emoji 图片
        const emojiImages = document.querySelectorAll('img[src*="/emoji/v2/svg/"]');
        emojiImages.forEach(img => replaceEmojiImage(img));
    }

    /**
     * 使用 MutationObserver 监听 DOM 变化
     * Twitter 使用 SPA 架构,内容动态加载
     */
    function observeDOMChanges() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // 检查新增节点本身
                        if (node.tagName === 'IMG' && node.src && node.src.includes('/emoji/v2/svg/')) {
                            replaceEmojiImage(node);
                        }
                        // 检查新增节点内的 emoji 图片
                        const imgs = node.querySelectorAll ? node.querySelectorAll('img[src*="/emoji/v2/svg/"]') : [];
                        imgs.forEach(img => replaceEmojiImage(img));
                    }
                });
            });
        });

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

        return observer;
    }

    // 初始化:替换当前页面的 emoji
    replaceAllEmojis();

    // 启动监听器
    observeDOMChanges();

    // 使用防抖处理滚动和其他事件
    let debounceTimer;
    function debouncedReplace() {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(replaceAllEmojis, 500);
    }

    // 监听滚动事件(懒加载内容)
    window.addEventListener('scroll', debouncedReplace, { passive: true });
})();