您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Creates the option next to edited and deleted Reddit comments/posts to show the original comment from before it was edited
当前为
- // ==UserScript==
- // @name Unedit and Undelete for Reddit
- // @namespace http://tampermonkey.net/
- // @version 3.17.4
- // @description Creates the option next to edited and deleted Reddit comments/posts to show the original comment from before it was edited
- // @author Jonah Lawrence (DenverCoder1)
- // @grant none
- // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
- // @license MIT
- // @icon https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/master/images/logo512.png
- // @match https://*.reddit.com/
- // @match https://*.reddit.com/me/f/*
- // @match https://*.reddit.com/message/*
- // @match https://*.reddit.com/r/*
- // @match https://*.reddit.com/user/*
- // @exclude https://*.reddit.com/*/about/banned*
- // @exclude https://*.reddit.com/*/about/contributors*
- // @exclude https://*.reddit.com/*/about/edit*
- // @exclude https://*.reddit.com/*/about/flair*
- // @exclude https://*.reddit.com/*/about/log*
- // @exclude https://*.reddit.com/*/about/moderators*
- // @exclude https://*.reddit.com/*/about/muted*
- // @exclude https://*.reddit.com/*/about/rules*
- // @exclude https://*.reddit.com/*/about/stylesheet*
- // @exclude https://*.reddit.com/*/about/traffic*
- // @exclude https://*.reddit.com/*/wiki/*
- // @exclude https://mod.reddit.com/*
- // ==/UserScript==
- /* jshint esversion: 8 */
- (function () {
- "use strict";
- /**
- * The current version of the script
- * @type {string}
- */
- const VERSION = "3.17.4";
- /**
- * Whether or not we are on old reddit and not redesign.
- * This will be set in the "load" event listener.
- * @type {boolean}
- */
- let isOldReddit = false;
- /**
- * Whether or not we are on compact mode.
- * This will be set in the "load" event listener.
- * @type {boolean}
- */
- let isCompact = false;
- /**
- * Timeout to check for new edited comments on page.
- * This will be updated when scrolling.
- * @type {number?}
- */
- let scriptTimeout = null;
- /**
- * The element that is currently requesting content
- * @type {Element?}
- */
- let currentLoading = null;
- /**
- * List of submission ids of edited posts.
- * Used on Reddit redesign since the submissions are not marked as such.
- * This is set in the "load" event listener from the Reddit JSON API.
- * @type {Array<{id: string, edited: float}>}
- */
- let editedSubmissions = [];
- /**
- * The current URL that is being viewed.
- * On Redesign, this can change without the user leaving page,
- * so we want to look for new edited submissions if it changes.
- * @type {string}
- */
- let currentURL = window.location.href;
- /**
- * Showdown markdown converter
- * @type {showdown.Converter}
- */
- const mdConverter = new showdown.Converter({
- tables: true,
- simplifiedAutoLink: true,
- literalMidWordUnderscores: true,
- strikethrough: true,
- ghCodeBlocks: true,
- disableForced4SpacesIndentedSublists: true,
- });
- /**
- * Logging methods for displaying formatted logs in the console.
- *
- * logging.info("This is an info message");
- * logging.warn("This is a warning message");
- * logging.error("This is an error message");
- * logging.table({a: 1, b: 2, c: 3});
- */
- const logging = {
- INFO: "info",
- WARN: "warn",
- ERROR: "error",
- TABLE: "table",
- /**
- * Log a message to the console
- * @param {string} level The console method to use e.g. "log", "info", "warn", "error", "table"
- * @param {...string} messages - Any number of messages to log
- */
- _format_log(level, ...messages) {
- const logger = level in console ? console[level] : console.log;
- logger(`%c[unedit-for-reddit] %c[${level.toUpperCase()}]`, "color: #00b6b6", "color: #888800", ...messages);
- },
- /**
- * Log an info message to the console
- * @param {...string} messages - Any number of messages to log
- */
- info(...messages) {
- logging._format_log(this.INFO, ...messages);
- },
- /**
- * Log a warning message to the console
- * @param {...string} messages - Any number of messages to log
- */
- warn(...messages) {
- logging._format_log(this.WARN, ...messages);
- },
- /**
- * Log an error message to the console
- * @param {...string} messages - Any number of messages to log
- */
- error(...messages) {
- logging._format_log(this.ERROR, ...messages);
- },
- /**
- * Log a table to the console
- * @param {Object} data - The table to log
- */
- table(data) {
- logging._format_log(this.TABLE, data);
- },
- };
- /**
- * Storage methods for saving and retrieving data from local storage.
- *
- * Use the storage API or chrome.storage API if available, otherwise use localStorage.
- *
- * storage.get("key").then((value) => { ... });
- * storage.get("key", "default value").then((value) => { ... });
- * storage.set("key", "value").then(() => { ... });
- */
- const storage = {
- /**
- * Get a value from storage
- * @param {string} key - The key to retrieve
- * @param {string?} defaultValue - The default value to return if the key does not exist
- * @returns {Promise<string>} A promise that resolves with the value
- */
- get(key, defaultValue = null) {
- // retrieve from storage API
- if (storage._isBrowserStorageAvailable()) {
- logging.info(`Retrieving '${key}' from browser.storage.local`);
- return browser.storage.local.get(key).then((result) => {
- return result[key] || localStorage.getItem(key) || defaultValue;
- });
- } else if (storage._isChromeStorageAvailable()) {
- logging.info(`Retrieving '${key}' from chrome.storage.local`);
- return new Promise((resolve) => {
- chrome.storage.local.get(key, (result) => {
- resolve(result[key] || localStorage.getItem(key) || defaultValue);
- });
- });
- } else {
- logging.info(`Retrieving '${key}' from localStorage`);
- return Promise.resolve(localStorage.getItem(key) || defaultValue);
- }
- },
- /**
- * Set a value in storage
- * @param {string} key - The key to set
- * @param {string} value - The value to set
- * @returns {Promise<void>} A promise that resolves when the value is set
- */
- set(key, value) {
- if (storage._isBrowserStorageAvailable()) {
- logging.info(`Storing '${key}' in browser.storage.local`);
- return browser.storage.local.set({ [key]: value });
- } else if (storage._isChromeStorageAvailable()) {
- logging.info(`Storing '${key}' in chrome.storage.local`);
- return new Promise((resolve) => {
- chrome.storage.local.set({ [key]: value }, resolve);
- });
- } else {
- logging.info(`Storing '${key}' in localStorage`);
- return Promise.resolve(localStorage.setItem(key, value));
- }
- },
- /**
- * Return whether browser.storage is available
- * @returns {boolean} Whether browser.storage is available
- */
- _isBrowserStorageAvailable() {
- return typeof browser !== "undefined" && browser.storage;
- },
- /**
- * Return whether chrome.storage is available
- * @returns {boolean} Whether chrome.storage is available
- */
- _isChromeStorageAvailable() {
- return typeof chrome !== "undefined" && chrome.storage;
- },
- };
- /**
- * Parse the URL for the submission ID and comment ID if it exists.
- * @returns {{submissionId: string|null, commentId: string|null}}
- */
- function parseURL() {
- const match = window.location.href.match(/\/comments\/([A-Za-z0-9]+)\/(?:.*?\/([A-Za-z0-9]+))?/);
- return {
- submissionId: (match && match[1]) || null,
- commentId: (match && match[2]) || null,
- };
- }
- /**
- * Find the ID of a comment or submission.
- * @param {Element} innerEl An element inside the comment.
- * @returns {string} The Reddit ID of the comment.
- */
- function getPostId(innerEl) {
- let postId = "";
- // redesign
- if (!isOldReddit) {
- const post = innerEl?.closest("[class*='t1_'], [class*='t3_']");
- if (post) {
- postId = Array.from(post.classList).filter(function (el) {
- return el.indexOf("t1_") > -1 || el.indexOf("t3_") > -1;
- })[0];
- } else {
- // if post not found, try to find the post id in the URL
- const parsedURL = parseURL();
- postId = parsedURL.commentId || parsedURL.submissionId || postId;
- }
- }
- // old reddit
- else if (!isCompact) {
- // old reddit comment
- postId = innerEl?.closest(".thing")?.id.replace("thing_", "");
- // old reddit submission
- if (!postId && isInSubmission(innerEl)) {
- const match = window.location.href.match(/comments\/([A-Za-z0-9]{5,8})\//);
- postId = match ? match[1] : null;
- // submission in list view
- if (!postId) {
- const thing = innerEl.closest(".thing");
- postId = thing?.id.replace("thing_", "");
- }
- }
- // if still not found, check for the .reportform element
- if (!postId) {
- postId = innerEl?.closest(".entry")?.querySelector(".reportform")?.className.replace(/.*t1/, "t1");
- }
- // if still not found check the url
- if (!postId) {
- const parsedURL = parseURL();
- postId = parsedURL.commentId || parsedURL.submissionId || postId;
- }
- // otherwise log an error
- if (!postId) {
- logging.error("Could not find post id", innerEl);
- postId = "";
- }
- }
- // compact
- else {
- const thing = innerEl?.closest(".thing");
- if (thing) {
- const idClass = [...thing.classList].find((c) => c.startsWith("id-"));
- postId = idClass ? idClass.replace("id-", "") : "";
- }
- // if not found, check the url
- if (!postId) {
- const parsedURL = parseURL();
- postId = parsedURL.commentId || parsedURL.submissionId || postId;
- }
- }
- // if the post appears on the page after the last 3 characters are removed, remove them
- const reMatch = postId.match(/(t1_\w+)\w{3}/) || postId.match(/(t3_\w+)\w{3}/);
- if (reMatch && document.querySelector(`.${reMatch[1]}, #thing_${reMatch[1]}`)) {
- postId = reMatch[1];
- }
- return postId;
- }
- /**
- * Get the container of the comment or submission body for appending the original comment to.
- * @param {string} postId The ID of the comment or submission
- * @returns {Element} The container element of the comment or submission body.
- */
- function getPostBodyElement(postId) {
- let bodyEl = null,
- baseEl = null;
- // redesign
- if (!isOldReddit) {
- baseEl = document.querySelector(`#${postId}, .Comment.${postId}`);
- // in post preview popups, the id will appear again but in #overlayScrollContainer
- const popupEl = document.querySelector(`#overlayScrollContainer .Post.${postId}`);
- baseEl = popupEl ? popupEl : baseEl;
- if (baseEl) {
- if (baseEl.getElementsByClassName("RichTextJSON-root").length > 0) {
- bodyEl = baseEl.getElementsByClassName("RichTextJSON-root")[0];
- } else if (isInSubmission(baseEl) && baseEl?.firstElementChild?.lastElementChild) {
- const classicBodyEl = baseEl.querySelector(`div[data-adclicklocation="background"]`);
- if (classicBodyEl) {
- bodyEl = classicBodyEl;
- } else {
- bodyEl = baseEl.firstElementChild.lastElementChild;
- if (bodyEl.childNodes.length === 1) {
- bodyEl = bodyEl.firstElementChild;
- }
- }
- } else {
- bodyEl = baseEl;
- }
- } else {
- // check for a paragraph with the text "That Comment Is Missing"
- const missingCommentEl = document.querySelectorAll(`div > div > svg:first-child + p`);
- [...missingCommentEl].some(function (el) {
- if (el.innerText === "That Comment Is Missing") {
- bodyEl = el.parentElement;
- return true;
- }
- });
- }
- }
- // old reddit
- else if (!isCompact) {
- // old reddit comments
- baseEl = document.querySelector(`form[id*='${postId}'] .md`);
- if (baseEl?.closest(".entry")) {
- bodyEl = baseEl;
- } else {
- baseEl = document.querySelector(".report-" + postId);
- bodyEl = baseEl
- ? baseEl.closest(".entry").querySelector(".usertext")
- : document.querySelector("p#noresults");
- }
- // old reddit submissions
- if (!bodyEl) {
- bodyEl =
- document.querySelector("div[data-url] .entry form .md") ||
- document.querySelector("div[data-url] .entry form .usertext-body") ||
- document.querySelector("div[data-url] .entry .top-matter");
- }
- // link view
- if (!bodyEl) {
- bodyEl = document.querySelector(`.id-${postId}`);
- }
- }
- // compact view
- else {
- bodyEl = document.querySelector(`.id-${postId} .md, .id-${postId} form.usertext`);
- // if not found, check for the .usertext element containing it as part of its id
- if (!bodyEl) {
- bodyEl = document.querySelector(".showOriginal")?.parentElement;
- }
- }
- return bodyEl;
- }
- /**
- * Check if surrounding elements imply element is in a selftext submission.
- * @param {Element} innerEl An element inside the post to check.
- * @returns {boolean} Whether or not the element is in a selftext submission
- */
- function isInSubmission(innerEl) {
- const selectors = [
- "a.thumbnail", // old reddit on profile page or list view
- "div[data-url]", // old reddit on submission page
- ".Post", // redesign
- ];
- // class list of .thing contains id-t3_...
- const thing = innerEl?.closest(".thing");
- if (thing) {
- const idClass = [...thing.classList].find((c) => c.startsWith("id-"));
- if (idClass) {
- return idClass.startsWith("id-t3_");
- }
- }
- return Boolean(innerEl.closest(selectors.join(", ")));
- }
- /**
- * Check if the element bounds are within the window bounds.
- * @param {Element} element The element to check
- * @returns {boolean} Whether or not the element is within the window
- */
- function isInViewport(element) {
- const rect = element.getBoundingClientRect();
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
- rect.right <= (window.innerWidth || document.documentElement.clientWidth)
- );
- }
- /**
- * Generate HTML from markdown for a comment or submission.
- * @param {string} postType The type of post - "comment" or "post" (submission)
- * @param {string} original The markdown to convert
- * @returns {string} The HTML of the markdown
- */
- function redditPostToHTML(postType, original) {
- // fix Reddit tables to have at least two dashes per cell in the alignment row
- let body = original.replace(/(?<=^\s*|\|\s*)(:?)-(:?)(?=\s*\|[-|\s:]*$)/gm, "$1--$2");
- // convert superscripts in the form "^(some text)" or "^text" to <sup>text</sup>
- const multiwordSuperscriptRegex = /\^\((.+?)\)/gm;
- while (multiwordSuperscriptRegex.test(body)) {
- body = body.replace(multiwordSuperscriptRegex, "<sup>$1</sup>");
- }
- const superscriptRegex = /\^(\S+)/gm;
- while (superscriptRegex.test(body)) {
- body = body.replace(superscriptRegex, "<sup>$1</sup>");
- }
- // convert user and subreddit mentions to links (can be /u/, /r/, u/, or r/)
- body = body.replace(/(?<=^|[^\w\/])(\/?)([ur]\/\w+)/gm, "[$1$2](/$2)");
- // add spaces after '>' to keep blockquotes (if it has '>!' ignore since that is spoilertext)
- body = body.replace(/^((?:>|>)+)(?=[^!\s])/gm, function (match, p1) {
- return p1.replace(/>/g, ">") + " ";
- });
- // convert markdown to HTML
- let html = mdConverter.makeHtml("\n\n### Original " + postType + ":\n\n" + body);
- // convert Reddit spoilertext
- html = html.replace(
- /(?<=^|\s|>)>!(.+?)!<(?=$|\s|<)/gm,
- "<span class='md-spoiler-text' title='Reveal spoiler'>$1</span>"
- );
- // replace ​ with a zero-width space
- return html.replace(/&#x200B;/g, "\u200B");
- }
- /**
- * Create a new paragraph containing the body of the original comment/post.
- * @param {Element} commentBodyElement The container element of the comment/post body.
- * @param {string} postType The type of post - "comment" or "post" (submission)
- * @param {object} postData The archived data of the original comment/post.
- * @param {Boolean} includeBody Whether or not to include the body of the original comment/post.
- */
- function showOriginalComment(commentBodyElement, postType, postData, includeBody) {
- const originalBody = typeof postData?.body === "string" ? postData.body : postData?.selftext;
- // create paragraph element
- const origBodyEl = document.createElement("p");
- origBodyEl.className = "og";
- // set text
- origBodyEl.innerHTML = includeBody ? redditPostToHTML(postType, originalBody) : "";
- // author and date details
- const detailsEl = document.createElement("div");
- detailsEl.style.fontSize = "12px";
- detailsEl.appendChild(document.createTextNode("Posted by "));
- const authorEl = document.createElement("a");
- authorEl.href = `/user/${postData.author}`;
- authorEl.innerText = postData.author;
- detailsEl.appendChild(authorEl);
- detailsEl.appendChild(document.createTextNode(" · "));
- const dateEl = document.createElement("a");
- dateEl.href = postData.permalink;
- dateEl.title = new Date(postData.created_utc * 1000).toString();
- dateEl.innerText = getRelativeTime(postData.created_utc);
- detailsEl.appendChild(dateEl);
- // append horizontal rule if the original body is shown
- if (includeBody) {
- origBodyEl.appendChild(document.createElement("hr"));
- }
- // append to original comment
- origBodyEl.appendChild(detailsEl);
- const existingOg = commentBodyElement.querySelector(".og");
- if (existingOg && includeBody) {
- // if there is an existing paragraph and this element contains the body, replace it
- existingOg.replaceWith(origBodyEl);
- } else if (!existingOg) {
- // if there is no existing paragraph, append it
- commentBodyElement.appendChild(origBodyEl);
- }
- // scroll into view
- setTimeout(function () {
- if (!isInViewport(origBodyEl)) {
- origBodyEl.scrollIntoView({ behavior: "smooth" });
- }
- }, 500);
- // Redesign
- if (!isOldReddit) {
- // Make sure collapsed submission previews are expanded to not hide the original comment.
- commentBodyElement.parentElement.style.maxHeight = "unset";
- }
- // Old reddit
- else {
- // If the comment is collapsed, expand it so the original comment is visible
- expandComment(commentBodyElement);
- }
- }
- /**
- * Expand comment if it is collapsed (on old reddit only).
- * @param {Element} innerEl An element inside the comment.
- */
- function expandComment(innerEl) {
- const collapsedComment = innerEl.closest(".collapsed");
- if (collapsedComment) {
- collapsedComment.classList.remove("collapsed");
- collapsedComment.classList.add("noncollapsed");
- }
- }
- /**
- * Handle show original event given the post to show content for.
- * @param {Element} linkEl The link element for showing the status.
- * @param {object} out The response from the API.
- * @param {object} post The archived data of the original comment/post.
- * @param {string} postId The ID of the original comment/post.
- * @param {Boolean} includeBody Whether or not to include the body of the original comment/post.
- */
- function handleShowOriginalEvent(linkEl, out, post, postId, includeBody) {
- // locate comment body
- const commentBodyElement = getPostBodyElement(postId);
- // check that comment was fetched and body element exists
- if (!commentBodyElement) {
- // the comment body element was not found
- linkEl.innerText = "body element not found";
- linkEl.title = "Please report this issue to the developer on GitHub.";
- logging.error("Body element not found:", out);
- } else if (typeof post?.body === "string") {
- // create new paragraph containing the body of the original comment
- showOriginalComment(commentBodyElement, "comment", post, includeBody);
- // remove loading status from comment
- linkEl.innerText = "";
- linkEl.removeAttribute("title");
- logging.info("Successfully loaded comment.");
- } else if (typeof post?.selftext === "string") {
- // check if result has selftext instead of body (it is a submission post)
- // create new paragraph containing the selftext of the original submission
- showOriginalComment(commentBodyElement, "post", post, includeBody);
- // remove loading status from post
- linkEl.innerText = "";
- linkEl.removeAttribute("title");
- logging.info("Successfully loaded post.");
- } else if (out?.data?.length === 0) {
- // data was returned empty
- linkEl.innerText = "not found";
- linkEl.title = "No matching results were found in the Pushshift archive.";
- logging.warn("No results:", out);
- } else if (out?.data?.length > 0) {
- // no matching comment/post was found in the data
- linkEl.innerText = "not found";
- linkEl.title = "The comment/post was not found in the Pushshift archive.";
- logging.warn("No matching post:", out);
- } else {
- // other issue occurred with displaying comment
- if (linkEl.innerText === "fetch failed") {
- const errorLink = linkEl.parentElement.querySelector(".error-link");
- const linkToPushshift = errorLink || document.createElement("a");
- linkToPushshift.target = "_blank";
- linkToPushshift.style = `text-decoration: underline;
- cursor: pointer;
- margin-left: 6px;
- font-style: normal;
- font-weight: bold;
- color: #e5766e;`;
- linkToPushshift.className = linkEl.className;
- linkToPushshift.classList.add("error-link");
- linkToPushshift.href = out?.detail
- ? "https://api.pushshift.io/signup"
- : "https://www.reddit.com/r/pushshift/";
- linkToPushshift.innerText = out?.detail || "CHECK r/PUSHSHIFT FOR MORE INFO";
- if (errorLink === null) {
- linkEl.parentElement.appendChild(linkToPushshift);
- }
- // unhide token container if token is missing or invalid
- if (out?.detail) {
- const tokenContainer = document.querySelector("#tokenContainer");
- tokenContainer.style.display = "block";
- storage.set("hideTokenContainer", "false");
- }
- }
- linkEl.innerText = "fetch failed";
- linkEl.title = "A Pushshift error occurred. Please check r/pushshift for updates.";
- logging.error("Fetch failed:", out);
- }
- }
- /**
- * Fetch alternative that runs fetch from the window context using a helper element.
- *
- * This is necessary because in Firefox the headers are not sent when running fetch from the addon context.
- *
- * @param {string} url The URL to fetch.
- * @param {object} options The options to pass to fetch.
- * @returns {Promise} The fetch promise.
- */
- function inlineFetch(url, options) {
- const outputContainer = document.createElement("div");
- outputContainer.id = "outputContainer" + Math.floor(Math.random() * Math.pow(10, 10));
- outputContainer.style.display = "none";
- document.body.appendChild(outputContainer);
- const responseContainer = document.createElement("div");
- responseContainer.id = "responseContainer" + Math.floor(Math.random() * Math.pow(10, 10));
- responseContainer.style.display = "none";
- document.body.appendChild(responseContainer);
- const temp = document.createElement("button");
- temp.setAttribute("type", "button");
- temp.setAttribute(
- "onclick",
- `fetch("${url}", ${JSON.stringify(options)})
- .then(r => {
- document.querySelector("#${responseContainer.id}").innerText = JSON.stringify({
- ok: r.ok,
- status: r.status,
- statusText: r.statusText,
- headers: Object.fromEntries(r.headers.entries()),
- });
- return r.text();
- })
- .then(t => document.querySelector("#${outputContainer.id}").innerText = t)`
- );
- temp.style.display = "none";
- document.body.appendChild(temp);
- temp.click();
- // wait for fetch to complete and return a promise
- return new Promise((resolve) => {
- const interval = setInterval(() => {
- if (outputContainer.innerText && responseContainer.innerText) {
- clearInterval(interval);
- const responseData = JSON.parse(responseContainer.innerText);
- const mockResponse = {
- text: () => outputContainer.innerText,
- json: () => JSON.parse(outputContainer.innerText),
- ok: responseData.ok,
- status: responseData.status,
- statusText: responseData.statusText,
- headers: {
- get: (header) => responseData.headers[header],
- },
- };
- resolve(mockResponse);
- outputContainer.remove();
- responseContainer.remove();
- temp.remove();
- }
- }, 100);
- });
- }
- /**
- * Create a link to view the original comment/post.
- * @param {Element} innerEl An element inside the comment or post to create a link for.
- */
- function createLink(innerEl) {
- // if there is already a link, don't create another unless the other was a show author link
- if (innerEl.parentElement.querySelector("a.showOriginal:not(.showAuthorOnly)")) {
- return;
- }
- // create link to "Show orginal" or "Show author"
- const showAuthor = innerEl.classList.contains("showAuthorOnly");
- const showLinkEl = document.createElement("a");
- showLinkEl.innerText = showAuthor ? "Show author" : "Show original";
- showLinkEl.className = innerEl.className + " showOriginal";
- showLinkEl.classList.remove("error");
- showLinkEl.style.textDecoration = "underline";
- showLinkEl.style.cursor = "pointer";
- showLinkEl.style.marginLeft = "6px";
- showLinkEl.title = "Click to show data from the original post or comment";
- innerEl.parentElement.appendChild(showLinkEl);
- innerEl.classList.add("match");
- // find id of selected comment or submission
- const postId = getPostId(showLinkEl);
- showLinkEl.alt = `View original post for ID ${postId}`;
- if (!postId) {
- showLinkEl.parentElement.removeChild(showLinkEl);
- }
- // click event
- showLinkEl.addEventListener(
- "click",
- async function () {
- // allow only 1 request at a time
- if (typeof currentLoading != "undefined" && currentLoading !== null) {
- return;
- }
- // create url for getting comment/post from pushshift api
- const URLs = [];
- const idURL = isInSubmission(this)
- ? `https://api.pushshift.io/reddit/search/submission/?ids=${postId}&fields=selftext,author,id,created_utc,permalink`
- : `https://api.pushshift.io/reddit/search/comment/?ids=${postId}&fields=body,author,id,link_id,created_utc,permalink`;
- URLs.push(idURL);
- // create url for getting author comments/posts from pushshift api
- const author = this.parentElement.querySelector("a[href*=user]")?.innerText;
- if (author) {
- const authorURL = isInSubmission(this)
- ? `https://api.pushshift.io/reddit/search/submission/?author=${author}&size=200&fields=selftext,author,id,created_utc,permalink`
- : `https://api.pushshift.io/reddit/search/comment/?author=${author}&size=200&fields=body,author,id,link_id,created_utc,permalink`;
- URLs.push(authorURL);
- }
- // if the author is unknown, check the parent post as an alternative instead
- else if (!isInSubmission(this)) {
- const parsedURL = parseURL();
- if (parsedURL.submissionId) {
- const parentURL = `https://api.pushshift.io/reddit/comment/search?q=*&link_id=${parsedURL.submissionId}&size=200&fields=body,author,id,link_id,created_utc,permalink`;
- URLs.push(parentURL);
- }
- }
- // set loading status
- currentLoading = this;
- this.innerText = "loading...";
- this.title = "Loading data from the original post or comment";
- logging.info(`Fetching from ${URLs.join(" and ")}`);
- const token = document.querySelector("#apiToken").value;
- // request from pushshift api
- await Promise.all(
- URLs.map((url) =>
- fetch(url, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- "User-Agent": "Unedit and Undelete for Reddit",
- accept: "application/json",
- Authorization: `Bearer ${token}`,
- },
- })
- .then((response) => {
- if (!response.ok) {
- logging.error("Response not ok:", response);
- }
- try {
- return response.json();
- } catch (e) {
- throw Error(`Invalid JSON Response: ${response}`);
- }
- })
- .catch((error) => {
- logging.error("Error:", error);
- })
- )
- )
- .then((responses) => {
- responses.forEach((out) => {
- // locate the comment that was being loaded
- const loading = currentLoading;
- // exit if already found
- if (loading.innerText === "") {
- return;
- }
- const post = out?.data?.find((p) => p?.id === postId?.split("_").pop());
- logging.info("Response:", { author, id: postId, post, data: out?.data });
- const includeBody = !loading.classList.contains("showAuthorOnly");
- handleShowOriginalEvent(loading, out, post, postId, includeBody);
- });
- })
- .catch(function (err) {
- throw err;
- });
- // reset status
- currentLoading = null;
- },
- false
- );
- }
- /**
- * Convert unix timestamp in seconds to a relative time string (e.g. "2 hours ago").
- * @param {number} timestamp A unix timestamp in seconds.
- * @returns {string} A relative time string.
- */
- function getRelativeTime(timestamp) {
- const time = new Date(timestamp * 1000);
- const now = new Date();
- const seconds = Math.round((now.getTime() - time.getTime()) / 1000);
- const minutes = Math.round(seconds / 60);
- const hours = Math.round(minutes / 60);
- const days = Math.round(hours / 24);
- const months = Math.round(days / 30.5);
- const years = Math.round(days / 365);
- if (years > 0 && months >= 12) {
- return `${years} ${years === 1 ? "year" : "years"} ago`;
- }
- if (months > 0 && days >= 30) {
- return `${months} ${months === 1 ? "month" : "months"} ago`;
- }
- if (days > 0 && hours >= 24) {
- return `${days} ${days === 1 ? "day" : "days"} ago`;
- }
- if (hours > 0 && minutes >= 60) {
- return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
- }
- if (minutes > 0 && seconds >= 60) {
- return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
- }
- return "just now";
- }
- /**
- * Locate comments and add links to each.
- */
- function findEditedComments() {
- // when function runs, cancel timeout
- if (scriptTimeout) {
- scriptTimeout = null;
- }
- // list elements to check for edited or deleted status
- let selectors = [],
- elementsToCheck = [],
- editedComments = [];
- // redesign
- if (!isOldReddit) {
- // check for edited/deleted comments and deleted submissions
- selectors = [
- ".Comment div:first-of-type span:not([data-text]):not(.found)", // Comments "edited..." or "Comment deleted/removed..."
- ".Post div div div:last-of-type div ~ div:last-of-type:not([data-text]):not(.found)", // Submissions "It doesn't appear in any feeds..." message
- ".Post > div:only-child > div:nth-of-type(5) > div:last-of-type > div:not([data-text]):only-child:not(.found)", // Submissions "Sorry, this post is no longer available." message
- ".Comment div.RichTextJSON-root > p:only-child:not([data-text]):not(.found)", // Comments "[unavailable]" message
- "div > div > svg:first-child + p:not(.found)", // "That Comment Is Missing" page
- ];
- elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", ")));
- editedComments = elementsToCheck.filter(function (el) {
- el.classList.add("found");
- // we only care about the element if it has no children
- if (el.children.length) {
- return false;
- }
- // there are only specific phrases we care about in a P element
- if (
- el.tagName === "P" &&
- el.innerText !== "[unavailable]" &&
- el.innerText !== "[ Removed by Reddit ]" &&
- el.innerText !== "That Comment Is Missing"
- ) {
- return false;
- }
- // include "[unavailable]" comments (blocked by user) if from a deleted user
- const isUnavailable =
- el.innerText === "[unavailable]" &&
- el?.parentElement?.parentElement?.parentElement
- ?.querySelector("div")
- ?.innerText?.includes("[deleted]");
- const isEditedOrRemoved =
- el.innerText.substring(0, 6) === "edited" || // include edited comments
- el.innerText.substring(0, 15) === "Comment deleted" || // include comments deleted by user
- el.innerText.substring(0, 15) === "Comment removed" || // include comments removed by moderator
- el.innerText.substring(0, 30) === "It doesn't appear in any feeds" || // include deleted submissions
- el.innerText.substring(0, 23) === "Moderators remove posts" || // include submissions removed by moderators
- isUnavailable || // include unavailable comments (blocked by user)
- el.innerText === "[ Removed by Reddit ]" || // include comments removed by Reddit
- el.innerText === "That Comment Is Missing" || // include comments not found in comment tree
- el.innerText.substring(0, 29) === "Sorry, this post is no longer"; // include unavailable submissions (blocked by user)
- const isDeletedAuthor = el.innerText === "[deleted]"; // include comments from deleted users
- // if the element has a deleted author, make a link to only show the deleted author
- if (isDeletedAuthor) {
- el.classList.add("showAuthorOnly");
- }
- // keep element if it is edited or removed or if it has a deleted author
- return isEditedOrRemoved || isDeletedAuthor;
- });
- // Edited submissions found using the Reddit API
- editedSubmissions.forEach((submission) => {
- let found = false;
- const postId = submission.id;
- const editedAt = submission.edited;
- const deletedAuthor = submission.deletedAuthor;
- const deletedPost = submission.deletedPost;
- selectors = [
- `#t3_${postId} > div:first-of-type > div:nth-of-type(2) > div:first-of-type > div:first-of-type > span:first-of-type:not(.found)`, // Submission page
- `#t3_${postId} > div:first-of-type > div:nth-of-type(2) > div:first-of-type > div:first-of-type > div:first-of-type > div:first-of-type > span:first-of-type:not(.found)`, // Comment context page
- `#t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:first-of-type > div:first-of-type:not(.found)`, // Subreddit listing view
- `.Post.t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:nth-of-type(2) > div:not([data-adclicklocation]):first-of-type:not(.found)`, // Profile/home/classic listing view
- `.Post.t3_${postId} > div:first-of-type > div[data-click-id="background"] > div:first-of-type > div[data-click-id="body"] > div[data-adclicklocation="top_bar"]:not(.found)`, // Compact listing view
- `.Post.t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:nth-of-type(2) div[data-adclicklocation="top_bar"]:not(.found)`, // Profile/home listing view
- `.Post.t3_${postId}:not(.scrollerItem) > div:first-of-type > div:nth-of-type(2) > div:nth-of-type(2) > div:first-of-type > div:first-of-type:not(.found)`, // Preview popup
- ];
- Array.from(document.querySelectorAll(selectors.join(", "))).forEach((el) => {
- // add found class so that it won't be checked again in the future
- el.classList.add("found");
- // if this is the first time we've found this post, add it to the list of posts to add the link to
- if (!found) {
- found = true;
- editedComments.push(el);
- if (editedAt) {
- if (!el.parentElement.querySelector(".edited-date")) {
- // display when the post was edited
- const editedDateElement = document.createElement("span");
- editedDateElement.classList.add("edited-date");
- editedDateElement.style.fontStyle = "italic";
- editedDateElement.innerText = ` \u00b7 edited ${getRelativeTime(editedAt)}`; // middle-dot = \u00b7
- el.parentElement.appendChild(editedDateElement);
- }
- } else if (deletedAuthor && !deletedPost) {
- // if the post was not edited, make a link to only show the deleted author
- el.classList.add("showAuthorOnly");
- }
- }
- });
- });
- // If the url has changed, check for edited submissions again
- // This is an async fetch that will check for edited submissions again when it is done
- if (currentURL !== window.location.href) {
- logging.info(`URL changed from ${currentURL} to ${window.location.href}`);
- currentURL = window.location.href;
- checkForEditedSubmissions();
- }
- }
- // old Reddit and compact Reddit
- else {
- selectors = [
- ".entry p.tagline time:not(.found)", // Comment or Submission "last edited" timestamp
- ".entry p.tagline em:not(.found), .entry .tagline span:first-of-type:not(.flair):not(.found)", // Comment "[deleted]" author
- "div[data-url] p.tagline span:first-of-type:not(.flair):not(.found)", // Submission "[deleted]" author
- "div[data-url] .usertext-body em:not(.found), form.usertext em:not(.found)", // Submission "[removed]" body
- ".entry .usertext .usertext-body > div.md > p:only-child:not(.found)", // Comment "[unavailable]" body
- "p#noresults", // "there doesn't seem to be anything here" page
- ];
- elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", ")));
- editedComments = elementsToCheck.filter(function (el) {
- el.classList.add("found");
- // The only messages we care about in a P element right now is "[unavailable]" or #noresults
- if (
- el.tagName === "P" &&
- el.innerText !== "[unavailable]" &&
- el.innerText !== "[ Removed by Reddit ]" &&
- el.id !== "noresults"
- ) {
- return false;
- }
- // include "[unavailable]" comments (blocked by user) if from a deleted user
- const isUnavailable =
- el.innerText === "[unavailable]" &&
- el?.closest(".entry").querySelector(".tagline").innerText.includes("[deleted]");
- const isEditedRemovedOrDeletedAuthor =
- el.title.substring(0, 11) === "last edited" || // include edited comments or submissions
- el.innerText === "[deleted]" || // include comments or submissions deleted by user
- el.innerText === "[removed]" || // include comments or submissions removed by moderator
- el.innerText === "[ Removed by Reddit ]" || // include comments or submissions removed by Reddit
- el.id === "noresults" || // include "there doesn't seem to be anything here" page
- isUnavailable; // include unavailable submissions (blocked by user)
- // if the element is a deleted author and not edited or removed, only show the deleted author
- if (
- el.innerText === "[deleted]" &&
- el.tagName.toUpperCase() === "SPAN" && // tag name is span (not em as it appears for deleted comments)
- ["[deleted]", "[removed]"].indexOf(el.closest(".entry")?.querySelector(".md")?.innerText) === -1 // content of post is not deleted or removed
- ) {
- el.classList.add("showAuthorOnly");
- }
- // keep element if it is edited or removed or if it has a deleted author
- return isEditedRemovedOrDeletedAuthor;
- });
- }
- // create links
- editedComments.forEach(function (el) {
- // for removed submissions, add the link to an element in the tagline instead of the body
- if (el.closest(".usertext-body") && el.innerText === "[removed]") {
- el = el.closest(".entry")?.querySelector("p.tagline span:first-of-type") || el;
- }
- createLink(el);
- });
- }
- /**
- * If the script timeout is not already set, set it and
- * run the findEditedComments in a second, otherwise do nothing.
- */
- function waitAndFindEditedComments() {
- if (!scriptTimeout) {
- scriptTimeout = setTimeout(findEditedComments, 1000);
- }
- }
- /**
- * Check for edited submissions using the Reddit JSON API.
- *
- * Since the Reddit Redesign website does not show if a submission was edited,
- * we will check the data in the Reddit JSON API for the information.
- */
- function checkForEditedSubmissions() {
- // don't need to check if we're not on a submission page or list view
- if (!document.querySelector(".Post, .ListingLayout-backgroundContainer")) {
- return;
- }
- // append .json to the page URL but before the ?
- const [url, query] = window.location.href.split("?");
- const jsonUrl = `${url}.json` + (query ? `?${query}` : "");
- logging.info(`Fetching additional info from ${jsonUrl}`);
- fetch(jsonUrl, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- "User-Agent": "Unedit and Undelete for Reddit",
- },
- })
- .then(function (response) {
- if (!response.ok) {
- throw new Error(`${response.status} ${response.statusText}`);
- }
- return response.json();
- })
- .then(function (data) {
- logging.info("Response:", data);
- const out = data?.length ? data[0] : data;
- const children = out?.data?.children;
- if (children) {
- editedSubmissions = children
- .filter(function (post) {
- return post.kind === "t3" && (post.data.edited || post.data.author === "[deleted]");
- })
- .map(function (post) {
- return {
- id: post.data.id,
- edited: post.data.edited,
- deletedAuthor: post.data.author === "[deleted]",
- deletedPost: post.data.selftext === "[deleted]" || post.data.selftext === "[removed]",
- };
- });
- logging.info("Edited submissions:", editedSubmissions);
- setTimeout(findEditedComments, 1000);
- }
- })
- .catch(function (error) {
- logging.error(`Error fetching additional info from ${jsonUrl}`, error);
- });
- }
- // check for new comments when you scroll
- window.addEventListener("scroll", waitAndFindEditedComments, true);
- // check for new comments when you click
- document.body.addEventListener("click", waitAndFindEditedComments, true);
- // add additional styling, find edited comments, and set old reddit status on page load
- function init() {
- // output the version number to the console
- logging.info(`Unedit and Undelete for Reddit v${VERSION}`);
- // determine if reddit is old or redesign
- isOldReddit = /old\.reddit/.test(window.location.href) || !!document.querySelector("#header-img");
- isCompact = document.querySelector("#header-img-a")?.href?.endsWith(".compact") || false;
- // upgrade insecure requests
- document.head.insertAdjacentHTML(
- "beforeend",
- `<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">`
- );
- // Reddit redesign
- if (!isOldReddit) {
- // fix styling of created paragraphs in new reddit
- document.head.insertAdjacentHTML(
- "beforeend",
- `<style>
- p.og {
- background: rgb(255, 245, 157) !important;
- color: black !important;
- opacity: 0.96;
- font-size: 14px;
- padding: 16px;
- line-height: 20px;
- border-radius: 4px;
- width: auto;
- width: -moz-available;
- width: -webkit-fill-available;
- }
- p.og pre {
- font-family: monospace;
- background: #fff59d;
- padding: 6px;
- margin: 6px 0;
- color: black;
- }
- p.og h1, p.og h2, p.og h3, p.og h4, p.og h5, p.og h6, p.og p, p.og div {
- margin: 1em 0 0.5em 0;
- }
- p.og h1 {
- font-size: 2em;
- }
- p.og h2 {
- font-size: 1.5em;
- }
- p.og>h3:first-child {
- font-weight: bold;
- margin-bottom: 0.5em;
- }
- p.og h3 {
- font-size: 1.17em;
- }
- p.og h4 {
- font-size: 1em;
- }
- p.og h5 {
- font-size: 0.83em;
- }
- p.og h6 {
- font-size: 0.67em;
- }
- p.og a {
- color: #3e88a0;
- text-decoration: underline;
- }
- p.og pre {
- background: #d7d085 !important;
- }
- p.og :not(pre)>code {
- font-family: monospace;
- background: #d7d085 !important;
- padding: 1px !important;
- }
- p.og summary {
- cursor: pointer;
- }
- p.og hr {
- border: none;
- border-bottom: 1px solid #666;
- background: transparent;
- }
- p.og table {
- border: 2px solid black;
- }
- p.og table td, p.og table th {
- border: 1px solid black;
- padding: 4px;
- }
- p.og sup {
- position: relative;
- font-size: .7em;
- line-height: .7em;
- top: -0.4em;
- }
- span.md-spoiler-text {
- background: #545452;
- border-radius: 2px;
- transition: background 1s ease-out;
- cursor: pointer;
- color: #545452;
- }
- span.md-spoiler-text.revealed {
- background: rgba(84,84,82,.1);
- color: inherit;
- }
- p.og em {
- font-style: italic;
- }
- p.og strong {
- font-weight: bold;
- }
- p.og blockquote {
- border-left: 4px solid #c5c1ad;
- padding: 0 8px;
- margin-left: 5px;
- margin-top: 0.35714285714285715em;
- margin-bottom: 0.35714285714285715em;
- }
- p.og ol {
- list-style: auto;
- margin-left: 1.5em;
- }
- p.og ul {
- list-style: initial;
- margin-left: 1.5em;
- }
- span.edited-date, a.showOriginal {
- font-size: small;
- }
- /* Add some space under the View all comments button on "That Comment Is Missing" page */
- div:first-child > div:first-child > svg + p + a[role="button"] {
- margin-bottom: 1em;
- }
- </style>`
- );
- // listen for spoilertext in original body to be revealed
- window.addEventListener(
- "click",
- function (e) {
- /**
- * @type {HTMLSpanElement}
- */
- const spoiler = e.target.closest("span.md-spoiler-text");
- if (spoiler) {
- spoiler.classList.add("revealed");
- spoiler.removeAttribute("title");
- spoiler.style.cursor = "auto";
- }
- },
- false
- );
- // check for edited submissions
- checkForEditedSubmissions();
- }
- // Old Reddit
- else {
- // fix styling of created paragraphs in old reddit
- document.head.insertAdjacentHTML(
- "beforeend",
- `<style>
- div p.og {
- background: rgb(255, 245, 157) !important;
- color: black !important;
- opacity: 0.96;
- font-size: 14px;
- padding: 16px;
- line-height: 20px;
- border-radius: 7px;
- }
- p.og p, p.og h1, p.og h2, p.og h3, p.og h4, p.og h5, p.og h6, p.og pre, p.og :not(pre)>code, p.og div {
- color: black !important;
- margin: 0.4em 0 0.2em 0;
- }
- p.og :not(pre)>code {
- background: #d7d085 !important;
- padding: 1px !important;
- }
- div p.og a {
- color: #0079d3 !important;
- }
- div p.og a:visited {
- color: #469ad8!important;
- }
- p.og table {
- border: 2px solid black;
- }
- p.og table td, p.og table th {
- border: 1px solid black;
- padding: 4px;
- }
- p.og table tr {
- background: none !important;
- }
- p.og strong {
- font-weight: 600;
- }
- p.og em {
- font-style: italic;
- }
- /* Override for RES Night mode */
- .res-nightmode .entry.res-selected .md-container > .md p.og,
- .res-nightmode .entry.res-selected .md-container > .md p.og p {
- color: black !important;
- }
- /* Override RES title text display */
- .res-betteReddit-showLastEditedTimestamp .edited-timestamp.showOriginal[title]::after {
- content: "";
- }
- </style>`
- );
- }
- // find edited comments
- findEditedComments();
- // create an input field in the bottom right corner of the screen for the api token
- document.head.insertAdjacentHTML(
- "beforeend",
- `<style>
- #apiToken {
- width: 300px;
- padding: 5px;
- }
- #requestTokenLink {
- color: white;
- border-radius: 3px;
- margin-left: 5px;
- }
- #saveButton {
- background: #2D3133;
- color: white;
- border-radius: 3px;
- padding: 5px;
- margin-left: 5px;
- border: none;
- cursor: pointer;
- }
- #saveButton:hover {
- background: #545452;
- }
- #saveButton:active {
- background: #2D3133;
- }
- #closeButton {
- margin-left: 5px;
- border-radius: 5px;
- color: rgb(255, 255, 255);
- padding: 5px;
- background: transparent;
- border: 0;
- cursor: pointer;
- }
- #tokenContainer {
- position: fixed;
- bottom: 0;
- right: 0;
- z-index: 999999999;
- padding: 6px;
- background: #CC3700;
- border-radius: 5px;
- }
- </style>`
- );
- const tokenInput = document.createElement("input");
- tokenInput.type = "text";
- tokenInput.id = "apiToken";
- tokenInput.placeholder = "Pushshift API Token";
- // if there is a token saved in local storage, use it
- storage.get("apiToken").then((token) => {
- if (token) {
- tokenInput.value = token;
- }
- });
- const requestTokenLink = document.createElement("a");
- requestTokenLink.href = "https://api.pushshift.io/signup";
- requestTokenLink.target = "_blank";
- requestTokenLink.rel = "noopener noreferrer";
- requestTokenLink.textContent = "Request Token";
- requestTokenLink.id = "requestTokenLink";
- const saveButton = document.createElement("button");
- saveButton.textContent = "Save";
- saveButton.id = "saveButton";
- saveButton.addEventListener("click", function () {
- // save in local storage
- storage.set("apiToken", tokenInput.value);
- });
- tokenInput.addEventListener("keydown", function (e) {
- if (e.key === "Enter") {
- saveButton.click();
- }
- });
- const closeButton = document.createElement("button");
- closeButton.textContent = "\u00D7"; // times symbol
- closeButton.id = "closeButton";
- const tokenContainer = document.createElement("div");
- tokenContainer.id = "tokenContainer";
- tokenContainer.appendChild(tokenInput);
- tokenContainer.appendChild(saveButton);
- tokenContainer.appendChild(requestTokenLink);
- tokenContainer.appendChild(closeButton);
- closeButton.addEventListener("click", function () {
- // set the token container to display none
- tokenContainer.style.display = "none";
- // save preference in local storage
- storage.set("hideTokenContainer", "true");
- });
- // if the user has hidden the token container before, hide it again
- storage.get("hideTokenContainer").then((hideTokenContainer) => {
- if (hideTokenContainer === "true") {
- tokenContainer.style.display = "none";
- }
- });
- document.body.appendChild(tokenContainer);
- // switch from fetch to inlineFetch if browser is Firefox
- if (navigator.userAgent.includes("Firefox")) {
- fetch = inlineFetch;
- }
- }
- // if the window is loaded, run init(), otherwise wait for it to load
- if (document.readyState === "complete") {
- init();
- } else {
- window.addEventListener("load", init, false);
- }
- })();