Last active
November 22, 2024 18:33
-
-
Save angeld23/be0a517fbd9a8853a9d05302656e1666 to your computer and use it in GitHub Desktop.
Vanished Tweet Recovery: Detects whenever a tweet mysteriously vanishes from your timeline for no reason and allows you to re-open it
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 Vanished Tweet Recovery | |
// @namespace https://d23.dev/ | |
// @version 1.2 | |
// @description Detects whenever a tweet mysteriously vanishes from your timeline for no reason and allows you to re-open it | |
// @author angeld23 | |
// @match *://*.x.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=x.com | |
// @grant none | |
// ==/UserScript== | |
"use strict"; | |
(() => { | |
/** | |
* Calls the provided callback when the document is loaded | |
*/ | |
function onReady(fn) { | |
if (document.readyState != "loading") { | |
fn(); | |
} | |
else { | |
document.addEventListener("DOMContentLoaded", fn); | |
} | |
} | |
function isTweet(node) { | |
if (!node) | |
return false; | |
const tweet = node; | |
if (tweet instanceof HTMLElement) { | |
if (tweet.getAttribute("data-testid") === "tweet") { | |
return true; | |
} | |
} | |
return false; | |
} | |
function shouldNotify(tweet) { | |
if (Date.now() - lastNotify < 250) | |
return false; | |
if (Date.now() - lastPageLoad < 1000) | |
return false; | |
if (Date.now() - lastInput < 200) | |
return false; | |
if (tweet._doNotNotify) | |
return false; | |
if (tweet._addedTime === undefined) | |
return false; | |
if (tweet._parent === undefined) | |
return false; | |
if (Date.now() - tweet._addedTime < 250) | |
return false; | |
if (!tweet._onScreen) | |
return false; | |
return true; | |
} | |
function isOnScreen(elm) { | |
const rect = elm.getBoundingClientRect(); | |
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); | |
return !(rect.bottom < 0 || rect.top - viewHeight >= 0); | |
} | |
function getTweetTextFromContainer(container) { | |
const result = []; | |
Array.from(container.children).forEach((_element) => { | |
const element = _element; | |
if (element.tagName === "IMG") { | |
return result.push({ | |
elementType: "emoji", | |
src: element.getAttribute("src") ?? "", | |
}); | |
} | |
const anchor = element.querySelector("a") ?? (element.tagName === "A" && element); | |
if (anchor) { | |
result.push({ | |
elementType: "link", | |
text: element.outerText.split("://")[1] ?? element.outerText, | |
href: anchor.getAttribute("href") ?? "", | |
}); | |
const emoji = anchor.querySelector("img"); | |
if (emoji) { | |
result.push({ | |
elementType: "emoji", | |
src: emoji.getAttribute("src") ?? "", | |
}); | |
} | |
return; | |
} | |
return result.push({ | |
elementType: "normal", | |
text: element.outerText, | |
}); | |
}); | |
return result; | |
} | |
function renderTweetText(text, linkColor) { | |
const parent = document.createElement("p"); | |
text.map((textElement) => { | |
const elemType = textElement.elementType; | |
if (elemType === "emoji") { | |
const img = document.createElement("img"); | |
img.src = textElement.src; | |
img.style.width = "18px"; | |
img.style.height = "18px"; | |
img.style.margin = "0px 3px"; | |
img.style.verticalAlign = "-20%"; | |
return img; | |
} | |
if (elemType === "link") { | |
const anchor = document.createElement("a"); | |
anchor.href = textElement.href; | |
anchor.textContent = textElement.text; | |
anchor.style.color = linkColor ?? "rgb(29, 155, 240)"; | |
anchor.style.textDecoration = "none"; | |
anchor.addEventListener("mouseenter", () => { | |
anchor.style.textDecoration = "underline"; | |
}); | |
anchor.addEventListener("mouseleave", () => { | |
anchor.style.textDecoration = "none"; | |
}); | |
return anchor; | |
} | |
if (elemType === "normal") { | |
const span = document.createElement("span"); | |
span.textContent = textElement.text; | |
return span; | |
} | |
return document.createElement("span"); | |
}).forEach((elem) => parent.appendChild(elem)); | |
return parent; | |
} | |
let lastNotify = 0; | |
let notiContainer; | |
let currentZ = 1; | |
function showNotification(displayName, handle, bodyText, images, profilePictureImage, buttonLink) { | |
if (!notiContainer || !notiContainer.parentElement) { | |
const layers = document.querySelector("#layers"); | |
if (!layers) | |
return; | |
notiContainer = document.createElement("div"); | |
notiContainer.style.position = "fixed"; | |
notiContainer.style.width = "100%"; | |
notiContainer.style.height = "100%"; | |
notiContainer.style.pointerEvents = "none"; | |
layers.appendChild(notiContainer); | |
} | |
lastNotify = Date.now(); | |
const colorMode = ({ | |
"rgb(255, 255, 255)": "white", | |
"rgb(21, 32, 43)": "dim", | |
"rgb(0, 0, 0)": "black", | |
}[getComputedStyle(document.body).backgroundColor] ?? "dim"); | |
const borderColor = { | |
white: "rgb(239, 243, 244)", | |
dim: "rgb(56, 68, 77)", | |
black: "rgb(47, 51, 54)", | |
}[colorMode]; | |
const textColor = { | |
white: "rgb(15, 20, 25)", | |
dim: "rgb(247, 249, 249)", | |
black: "rgb(231, 233, 234)", | |
}[colorMode]; | |
const subtextColor = { | |
white: "rgb(83, 100, 113)", | |
dim: "rgb(139, 152, 165)", | |
black: "rgb(113, 118, 123)", | |
}[colorMode]; | |
let themeColor = "rgb(29, 155, 240)"; | |
const composeButton = document.querySelector("a[href='/compose/tweet']"); | |
if (composeButton) { | |
themeColor = getComputedStyle(composeButton).backgroundColor; | |
} | |
const div = document.createElement("div"); | |
div.style.position = "absolute"; | |
div.style.bottom = "100px"; | |
div.style.left = "-450px"; | |
div.style.padding = "15px"; | |
div.style.backgroundColor = document.body.style.backgroundColor; | |
div.style.color = "#fff"; | |
div.style.borderRadius = "15px"; | |
div.style.zIndex = String(currentZ++); | |
div.style.maxWidth = "400px"; | |
div.style.boxSizing = "border-box"; | |
div.style.pointerEvents = "auto"; | |
div.style.fontFamily = | |
"TwitterChirp, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"; | |
div.style.border = "1px solid " + borderColor; | |
div.style.overflow = "hidden"; | |
div.style.transition = "left ease 0.2s"; | |
const topTitle = document.createElement("p"); | |
topTitle.textContent = "Vanished Tweet Recovery"; | |
topTitle.style.fontSize = "15px"; | |
topTitle.style.marginBottom = "10px"; | |
topTitle.style.marginTop = "0px"; | |
topTitle.style.color = subtextColor; | |
div.appendChild(topTitle); | |
const profilePictureAndTextContainer = document.createElement("div"); | |
profilePictureAndTextContainer.style.display = "flex"; | |
div.appendChild(profilePictureAndTextContainer); | |
if (profilePictureImage) { | |
const profilePicture = document.createElement("img"); | |
profilePicture.src = profilePictureImage; | |
profilePicture.style.borderRadius = "50%"; | |
profilePicture.style.marginRight = "10px"; | |
profilePicture.style.marginBottom = "10px"; | |
profilePicture.style.width = "48px"; | |
profilePicture.style.height = "48px"; | |
profilePictureAndTextContainer.appendChild(profilePicture); | |
} | |
const textContainer = document.createElement("div"); | |
textContainer.style.maxWidth = profilePictureImage ? "calc(100% - 58px)" : "100%"; | |
profilePictureAndTextContainer.appendChild(textContainer); | |
const nameContainer = document.createElement("div"); | |
nameContainer.style.display = "flex"; | |
nameContainer.style.alignItems = "center"; | |
nameContainer.style.width = "100%"; | |
textContainer.appendChild(nameContainer); | |
const displayNameParagraph = renderTweetText(displayName, themeColor); | |
displayNameParagraph.style.fontSize = "15px"; | |
displayNameParagraph.style.fontWeight = "bold"; | |
displayNameParagraph.style.color = textColor; | |
displayNameParagraph.style.overflow = "hidden"; | |
displayNameParagraph.style.whiteSpace = "nowrap"; | |
displayNameParagraph.style.margin = "0"; | |
nameContainer.appendChild(displayNameParagraph); | |
const handleSpan = document.createElement("span"); | |
handleSpan.style.fontSize = "15px"; | |
handleSpan.textContent = handle; | |
handleSpan.style.fontWeight = "400"; | |
handleSpan.style.marginLeft = "4px"; | |
handleSpan.style.color = subtextColor; | |
handleSpan.style.whiteSpace = "nowrap"; | |
nameContainer.appendChild(handleSpan); | |
const isNoText = bodyText[0]?.elementType === "normal" && bodyText[0].text === "(No Text)"; | |
const body = renderTweetText(bodyText, themeColor); | |
body.style.color = isNoText ? subtextColor : textColor; | |
body.style.marginBottom = "12px"; | |
body.style.marginTop = "2px"; | |
body.style.lineHeight = "20px"; | |
textContainer.appendChild(body); | |
if (images.length > 0) { | |
const imageContainer = document.createElement("div"); | |
imageContainer.style.display = "flex"; | |
imageContainer.style.justifyContent = "center"; | |
imageContainer.style.height = "100px"; | |
imageContainer.style.maxWidth = "100%"; | |
imageContainer.style.marginBottom = "15px"; | |
images.forEach((image) => { | |
const imgElement = document.createElement("img"); | |
imgElement.src = image; | |
imgElement.style.margin = "0px 5px"; | |
imgElement.style.borderRadius = "5px"; | |
imgElement.style.objectFit = "cover"; | |
imgElement.style.minWidth = "50px"; | |
imgElement.style.height = "100%"; | |
imgElement.style.border = "1px solid " + borderColor; | |
imageContainer.appendChild(imgElement); | |
}); | |
div.appendChild(imageContainer); | |
} | |
const buttonContainer = document.createElement("div"); | |
buttonContainer.style.display = "flex"; | |
buttonContainer.style.justifyContent = "center"; | |
buttonContainer.style.gap = "10px"; | |
div.appendChild(buttonContainer); | |
const button = document.createElement("a"); | |
button.textContent = buttonLink ? "Open Tweet" : "(Failed to Get Link)"; | |
if (buttonLink) { | |
button.href = buttonLink; | |
} | |
button.style.display = "inline-block"; | |
button.style.backgroundColor = buttonLink ? themeColor : ""; | |
button.style.color = buttonLink ? "#fff" : subtextColor; | |
button.style.textDecoration = "none"; | |
button.style.fontWeight = buttonLink ? "bold" : ""; | |
button.style.padding = "10px 30px"; | |
button.style.borderRadius = "100000px"; | |
button.style.fontSize = "14px"; | |
button.style.transition = "background-color ease 0.5s"; | |
button.addEventListener("click", () => { | |
div.remove(); | |
}); | |
button.addEventListener("mouseenter", () => { | |
button.style.textDecoration = "underline"; | |
}); | |
button.addEventListener("mouseleave", () => { | |
button.style.textDecoration = "none"; | |
}); | |
buttonContainer.appendChild(button); | |
const dismissButton = document.createElement("button"); | |
dismissButton.textContent = "Dismiss"; | |
dismissButton.style.display = "inline-block"; | |
dismissButton.style.backgroundColor = document.body.style.backgroundColor; | |
dismissButton.style.color = textColor; | |
dismissButton.style.textDecoration = "none"; | |
dismissButton.style.fontWeight = "bold"; | |
dismissButton.style.padding = "10px 30px"; | |
dismissButton.style.borderRadius = "100000px"; | |
dismissButton.style.fontSize = "14px"; | |
dismissButton.style.border = "1px solid " + borderColor; | |
dismissButton.style.cursor = "pointer"; | |
dismissButton.style.transition = "background-color ease 0.5s"; | |
let removed = false; | |
const rem = () => { | |
if (removed) | |
return; | |
removed = true; | |
div.style.left = "-450px"; | |
setTimeout(() => { | |
div.remove(); | |
}, 500); | |
}; | |
dismissButton.addEventListener("click", rem); | |
setTimeout(rem, 10000); | |
dismissButton.addEventListener("mouseenter", () => { | |
dismissButton.style.textDecoration = "underline"; | |
}); | |
dismissButton.addEventListener("mouseleave", () => { | |
dismissButton.style.textDecoration = "none"; | |
}); | |
buttonContainer.appendChild(dismissButton); | |
notiContainer.appendChild(div); | |
setTimeout(() => { | |
div.style.left = "50px"; | |
}, 50); | |
dismissButton.textContent; | |
} | |
function onTweetVanish(tweet) { | |
if (!shouldNotify(tweet)) | |
return; | |
setTimeout(() => { | |
if (!shouldNotify(tweet)) | |
return; | |
const timeElem = tweet.querySelector("time"); | |
if (!timeElem) | |
return; | |
if (!timeElem.parentElement) | |
return; | |
const link = timeElem.parentElement.getAttribute("href"); | |
const textContainer = tweet.querySelector("div[data-testid='tweetText']"); | |
let text = []; | |
if (textContainer && textContainer.children.length > 0) { | |
text = getTweetTextFromContainer(textContainer); | |
} | |
else { | |
text = [{ elementType: "normal", text: "(No Text)" }]; | |
} | |
const nameContainer = tweet.querySelector("div[data-testid='User-Name']"); | |
let handle = "(Handle Unknown)"; | |
let displayName = [{ elementType: "normal", text: "(Display Name Unknown)" }]; | |
let timeAgo = ""; | |
if (nameContainer) { | |
const displayNameContainer = nameContainer.firstElementChild?.firstElementChild?.firstElementChild?.firstElementChild | |
?.firstElementChild?.firstElementChild; | |
if (displayNameContainer) { | |
displayName = getTweetTextFromContainer(displayNameContainer); | |
} | |
const handleSpan = nameContainer.querySelector("a[tabindex='-1']")?.firstElementChild?.firstElementChild; | |
if (handleSpan) { | |
handle = handleSpan.textContent ?? handle; | |
} | |
timeAgo = nameContainer.querySelector("time")?.textContent ?? "Now"; | |
} | |
let pfp = ""; | |
const pfpContainer = tweet.querySelector(`div[data-testid='UserAvatar-Container-${handle.slice(1)}'`); | |
if (pfpContainer) { | |
const img = pfpContainer.querySelector("img"); | |
if (img) { | |
pfp = img.src; | |
} | |
} | |
const images = []; | |
const imageContainers = tweet.querySelectorAll("div[data-testid='tweetPhoto']"); | |
imageContainers.forEach((container) => { | |
const img = container.querySelector("img"); | |
if (img) | |
images.push(img.src); | |
}); | |
showNotification(displayName, `${handle} · ${timeAgo}`, text, images, pfp, link ?? undefined); | |
}, 40); | |
} | |
function onRequestResponse(req) { | |
if (req.responseURL === "https://x.com/i/api/1.1/mutes/users/create.json" || | |
req.responseURL === "https://x.com/i/api/1.1/blocks/users/create.json" || | |
req.responseURL.startsWith("https://x.com/i/api/2/timeline/feedback.json")) { | |
lastInput = Date.now(); | |
} | |
} | |
(function (send) { | |
XMLHttpRequest.prototype.send = function (body) { | |
this.addEventListener("readystatechange", () => { | |
if (this.readyState === 4) { | |
onRequestResponse(this); | |
} | |
}); | |
return send.apply(this, [body]); | |
}; | |
})(XMLHttpRequest.prototype.send); | |
let lastPageLoad = Date.now(); | |
let lastInput = Date.now(); | |
onReady(() => { | |
let prevLocation = location.href; | |
setInterval(() => { | |
if (location.href !== prevLocation) { | |
lastPageLoad = Date.now(); | |
} | |
prevLocation = location.href; | |
}, 50); | |
const inputEvents = ["mousedown", "mouseup", "keydown", "keyup"]; | |
const observer = new MutationObserver((records) => { | |
records.forEach((record) => { | |
record.addedNodes.forEach((node) => { | |
inputEvents.forEach((name) => node.addEventListener(name, onInput)); | |
let _tweet = undefined; | |
if (isTweet(node)) { | |
_tweet = node; | |
} | |
else if (node instanceof Element) { | |
const results = node.querySelectorAll("article[data-testid='tweet']"); | |
if (results.length === 1) { | |
_tweet = results[0]; | |
} | |
} | |
if (!_tweet) | |
return; | |
const tweet = _tweet; | |
tweet._parent = node.parentElement ?? undefined; | |
tweet._addedTime = Date.now(); | |
tweet.setAttribute("data-vanishwatch", ""); | |
const loop = setInterval(() => { | |
if (!tweet.parentElement) { | |
clearInterval(loop); | |
return; | |
} | |
tweet._onScreen = isOnScreen(tweet); | |
}, 50); | |
}); | |
record.removedNodes.forEach((node) => { | |
const element = node; | |
if (isTweet(element)) { | |
onTweetVanish(element); | |
} | |
else if (isTweet(element._parent)) { | |
onTweetVanish(element._parent); | |
} | |
else if (element instanceof Element) { | |
const results = element.querySelectorAll("article[data-testid='tweet']"); | |
if (results.length === 1) { | |
onTweetVanish(results[0]); | |
} | |
} | |
}); | |
}); | |
}); | |
observer.observe(document, { | |
childList: true, | |
subtree: true, | |
}); | |
const onInput = () => { | |
lastInput = Date.now(); | |
Array.from(document.querySelectorAll("article[data-vanishwatch]")).forEach((elem) => { | |
const tweet = elem; | |
if (tweet._onScreen) { | |
tweet._onScreen = isOnScreen(tweet); | |
} | |
}); | |
}; | |
}); | |
})(); |
thank you so much for this!!
i have a small request though, could you add css classes to the popup's elements so it's easier to style them using custom CSS?
thank you :3
love ya
greatest of all time ty
not all heros wear capes
This is a good extension to have! Thank you.
Just thinking, is it possible to have the pop-ups appear on the right side of the website? I have extensions that make that side underutilised and it would be great to be able to move the pop-up position there. I've tried mucking with the CSS a little bit, but I couldn't figure out how to change the position of the pop-up after it initially appears.
very cool!
If the script isn't working for you, replace every mention of "twitter.com" with "x.com", including the API calls.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
truly revolutionary