Skip to content

Instantly share code, notes, and snippets.

@angeld23
Last active November 22, 2024 18:33
Show Gist options
  • Save angeld23/be0a517fbd9a8853a9d05302656e1666 to your computer and use it in GitHub Desktop.
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
// ==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);
}
});
};
});
})();
@gage64
Copy link

gage64 commented Jun 5, 2023

truly revolutionary

@RoootTheFox
Copy link

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

@Nick-Gabe
Copy link

love ya

@nodomw
Copy link

nodomw commented Jun 5, 2023

greatest of all time ty

@Uzixt
Copy link

Uzixt commented Jun 5, 2023

not all heros wear capes

@ValorZeroAdvent
Copy link

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.

@kokirbe
Copy link

kokirbe commented Dec 27, 2023

very cool!

@Colorized
Copy link

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