Last active
June 26, 2024 15:58
-
-
Save philippgitpush/c955a9b51fa4870208e233212519295f to your computer and use it in GitHub Desktop.
Userscript that adds a button on X (Formerly Twitter) that allows you to copy a tweet to the obsidian embed format (user plugin)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name X Embed Button | |
// @version 1.0.1 | |
// @description Userscript that adds a button on X (Formerly Twitter) that allows you to copy a tweet to the obsidian embed format (user plugin) | |
// @author philippgitpush | |
// @match https://x.com/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
function createEmbedButton(tweetElement) { | |
// Check if button already exists | |
if (tweetElement.querySelector('.embed-button')) { | |
return; | |
} | |
// Create the button | |
const button = document.createElement('button'); | |
button.className = 'embed-button'; | |
button.style.marginLeft = '8px'; // Modified margin-left | |
button.style.border = 'none'; // Removed border | |
button.style.background = 'none'; // Removed background | |
button.style.cursor = 'pointer'; // Kept cursor pointer | |
button.style.display = 'flex'; // Added display flex | |
button.style.padding = '0'; // Removed padding | |
button.style.justifyContent = 'center'; // Added justify-content center | |
button.style.alignContent = 'center'; // Added align-content center | |
button.style.flexDirection = 'column'; // Added flex-direction column | |
// Create the SVG icon | |
const svgIcon = ` | |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#536471" viewBox="0 0 256 256"> | |
<path d="M88,96a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,96Zm8,40h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16Zm32,16H96a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16ZM224,48V156.69A15.86,15.86,0,0,1,219.31,168L168,219.31A15.86,15.86,0,0,1,156.69,224H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48ZM48,208H152V160a8,8,0,0,1,8-8h48V48H48Zm120-40v28.7L196.69,168Z"></path> | |
</svg> | |
`; | |
button.innerHTML = svgIcon; | |
// Add hover effect | |
button.addEventListener('mouseenter', () => { | |
button.querySelector('path').setAttribute('fill', '#f62681'); | |
}); | |
button.addEventListener('mouseleave', () => { | |
button.querySelector('path').setAttribute('fill', '#536471'); | |
}); | |
// Append button to the tweet's action area | |
const actionArea = tweetElement.querySelector('[role="group"]'); | |
if (actionArea) { | |
actionArea.appendChild(button); | |
} | |
// Add event listener to the button | |
button.addEventListener('click', () => { | |
const displayName = tweetElement.querySelector('[data-testid="User-Name"] span').innerText; | |
const usernameElement = tweetElement.querySelector('a[href*="/status/"]'); | |
const username = usernameElement ? usernameElement.href.split('/')[3] : ''; | |
let tweetTextElement = tweetElement.querySelector('[data-testid="tweetText"]'); | |
let tweetText = tweetTextElement ? tweetTextElement.innerText.replace(/\n/g, ' ') : ''; // Replace newlines with spaces | |
tweetText = escapeSpecialCharacters(tweetText); // Escape special characters in tweetText | |
const tweetUrl = tweetElement.querySelector('time').parentElement.href; | |
let imageUrl = ''; | |
const imageElement = tweetElement.querySelector('[data-testid="tweetPhoto"] img'); | |
if (imageElement) { | |
imageUrl = getImageUrl(imageElement.src); | |
} else { | |
const videoElement = tweetElement.querySelector('video[poster]'); | |
if (videoElement) { | |
imageUrl = getImageUrl(videoElement.getAttribute('poster')); | |
} | |
} | |
const embedCode = ` | |
\`\`\`embed | |
title: "${escapeSpecialCharacters(displayName)} (@${username}) on X" | |
image: "${imageUrl}" | |
${tweetText ? `description: "${tweetText}"` : ''} | |
url: "${tweetUrl}" | |
\`\`\` | |
`; | |
navigator.clipboard.writeText(embedCode).then(() => { | |
showToast('Copied to clipboard'); | |
}); | |
}); | |
} | |
// Function to handle added nodes | |
function handleAddedNodes(nodes) { | |
nodes.forEach(node => { | |
if (node.nodeType === 1 && node.matches('[data-testid="tweet"]')) { | |
createEmbedButton(node); | |
} | |
// Handle newly added tweets in nested structures | |
const tweets = node.querySelectorAll ? node.querySelectorAll('[data-testid="tweet"]') : []; | |
tweets.forEach(tweet => { | |
createEmbedButton(tweet); | |
}); | |
}); | |
} | |
// Function to escape special characters | |
function escapeSpecialCharacters(text) { | |
return text.replace(/["]/g, '\\"'); | |
} | |
// Function to get the correct image URL | |
function getImageUrl(fullUrl) { | |
// Remove any query parameters and ensure '.jpg:large' suffix | |
const baseImageUrl = fullUrl.split(/[?#]/)[0]; // Split by '?', '#' to remove query parameters | |
return baseImageUrl.endsWith('.jpg') ? baseImageUrl.replace('.jpg', '') + '.jpg:large' : baseImageUrl.replace(/\.[^/.]+$/, '') + '.jpg:large'; | |
} | |
// Function to show toast message | |
function showToast(message) { | |
const toast = document.createElement('div'); | |
toast.className = 'css-175oi2r r-aqfbo4 r-zchlnj r-1d2f490 r-1xcajam r-1p0dtai r-12vffkv'; | |
toast.innerHTML = ` | |
<div class="css-175oi2r r-12vffkv"> | |
<div class="css-175oi2r r-12vffkv"> | |
<div class="css-175oi2r r-633pao r-f8sm7e r-13qz1uu r-1ye8kvj"> | |
<div role="alert" class="css-175oi2r r-1awozwy r-l5o3uw r-18u37iz r-1wtj0ep r-xyw6el r-105ug2t r-yz1j6i r-1kihuf0 r-z2wwpe r-zd98yo" style="transition-property: opacity; transition-duration: 170ms; transition-timing-function: cubic-bezier(0, 0, 1, 1); opacity: 1;" data-testid="toast"> | |
<div dir="ltr" class="css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-16dba41 r-1wbh5a2 r-3o4zer" style="white-space: unset;text-overflow: unset; color: rgb(255, 255, 255);"> | |
<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3" style="text-overflow: unset;">${message}</span> | |
</div> | |
<div aria-hidden="true" class="css-175oi2r r-18u37iz"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
document.body.appendChild(toast); | |
// Remove the toast after 3 seconds | |
setTimeout(() => { | |
toast.style.opacity = '0'; | |
setTimeout(() => { | |
toast.remove(); | |
}, 170); // Wait for the transition to finish | |
}, 3000); | |
} | |
// Set up the Mutation Observer | |
const observer = new MutationObserver(mutations => { | |
mutations.forEach(mutation => { | |
handleAddedNodes(mutation.addedNodes); | |
}); | |
}); | |
// Start observing the document body for added nodes | |
observer.observe(document.body, { childList: true, subtree: true }); | |
// Initial run to add buttons to existing tweets | |
const initialTweets = document.querySelectorAll('[data-testid="tweet"]'); | |
initialTweets.forEach(tweet => { | |
createEmbedButton(tweet); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment