Changes the logo, tab name, and naming of "tweets" on Twitter
// ==UserScript== // @name Twitter: bring back old name and logo // @name:de Twitter: alten Namen und Logo zurückbringen // @name:nl Twitter: oude naam en logo terugbrengen // @name:es Twitter: recupera el nombre y el logotipo antiguos // @namespace https://github.com/rybak // @version 30.12 // @description Changes the logo, tab name, and naming of "tweets" on Twitter // @description:de Ändert das Logo, den Tab-Namen und die Benennung von „Tweets“ auf Twitter // @description:nl Wijzigt het logo, de tabbladnaam en de naamgeving van "tweets" op Twitter // @description:es Cambia el logo, nombre de la pestaña y denominación de los "tweets" en Twitter // @author Andrei Rybak // @license MIT // @match https://twitter.com/* // @match https://x.com/* // @icon https://abs.twimg.com/favicons/twitter.2.ico // @grant GM_addStyle // @run-at document-body // ==/UserScript== /* * Copyright (c) 2023-2024 Andrei Rybak * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * Things which surprisingly don't need replacing/renaming as of 2023-08-26: * * 1. "Scheduled Tweets" are still called "Tweets" * 2. "Based on your Retweets" in the "For you" tab. (not sure, needs rechecking) * * Things deliberately left with the new name: * * 1. "Post" in "Post Analytics" -- a rarely used feature, don't care. * 2. "X Corp." in the copyright line of the "footer" (it's in the right sidebar on the web version) * 3. Anything on subdomains: about.twitter.com, developer.twitter.com, etc. * 4. Tweets counters in "What's happening". It's algorithmic trash, hide it with https://userstyles.world/style/10864/twitter-hide-trends-and-who-to-follow */ (function() { 'use strict'; /* * This is needed to replace the very first "X" in <title>, because * proper renaming and the MutationObserver for <title> are going * to be too late to fix it. */ document.title = "Twitter"; const TWITTER_2012_ICON_URL = 'https://abs.twimg.com/favicons/twitter.2.ico'; const LOG_PREFIX = "[old school Twitter]"; const FAVICON_SELECTOR = 'link[rel="icon"], link[rel="shortcut icon"]'; const DIALOG_TWEET_BUTTON_SELECTOR = 'button[data-testid="tweetButton"] > div > span > span'; const RETWEETED_SELECTOR = '[data-testid="socialContext"]'; const SHOW_N_TWEETS_SELECTOR = 'main div div section > div > div > div > div button[role="button"] > div > div > span'; const DEBUG = false; function error(...toLog) { console.error(LOG_PREFIX, ...toLog); } function warn(...toLog) { console.warn(LOG_PREFIX, ...toLog); } function info(...toLog) { console.info(LOG_PREFIX, ...toLog); } function debug(...toLog) { if (DEBUG) { console.debug(LOG_PREFIX, ...toLog); } } const waitingObservers = new Map(); class Ignorer { #selector; constructor(selector) { this.#selector = selector; } then() { // warn("Ignoring " + this.#selector); } } function uniqueWaitForElement(selector, needReplacing) { const oldObserver = waitingObservers.get(selector); if (oldObserver) { if (!needReplacing) { return new Ignorer(selector); } else { warn("Replaced waiting observer for ", selector); oldObserver.disconnect(); waitingObservers.delete(selector); } } return new Promise(resolve => { const queryR###lt = document.querySelector(selector); if (queryR###lt) { return resolve(queryR###lt); } const observer = new MutationObserver(mutations => { const queryR###lt = document.querySelector(selector); if (queryR###lt) { /* * Disconnect first, just in case the listeners * on the returned Promise trigger the observer * again. */ observer.disconnect(); waitingObservers.delete(selector); resolve(queryR###lt); } }); waitingObservers.set(selector, observer); observer.observe(document.body, { childList: true, subtree: true }); }); } function replaceLogoOnLoadingScreen() { /* * Fill is only for the light mode. */ GM_addStyle(` #placeholder > svg path { d: path("M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z"); } #placeholder svg.r-18jsvk2 path { fill: #1DA1F2; } `); } /* * Replace soul-less regular house icon with a birdhouse icon. */ function replaceBirdhouseHomeIcon() { const svgPathDark = 'M12 9c-2.209 0-4 1.791-4 4s1.791 4 4 4 4-1.791 4-4-1.791-4-4-4zm0 6c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm0-13.304L.622 8.807l1.06 1.696L3 9.679V19.5C3 20.881 4.119 22 5.5 22h13c1.381 0 2.5-1.119 2.5-2.5V9.679l1.318.824 1.06-1.696L12 1.696zM19 19.5c0 .276-.224.5-.5.5h-13c-.276 0-.5-.224-.5-.5V8.429l7-4.375 7 4.375V19.5z'; // const svgPathLight = 'M12 1.696L.622 8.807l1.06 1.696L3 9.679V19.5C3 20.881 4.119 22 5.5 22h13c1.381 0 2.5-1.119 2.5-2.5V9.679l1.318.824 1.06-1.696L12 1.696zM12 16.5c-1.933 0-3.5-1.567-3.5-3.5s1.567-3.5 3.5-3.5 3.5 1.567 3.5 3.5-1.567 3.5-3.5 3.5z'; const linkSelector = 'nav > a[href="/home"]'; GM_addStyle(` ${linkSelector} svg path { d: path("${svgPathDark}"); } `); } /* * Adapted from https://userstyles.world/style/11077/old-twitter-logo * by sapondanaisriwan. License: MIT. */ function replaceLogoInHeader() { GM_addStyle(` h1 svg path, [data-testid="TopNavBar"] div > div > div > div > div > div > div > svg path { d: path("M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z"); } h1 svg.r-18jsvk2 path, div.r-6026j[data-testid="TopNavBar"] div > div > div > div > div > div > svg path { fill: #1DA1F2; } `); } function replaceLogo() { replaceLogoOnLoadingScreen(); replaceLogoInHeader(); replaceBirdhouseHomeIcon(); } /* * Replaces all tab icons, shortcut icons, and favicons * with given `newUrl`. */ function setFavicon(newUrl) { const faviconNodes = document.querySelectorAll(FAVICON_SELECTOR); if (!faviconNodes || faviconNodes.length == 0) { error("Cannot find favicon elements."); return; } info("New URL", newUrl); faviconNodes.forEach(node => { info("Replacing old URL =", node.href); node.href = newUrl; }); } function renameTwitterInTabName() { let t = document.title; if (t == "X") { t = "Twitter"; } if (t.endsWith("/ X")) { t = t.replace("/ X", "/ Twitter"); } if (t.includes(" on X: ")) { t = t.replace(" on X: ", " on Twitter: "); } if (t.startsWith("Compose new post")) { t = t.replace("Compose new post", "Compose new tweet"); } if (t.startsWith("Posts with ")) { t = t.replace("Post", "Tweet"); } if (t.startsWith("Quotes of this post ")) { t = t.replace("Quotes of this post ", "Quotes of this tweet "); } if (t != document.title) { document.title = t; } } /* * Replaces text "123 posts" with "123 tweets" on user profile pages. */ function renameProfileTweetsCounter() { // Debug code for figuring out CSS selectors for mobile version /* const allDivs = document.querySelectorAll('div'); for (const div of allDivs) { if (div.innerHTML.endsWith('posts')) { prompt('Classes ', div.classList); return; } } return; */ /* * Tweets/Posts counters in user profiles are weird. Their nesting/wrapping * depends on the theme and on mobile vs desktop. * By "theme" I mean "More > Settings and Support > Display > Background". */ // <h2> tag = user's name in the header, right above the tweet counter const commonDesktopSelector = 'h2 + div:not(#layers)'; // mobile selector wasn't updated in v27.3 and in v29.4 const commonMobileSelector = '.css-901oao.css-1hf3ou5.r-37j5jr.r-1b43r93.r-16dba41.r-14yzgew.r-bcqeeo.r-qvutc0'; const POSTS_SELECTOR = `${commonDesktopSelector}, ${commonMobileSelector}`; uniqueWaitForElement(POSTS_SELECTOR).then(postsElement => { try { const s = postsElement.innerText; if (s.includes('tweets')) { return; } const m = s.match('([0-9.,KMB]+) posts'); if (m == null) { warn("Cannot match posts string", s); return; } if (m.length < 2) { error("Cannot match posts string", s, ". Got " + m.length + " elements in the match"); return; } postsElement.innerHTML = m[1] + " tweets"; } catch (e) { error("Cannot rename posts to tweets", e); } }); } function renameNavTabTweets() { uniqueWaitForElement('main nav [data-testid="ScrollSnap-List"] > div:first-child span').then(tweetsTabName => { if (tweetsTabName.innerText == "Posts") { tweetsTabName.innerHTML = "Tweets"; } }); } /* * Renames existing "Tweet" buttons in popup dialogs on desktop. */ function doRenameDialogTweetButton() { const newTweetButton = document.querySelector(DIALOG_TWEET_BUTTON_SELECTOR); if (newTweetButton == null) { return; } if (newTweetButton.innerText == "Post all") { newTweetButton.innerText = "Tweet all"; } else if (newTweetButton.innerText == "Post") { newTweetButton.innerText = "Tweet"; } debug("DIALOG_TWEET_BUTTON_SELECTOR", newTweetButton); } /* * Button "Tweet" needs to change dynamically into "Tweet all" when * more than two tweets are added to the "draft". * * This observer detects changes in its text, because the button * actually gets recreated inside the popup dialog. */ let tweetButtonObserver = null; /* * Renames various oval blue buttons used to send a tweet, i.e. "to tweet". */ function renameTweetButton() { uniqueWaitForElement('a[data-testid="SideNav_NewTweet_Button"] > div > span > div > div > span > span').then(tweetButton => { if (tweetButton.innerText == "Post") { // avoid renaming "Reply" tweetButton.innerHTML = "Tweet"; debug("SideNav", tweetButton); } }); uniqueWaitForElement(DIALOG_TWEET_BUTTON_SELECTOR).then(tweetButton => { tweetButton.innerHTML = "Tweet"; if (tweetButtonObserver != null) { return; } tweetButtonObserver = new MutationObserver(mutations => { doRenameDialogTweetButton(); }); /* * Separate observer is needed to avoid leaking `tweetButtonObserver` * and to reconnect `tweetButtonObserver` onto new buttons, when * they appear. */ const dialogObserver = new MutationObserver(mutations => { if (document.querySelector('[role="dialog"]') == null) { tweetButtonObserver.disconnect(); tweetButtonObserver = null; info("Disconnected tweetButtonObserver"); dialogObserver.disconnect(); } }); tweetButtonObserver.observe(document.querySelector('[role="dialog"]'), { childList: true, subtree: true }); info("Connected tweetButtonObserver"); dialogObserver.observe(document.body, { childList: true, subtree: true }); }); // button near the text input at the top of the home page uniqueWaitForElement('button[data-testid="tweetButtonInline"] > div > span > span').then(tweetButton => { // sometimes this button has the correct text "Reply" if (tweetButton.innerText == "Post") { debug("tweetButtonInline", tweetButton); tweetButton.innerHTML = "Tweet"; } }); } /* * Renames tabs "Retweets" and "Quote Tweets" on an individual tweet's "Tweet engagements" subpage. * E.g. on https://twitter.com/andrybak0/status/1711154194835554336/retweets */ function renameTweetEngagementsSubpageTabs() { const pathParts = document.location.pathname.split('/'); if (pathParts.length < 5) { return; } if (pathParts[2] !== 'status') { return; } if (!['quotes', 'retweets', 'likes'].includes(pathParts[4])) { return; } uniqueWaitForElement('a[href$="/retweets"] span').then(retweetsTabHeader => { retweetsTabHeader.replaceChildren(document.createTextNode("Retweets")); }); /* * Renames tab "Quotes" → "Quote Tweets" * A bunch of old screenshots for confirmation: * https://danieljmitchell.wordpress.com/2020/12/29/2020s-tweet-of-the-year/ */ uniqueWaitForElement('a[href$="/quotes"] span').then(quotesTabHeader => { quotesTabHeader.replaceChildren(document.createTextNode("Quote Tweets")); }); } /* * Renames "Add another tweet" button (for continuing your own existing thread). */ function renameAddAnotherTweetButton() { uniqueWaitForElement('main section > div > div > div > div > div > a[href="/compose/post"] span > span').then(addAnotherTweetButton => { if (addAnotherTweetButton.innerText == "Add another post") { addAnotherTweetButton.innerHTML = "Add another tweet"; } }); } /* * Renames the header on a page for a singular tweet. */ function renameTweetHeader() { uniqueWaitForElement('h2[role="heading"] > span').then(tweetHeader => { if (tweetHeader.innerText == "Post") { tweetHeader.innerHTML = "Tweet"; } else if (tweetHeader.innerText == "Reposted by") { tweetHeader.innerHTML = "Retweeted by"; } else if (tweetHeader.innerText == "Quotes") { /* * Source, confirming that they were indeed called that: * https://www.macrumors.com/2020/08/31/twitter-quote-tweets-feature/ */ tweetHeader.innerHTML = "Quote Tweets"; } else if (tweetHeader.innerText == "Post engagements") { tweetHeader.innerHTML = "Tweet engagements"; } }); } /* * Note: works only for desktop. */ function renameDraftEditorPlaceholder(argss) { uniqueWaitForElement('.public-DraftEditorPlaceholder-inner').then(placeholder => { for (const args of argss) { const targetText = args[0], replacementText = args[1], debugMessage = args[2]; if (placeholder.innerText == targetText) { placeholder.replaceChildren(document.createTextNode(replacementText)); debug("Renamed placeholder in", debugMessage); break; } } }); } /* * Note: works only for mobile. */ function renameTextAreaAttributePlaceholder(argss) { for (const args of argss) { const targetText = args[0], replacementText = args[1]; uniqueWaitForElement(`textarea[placeholder="${targetText}"]`).then(textarea => { textarea.setAttribute('placeholder', replacementText); }); } } function renameTweetPlaceholders() { const argss = [ ["Post your reply!", "Tweet your reply!", "renameTweetYourReplyPlaceholder 1"], ["Post your reply", "Tweet your reply", "renameTweetYourReplyPlaceholder 2"], ["Add another post!", "Add another tweet!", "renameAddAnotherTweetPlaceholder 1"], ["Add another post", "Add another tweet", "renameAddAnotherTweetPlaceholder 2"] ]; renameDraftEditorPlaceholder(argss); renameTextAreaAttributePlaceholder(argss); /* * TODO: is there some way to detect desktop vs mobile? */ } function doRenameRetweeted() { const allRetweeted = document.querySelectorAll(RETWEETED_SELECTOR); debug(`doRenameRetweeted: renaming ${allRetweeted.length} of "... reposted" to "... retweeted"`); let counter = 0; for (const retweeted of allRetweeted) { if (retweeted.childNodes.length === 1 && retweeted.childNodes[0].textContent === "You reposted") { retweeted.childNodes[0].remove(); retweeted.append("You retweeted"); counter++; continue; } // debug(retweeted.childNodes); if (retweeted.childNodes.length < 2) { continue; } const retweetedText = retweeted.childNodes[1]; if (retweetedText.textContent === " reposted") { retweetedText.remove(); retweeted.append(" retweeted"); counter++; } } if (counter > 0) { debug(`Renamed fresh ${counter} retweeted text nodes.`); } } /* * Whenever timeline gets recreated/replaced on-the-fly, * we need to wait a bit, until the retweets actually * appear in the document. */ function renameRetweetedGently() { uniqueWaitForElement(RETWEETED_SELECTOR).then(ignored => { doRenameRetweeted(); }); } let showTweetsObserver; /* * Clickable link at the top of the timeline. * "Show 42 tweets" */ function renameShowTweets() { const showTweetsArray = document.querySelectorAll(SHOW_N_TWEETS_SELECTOR); for (const showTweets of showTweetsArray) { let t = showTweets.childNodes[0].textContent; if (t.includes('posts')) { if (showTweetsObserver != null) { showTweetsObserver.disconnect(); showTweetsObserver = null; } t = t.replace('posts', 'tweets'); showTweets.childNodes[0].textContent = t; info("doRenameShowTweets: replaced", t); showTweetsObserver = new MutationObserver(ignored => { renameShowTweets(); }); showTweetsObserver.observe(showTweets, { characterData: true }); return; } } } let timelineObserver = null; /* * Reconnects the observer to the timeline node. * This is needed when the page changes completely and a new timeline * node appears in the DOM. */ function renewTimelineObserver() { /* * Renaming "Jane Doe retweeted" when you know where these nodes are is easy. * That's the function `doRenameRetweeted()` above. But keeping track of them * appearing in the timeline is difficult, if you also don't want to waste * CPU unnecessarily. * * The code below kinda works, but the user still sees "reposted" from time * to time. Any suggestions for improvements are welcome. * * We wait for the <section> that the user sees to appear. */ uniqueWaitForElement('main [data-testid="primaryColumn"] section.css-175oi2r').then(timeline => { if (timelineObserver !== null) { timelineObserver.disconnect(); timelineObserver = null; info("Disconnected timeline observer"); } if (document.querySelector('main [data-testid="primaryColumn"] nav') == null) { info('renewTimelineObserver: Not on a timeline view. Aborting.'); return; } renameRetweetedGently(); renameShowTweets(); timelineObserver = new MutationObserver(mutationsList => { doRenameRetweeted(); renameShowTweets(); }); /* * And we observe all <section> tags: */ const allSections = document.querySelectorAll('main section.css-175oi2r'); info("Renewing timeline observer for", allSections.length, "tags"); if (allSections.length == 0) { error("Cannot find the timeline <section> tag"); } for (const section of allSections) { timelineObserver.observe(section, { subtree: true, childList: true, characterData: true }); info("Added timeline observer", section); } }); doRenameRetweeted(); } function renameRetweetLink() { uniqueWaitForElement('[data-testid="retweetConfirm"] span.css-901oao.css-16my406.r-poiln3.r-bcqeeo.r-qvutc0').then(retweetLink => { /* * TODO: on desktop, this gets called twice, unfortunately. */ if (retweetLink.innerText == "Repost") { retweetLink.innerHTML = "Retweet"; debug("Renamed 'Retweet' in renameRetweetLink"); } }); } function renameRetweetTooltip() { uniqueWaitForElement('[data-testid="HoverLabel"] span').then(retweetTooltip => { if (retweetTooltip.innerText == "Repost") { retweetTooltip.innerText = "Retweet"; debug("Renamed 'Retweet' in renameRetweetTooltip"); } else if (retweetTooltip.innerText == "Undo repost") { retweetTooltip.innerText = "Undo retweet"; debug("Renamed 'Undo retweet' in renameRetweetTooltip"); } }); } /* * There are at least two affected dropdowns: * 1. "More" under the three dots button in the top right of a tweet * 2. "Share" under the button with "Send" icon (desktop) or * "Share" icon (mobile) in the bottom right of a tweet. */ function renameDropdownItems() { // Desktop: [data-testid="Dropdown"] // Mobile : [data-testid="sheetDialog"] uniqueWaitForElement('#layers [role="menu"]').then(dropdown => { /* * TODO: on desktop, this gets called twice, unfortunately. */ dropdown.querySelectorAll('[role="menuitem"] span').forEach(span => { if (span.innerText.includes("post")) { span.innerHTML = span.innerText.replace("post", "tweet"); debug("Renamed 'tweet' in renameDropdownItems"); } }); }); } function renameRetweetedByPopupHeader() { uniqueWaitForElement('#layers h2 > .css-901oao.css-16my406.r-poiln3.r-bcqeeo.r-qvutc0').then(tweetHeader => { if (tweetHeader.innerText == "Reposted by") { tweetHeader.innerHTML = "Retweeted by"; } }); } function doRenameYourTweetWasSent() { const maybeToast = document.querySelector('#layers [data-testid="toast"] > div > span'); if (maybeToast) { const t = maybeToast.innerText; if (t.includes(' post ')) { maybeToast.innerHTML = t.replace(' post ', ' tweet '); debug("doRenameYourTweetWasSent", t); } } } function doRenameDeletePostQuestion() { const maybeHeader = document.querySelector('#layers [data-testid="confirmationSheetDialog"] > h1'); if (maybeHeader) { if (maybeHeader.innerText == "Delete post?") { maybeHeader.innerHTML = "Delete tweet?"; debug("doRenameDeletePostQuestion"); } } } let layersObserver; /* * #layers is the element where tooltips, dropdown menues, and * popup replies (as opposed to inline replies) are shown. */ function renewLayersObserver() { uniqueWaitForElement('#layers').then(retweetDropdownContainer => { if (layersObserver != null) { layersObserver.disconnect(); layersObserver = null; info("Disconnected layersObserver"); } layersObserver = new MutationObserver(mutationsList => { renameRetweetLink(); renameRetweetTooltip(); renameDropdownItems(); /* * There are both inline and popup "Reply" fields with placeholders. */ renameTweetPlaceholders(); doRenameDialogTweetButton(); renameRetweetedByPopupHeader(); doRenameYourTweetWasSent(); doRenameDeletePostQuestion(); }); layersObserver.observe(retweetDropdownContainer, { subtree: true, childList: true }); info("Added layersObserver"); }); } let pillObserver; function renameSeeTweetsPill() { /* * Several types of "pills": * - "X, Y, Z tweeted" * - "See new tweets" */ uniqueWaitForElement('[data-testid="pillLabel"] span span span.css-901oao.css-16my406.r-poiln3.r-bcqeeo.r-qvutc0').then(pill => { if (pill.innerText == "posted") { if (pillObserver != null) { pillObserver.disconnect(); pillObserver = null; } pill.innerHTML = "tweeted"; debug('Renamed "tweeted" pill'); } else if (pill.innerText == "See new posts") { pill.innerHTML = "See new tweets"; debug('Renamed "See new tweets" pill'); /* * FIXME: A dirty hack to sync the pill with the clickable link * can't figure out how to make `renewTimelineObserver()` * detect the clickable link properly. */ renameShowTweets(); /* * If a user keeps the timeline open long enough, pill "See new tweets" * turns into "X, Y, Z posted". This observer will make sure to rename * it to "X, Y, Z tweeted". */ pillObserver = new MutationObserver(() => { renameSeeTweetsPill(); }); pillObserver.observe(pill, { characterData: true }); } }); } function renameTweetInNotifications() { /* * This mess of a selector tries to minimize the amount of `spanNodes` that match. */ const spanNodes = document.querySelectorAll('article div > div > div > span > span'); debug(`renameTweetInNotifications: renaming ${spanNodes.length} nodes (shouldn't be too high!)`); spanNodes.forEach(spanNode => { if (spanNode.innerText.includes("post") || spanNode.innerText.includes("Post")) { let s = spanNode.innerText; s = s.replaceAll(" repost", " retweet"); s = s.replaceAll(" post", " tweet"); s = s.replaceAll(" Post", " Tweet"); s = s.replaceAll(" Repost", " Retweet"); spanNode.innerText = s; } }); } let notificationsObserver; function renewNotificationsObserver() { if (document.location.pathname != '/notifications') { if (notificationsObserver != null) { notificationsObserver.disconnect(); info("Disconnected notificationsObserver"); } return; } uniqueWaitForElement('[aria-label="Timeline: Notifications"]').then(notificationsContainer => { notificationsObserver = new MutationObserver(mutationsList => { renameTweetInNotifications(); }); notificationsObserver.observe(notificationsContainer, { subtree: true, childList: true }); info("Added notificationsObserver"); }); } /* * "This Tweet is from an account you muted." * "This Tweet is from an account you blocked." * "You’re unable to view this Tweet because this account owner limits who can view their Tweets." * "This Tweet was deleted by the Tweet author." * "This Tweet is from an account that no longer exists." */ function doRenameHiddenTweets() { const spanNodes = document.querySelectorAll('section article > div > div > div > div > div > div > div > span > span > span'); spanNodes.forEach(spanNode => { if (spanNode.innerText.includes("post") || spanNode.innerText.includes("Post")) { let s = spanNode.innerText; s = s.replaceAll(" repost", " retweet"); s = s.replaceAll(" post", " tweet"); s = s.replaceAll(" Post", " Tweet"); s = s.replaceAll(" Repost", " Retweet"); spanNode.innerText = s; } }); } function renameHiddenTweets() { uniqueWaitForElement('main section article > div > div > div > div > div > div > div > span > span > span').then(() => { doRenameHiddenTweets(); }); } function renameTranslateTweet() { const translateButtonSelector = 'section > div > div > div[data-testid="cellInnerDiv"] div[role="button"] span.css-1qaijid.r-bcqeeo.r-qvutc0.r-poiln3'; uniqueWaitForElement(translateButtonSelector).then(button => { const spanNodes = document.querySelectorAll(translateButtonSelector); spanNodes.forEach(spanNode => { if (spanNode.innerText.includes("post")) { spanNode.innerText = spanNode.innerText.replaceAll(" post", " tweet"); } }); }); } function renameSourcedFromAcrossTwitter() { const selector = 'section > div > div > div[data-testid="cellInnerDiv"] > div > div > div > div > span'; uniqueWaitForElement(selector).then(subheading => { info("renameSourcedFromAcrossTwitter", subheading, subheading.innerText); if (subheading.innerText == "Sourced from across X") { subheading.replaceChildren(document.createTextNode("Sourced from across Twitter")); } }); } function isAProfilePage() { const pathParts = document.location.pathname.split('/'); if (pathParts.length < 2) { return false; } return document.title.includes("(@" + pathParts[1] + ")"); } /* * There are four layers to the userscript: * * 1. Big rename in `rename()`, i.e. this function. It gets called * whenever we suspect that an on-the-fly change of the whole * page happened. It calls functions from layers 2 and 3. * See function `setUpRenamer()` for details. * 2. `MutationObserver`s for stuff that gets updated on-the-fly. * These observers are set up by various `renew...Observer()` * functions. These observers call functions from layers 3 and 4. * 3. Various `rename<this and that>()` functions, that wait for their * target element. * 4. Various `doRename<this and that>()` functions, that assume that * their target element already exists in the document. */ function rename() { // "Tweet" button and tab's <title> are ubiquitous renameTweetButton(); renameTwitterInTabName(); // targets for renaming on a singular tweet page renameTweetHeader(); renameTweetEngagementsSubpageTabs(); renameTweetPlaceholders(); renameTranslateTweet(); // targets for renaming on a user's profile if (isAProfilePage()) { renameProfileTweetsCounter(); renameNavTabTweets(); } // adding to your own thread renameAddAnotherTweetButton(); // timeline + tweets on a timeline renewLayersObserver(); renameSeeTweetsPill(); renameHiddenTweets(); renameSourcedFromAcrossTwitter(); renewNotificationsObserver(); } function setUpRenamer() { let title = document.title; const titleObserver = new MutationObserver(mutationsList => { const maybeNewTitle = document.title; if (maybeNewTitle != title) { info('Title changed:', maybeNewTitle); title = maybeNewTitle; if (!title.includes("Twitter") || title.endsWith("/ X")) { info("Big renaming: starting..."); rename(); renewTimelineObserver(); info("Big renaming: done ✅"); } } }); uniqueWaitForElement('title').then(elem => { titleObserver.observe(elem, { subtree: true, characterData: true, childList: true }); }); rename(); } replaceLogo(); rename(); uniqueWaitForElement(FAVICON_SELECTOR).then(ignored => { setFavicon(TWITTER_2012_ICON_URL); setUpRenamer(); }); })();