// ==UserScript==
// @name X Tweet Copy
// @namespace https://github.com/tizee-tampermonkey-scripts/tampermonkey-tweet-copy
// @version 1.2.2
// @description Adds a "Copy" button to each tweet that copies the tweet text along with its URL and shows a check mark animation upon success, preserving link URLs and styling.
// @author tizee
// @downloadURL https://raw.githubusercontent.com/tizee-tampermonkey-scripts/tampermonkey-tweet-copy/refs/heads/main/user.js
// @updateURL https://raw.githubusercontent.com/tizee-tampermonkey-scripts/tampermonkey-tweet-copy/refs/heads/main/user.js
// @match https://x.com/*
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Define SVG constants for the copy icon and the check mark.
const ORIGINAL_SVG = ``;
const CHECKMARK_SVG = ``;
// Inject CSS styles for the copy button and check mark animation using GM_addStyle.
GM_addStyle(`
.tm-copy-button {
cursor: pointer;
color: rgb(113, 118, 123);
font-size: 14px;
background: transparent;
border: none;
padding: 4px;
margin-left: 8px;
}
.tm-copy-button svg {
fill: currentcolor;
width: 1.5em;
height: 1.5em;
transition: transform 0.3s ease;
}
.tm-copy-button:hover svg {
color: rgb(29, 155, 240);
}
/* Animation for the check mark */
.tm-copy-checkmark {
animation: checkmark-pop 0.5s ease-in-out;
}
@keyframes checkmark-pop {
0% { transform: scale(0.8); opacity: 0.5; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
`);
/**
* Finds the tweet container (article element with data-testid="tweet")
* for the given element.
* @param {HTMLElement} element - The element to search from.
* @returns {HTMLElement|null} The tweet container or null if not found.
*/
function findTweetContainer(element) {
return element.closest('article[data-testid="tweet"]');
}
/**
* Creates and appends a copy button to the button group element.
* The button, when clicked, copies the tweet content with styling and tweet URL.
* @param {HTMLElement} groupEl - The container for tweet action buttons.
*/
function addCopyButtonToGroup(groupEl) {
if (!groupEl) return;
// Retrieve the tweet container for this group.
const tweetContainer = findTweetContainer(groupEl);
if (!tweetContainer) return;
// Avoid adding duplicate copy buttons.
if (groupEl.querySelector('.tm-copy-button')) return;
// Create the copy button.
const copyBtn = document.createElement('button');
copyBtn.className = 'tm-copy-button';
copyBtn.innerHTML = ORIGINAL_SVG;
copyBtn.addEventListener('click', (e) => {
// Prevent event propagation.
e.stopPropagation();
// Extract tweet text elements.
const textElements = tweetContainer.querySelectorAll('[data-testid="tweetText"]');
// Process each tweet text element to preserve styling and update links.
const tweetContent = Array.from(textElements).map(el => {
const clone = el.cloneNode(true);
// Replace each anchor's visible text with its full URL.
clone.querySelectorAll('a').forEach(a => {
if (a.href) {
a.textContent = a.href;
}
});
return {
html: clone.innerHTML,
text: clone.innerText
};
});
// Combine processed content with line breaks.
const tweetHTML = tweetContent.map(obj => obj.html).join('
');
const tweetPlainText = tweetContent.map(obj => obj.text).join('\n\n');
// Retrieve the tweet URL.
let tweetUrl = '';
const linkEl = tweetContainer.querySelector('a[href*="/status/"]');
if (linkEl && linkEl.href) {
tweetUrl = linkEl.href;
}
// Append tweet URL to the content and wrap in markdown backticks.
const copyHTML = `
${tweetHTML}