One Click Copy Link Button for Twitter(X)

Add a button to copy the URL of a tweet on Twitter without clicking dropdown. Default to vxtwitter but customizable.

  1. // ==UserScript==
  2. // @name One Click Copy Link Button for Twitter(X)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3
  5. // @description Add a button to copy the URL of a tweet on Twitter without clicking dropdown. Default to vxtwitter but customizable.
  6. // @author Dinomcworld
  7. // @match https://twitter.com/*
  8. // @match https://mobile.twitter.com/*
  9. // @match https://tweetdeck.twitter.com/*
  10. // @match https://x.com/*
  11. // @icon https://www.google.com/s2/favicons?domain=twitter.com
  12. // @grant none
  13. // @license MIT
  14.  
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. const baseUrl = 'https://vxtwitter.com';
  21.  
  22. const defaultSVG = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" viewBox="0 0 24 24" stroke-width="2" stroke="#71767C" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" /><path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" /></svg>';
  23. const copiedSVG = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard-check" viewBox="0 0 24 24" stroke-width="2" stroke="#00abfb" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" /><path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" /><path d="M9 14l2 2l4 -4" /></svg>';
  24.  
  25. function addCopyButtonToTweets() {
  26. const tweets = document.querySelectorAll('button[data-testid="bookmark"], button[data-testid="removeBookmark"]');
  27.  
  28. tweets.forEach(likeButton => {
  29. const parentDiv = likeButton.parentElement;
  30. const tweet = parentDiv.closest('article[data-testid="tweet"]');
  31. if (tweet && !tweet.querySelector('.custom-copy-icon')) {
  32. const copyIcon = document.createElement('div');
  33. copyIcon.classList.add('custom-copy-icon');
  34. copyIcon.setAttribute('aria-label', 'Copy link');
  35. copyIcon.setAttribute('role', 'button');
  36. copyIcon.setAttribute('tabindex', '0');
  37. copyIcon.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 19px; height: 19px; border-radius: 9999px; transition-duration: 0.2s; cursor: pointer;';
  38. copyIcon.innerHTML = defaultSVG;
  39.  
  40. copyIcon.addEventListener('click', (event) => {
  41. event.stopPropagation();
  42. const tweetUrl = extractTweetUrl(tweet);
  43. if (tweetUrl) {
  44. navigator.clipboard.writeText(tweetUrl)
  45. .then(() => {
  46. console.log('Tweet link copied!');
  47. copyIcon.innerHTML = copiedSVG;
  48. })
  49. .catch(err => console.error('Error copying link: ', err));
  50. }
  51. });
  52.  
  53. const parentDivClone = parentDiv.cloneNode(true);
  54. parentDivClone.style.cssText = 'display: flex; align-items: center;';
  55. parentDiv.parentNode.insertBefore(parentDivClone, parentDiv.nextSibling);
  56. parentDivClone.innerHTML = '';
  57. parentDivClone.appendChild(copyIcon);
  58. }
  59. });
  60. }
  61.  
  62.  
  63. function extractTweetUrl(tweetElement) {
  64. // Look for the link that contains a time element
  65. const linkElement = tweetElement.querySelector('a[href*="/status/"] > time');
  66.  
  67. if (linkElement) {
  68. let url = linkElement.parentElement.getAttribute('href');
  69. // Ensure the URL starts with a forward slash
  70. if (!url.startsWith('/')) {
  71. url = '/' + url;
  72. }
  73. return `${baseUrl}${url}`;
  74. }
  75.  
  76. // Fallback to looking for any link with "/status/" in case the structure changes
  77. const fallbackLink = tweetElement.querySelector('a[href*="/status/"]');
  78. if (fallbackLink) {
  79. let url = fallbackLink.getAttribute('href');
  80. // Ensure the URL starts with a forward slash
  81. if (!url.startsWith('/')) {
  82. url = '/' + url;
  83. }
  84. return `${baseUrl}${url}`;
  85. }
  86.  
  87. return null; // If we can't find a valid URL
  88. }
  89.  
  90. const observer = new MutationObserver(addCopyButtonToTweets);
  91. observer.observe(document.body, { childList: true, subtree: true });
  92.  
  93. addCopyButtonToTweets();
  94. })();