Skip to content

Instantly share code, notes, and snippets.

@angeld23
Last active March 4, 2024 20:18
Show Gist options
  • Star 41 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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.1
// @description Detects whenever a tweet mysteriously vanishes from your timeline for no reason and allows you to re-open it
// @author angeld23
// @match *://*.twitter.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.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://twitter.com/i/api/1.1/mutes/users/create.json" ||
req.responseURL === "https://twitter.com/i/api/1.1/blocks/users/create.json" ||
req.responseURL.startsWith("https://twitter.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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment