NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Fix Twitter links // @description Fix Twitter links from your timeline and say goodbye to its URL shortener! // @author Lucio Martinez // @license MIT // @copyright 2014-2020, Lucio Martinez (https://openuserjs.org/users/lucio-martinez) // @downloadURL https://openuserjs.org/install/lucio-martinez/Fix_Twitter_links.user.js // @updateURL https://openuserjs.org/meta/lucio-martinez/Fix_Twitter_links.meta.js // @supportURL https://github.com/luciomartinez/fix-twitter-links/issues // @namespace https://github.com/luciomartinez/fix-twitter-links // @version 0.5 // @grant none // @match https://twitter.com/* // @match https://tweetdeck.twitter.com/* // ==/UserScript== fixLinks(); function fixLinks() { const domain = window.location.hostname; if (domain === 'tweetdeck.twitter.com') { fixLinksInTweetDeck(); } else { fixLinksInTwitter(); } } function fixLinksInTwitter() { startWatchWithCleaner(parseLinksAtTwitter); } function fixLinksInTweetDeck() { startWatchWithCleaner(parseLinksAtTweetDeck); } function startWatchWithCleaner(cleaner) { startWatch(cleaner); } function startWatch(cleaner) { const watchedNode = document.getElementById('react-root'); watchForChangesOnNodeAndRunCleaner(watchedNode, cleaner); } function watchForChangesOnNodeAndRunCleaner(targetNode, cleaner) { const observer = createObserverWithCallbackBounced(cleaner); observer.observe(targetNode, { subtree: true, childList: true }); } function createObserverWithCallbackBounced(callback) { const callbackDebounced = debounce(callback); return new MutationObserver(callbackDebounced); } /** * Thanks to Chris Boakes! * https://chrisboakes.com/how-a-javascript-debounce-function-works/ * @param {function} callback - Function to be debounced * @param {number} [wait=250] - Specifies the milliseconds to debounce * @returns {function} Debounced function */ function debounce(callback, wait = 250) { let timeout; return (...args) => { const context = this; clearTimeout(timeout); timeout = setTimeout(() => callback.apply(context, args), wait); }; } function parseLinksAtTwitter() { const selectShortLinks = 'a[href^="https://t.co/"][title]:not([data-link-fixed])'; const links = document.querySelectorAll(selectShortLinks); links.forEach((link) => { updateLinkAndMarkAsFixed(link, link.title); }); } function parseLinksAtTweetDeck() { const selectFromTweets = '.tweet-text > .url-ext[href^="https://t.co/"][data-full-url]:not([data-link-fixed], .is-vishidden)'; const selectFromQuotedTweets = '.quoted-tweet .url-ext[href^="https://t.co/"][data-full-url]:not([data-link-fixed])'; const selectFromTweetDetail = '.tweet-detail .url-ext[href^="https://t.co/"][data-full-url]:not([data-link-fixed])'; const multipleSelects = `${selectFromTweets}, ${selectFromQuotedTweets}, ${selectFromTweetDetail}`; const links = document.querySelectorAll(multipleSelects); links.forEach((link) => { updateLinkAndMarkAsFixed(link, link.dataset.fullUrl); }); } function updateLinkAndMarkAsFixed(anchorEl, fixedUrl) { console.info('Fix Twitter links: URL parsed', fixedUrl); setHrefTo(anchorEl, fixedUrl); markAsFixed(anchorEl); } function setHrefTo(el, url) { el.href = url; } function markAsFixed(el) { el.dataset.linkFixed = '1'; }