Unedit and Undelete for Reddit

Creates the option next to edited and deleted Reddit comments/posts to show the original comment from before it was edited

当前为 2023-06-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Unedit and Undelete for Reddit
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.17.4
  5. // @description Creates the option next to edited and deleted Reddit comments/posts to show the original comment from before it was edited
  6. // @author Jonah Lawrence (DenverCoder1)
  7. // @grant none
  8. // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
  9. // @license MIT
  10. // @icon https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/master/images/logo512.png
  11. // @match https://*.reddit.com/
  12. // @match https://*.reddit.com/me/f/*
  13. // @match https://*.reddit.com/message/*
  14. // @match https://*.reddit.com/r/*
  15. // @match https://*.reddit.com/user/*
  16. // @exclude https://*.reddit.com/*/about/banned*
  17. // @exclude https://*.reddit.com/*/about/contributors*
  18. // @exclude https://*.reddit.com/*/about/edit*
  19. // @exclude https://*.reddit.com/*/about/flair*
  20. // @exclude https://*.reddit.com/*/about/log*
  21. // @exclude https://*.reddit.com/*/about/moderators*
  22. // @exclude https://*.reddit.com/*/about/muted*
  23. // @exclude https://*.reddit.com/*/about/rules*
  24. // @exclude https://*.reddit.com/*/about/stylesheet*
  25. // @exclude https://*.reddit.com/*/about/traffic*
  26. // @exclude https://*.reddit.com/*/wiki/*
  27. // @exclude https://mod.reddit.com/*
  28. // ==/UserScript==
  29.  
  30. /* jshint esversion: 8 */
  31.  
  32. (function () {
  33. "use strict";
  34.  
  35. /**
  36. * The current version of the script
  37. * @type {string}
  38. */
  39. const VERSION = "3.17.4";
  40.  
  41. /**
  42. * Whether or not we are on old reddit and not redesign.
  43. * This will be set in the "load" event listener.
  44. * @type {boolean}
  45. */
  46. let isOldReddit = false;
  47.  
  48. /**
  49. * Whether or not we are on compact mode.
  50. * This will be set in the "load" event listener.
  51. * @type {boolean}
  52. */
  53. let isCompact = false;
  54.  
  55. /**
  56. * Timeout to check for new edited comments on page.
  57. * This will be updated when scrolling.
  58. * @type {number?}
  59. */
  60. let scriptTimeout = null;
  61.  
  62. /**
  63. * The element that is currently requesting content
  64. * @type {Element?}
  65. */
  66. let currentLoading = null;
  67.  
  68. /**
  69. * List of submission ids of edited posts.
  70. * Used on Reddit redesign since the submissions are not marked as such.
  71. * This is set in the "load" event listener from the Reddit JSON API.
  72. * @type {Array<{id: string, edited: float}>}
  73. */
  74. let editedSubmissions = [];
  75.  
  76. /**
  77. * The current URL that is being viewed.
  78. * On Redesign, this can change without the user leaving page,
  79. * so we want to look for new edited submissions if it changes.
  80. * @type {string}
  81. */
  82. let currentURL = window.location.href;
  83.  
  84. /**
  85. * Showdown markdown converter
  86. * @type {showdown.Converter}
  87. */
  88. const mdConverter = new showdown.Converter({
  89. tables: true,
  90. simplifiedAutoLink: true,
  91. literalMidWordUnderscores: true,
  92. strikethrough: true,
  93. ghCodeBlocks: true,
  94. disableForced4SpacesIndentedSublists: true,
  95. });
  96.  
  97. /**
  98. * Logging methods for displaying formatted logs in the console.
  99. *
  100. * logging.info("This is an info message");
  101. * logging.warn("This is a warning message");
  102. * logging.error("This is an error message");
  103. * logging.table({a: 1, b: 2, c: 3});
  104. */
  105. const logging = {
  106. INFO: "info",
  107. WARN: "warn",
  108. ERROR: "error",
  109. TABLE: "table",
  110.  
  111. /**
  112. * Log a message to the console
  113. * @param {string} level The console method to use e.g. "log", "info", "warn", "error", "table"
  114. * @param {...string} messages - Any number of messages to log
  115. */
  116. _format_log(level, ...messages) {
  117. const logger = level in console ? console[level] : console.log;
  118. logger(`%c[unedit-for-reddit] %c[${level.toUpperCase()}]`, "color: #00b6b6", "color: #888800", ...messages);
  119. },
  120.  
  121. /**
  122. * Log an info message to the console
  123. * @param {...string} messages - Any number of messages to log
  124. */
  125. info(...messages) {
  126. logging._format_log(this.INFO, ...messages);
  127. },
  128.  
  129. /**
  130. * Log a warning message to the console
  131. * @param {...string} messages - Any number of messages to log
  132. */
  133. warn(...messages) {
  134. logging._format_log(this.WARN, ...messages);
  135. },
  136.  
  137. /**
  138. * Log an error message to the console
  139. * @param {...string} messages - Any number of messages to log
  140. */
  141. error(...messages) {
  142. logging._format_log(this.ERROR, ...messages);
  143. },
  144.  
  145. /**
  146. * Log a table to the console
  147. * @param {Object} data - The table to log
  148. */
  149. table(data) {
  150. logging._format_log(this.TABLE, data);
  151. },
  152. };
  153.  
  154. /**
  155. * Storage methods for saving and retrieving data from local storage.
  156. *
  157. * Use the storage API or chrome.storage API if available, otherwise use localStorage.
  158. *
  159. * storage.get("key").then((value) => { ... });
  160. * storage.get("key", "default value").then((value) => { ... });
  161. * storage.set("key", "value").then(() => { ... });
  162. */
  163. const storage = {
  164. /**
  165. * Get a value from storage
  166. * @param {string} key - The key to retrieve
  167. * @param {string?} defaultValue - The default value to return if the key does not exist
  168. * @returns {Promise<string>} A promise that resolves with the value
  169. */
  170. get(key, defaultValue = null) {
  171. // retrieve from storage API
  172. if (storage._isBrowserStorageAvailable()) {
  173. logging.info(`Retrieving '${key}' from browser.storage.local`);
  174. return browser.storage.local.get(key).then((result) => {
  175. return result[key] || localStorage.getItem(key) || defaultValue;
  176. });
  177. } else if (storage._isChromeStorageAvailable()) {
  178. logging.info(`Retrieving '${key}' from chrome.storage.local`);
  179. return new Promise((resolve) => {
  180. chrome.storage.local.get(key, (result) => {
  181. resolve(result[key] || localStorage.getItem(key) || defaultValue);
  182. });
  183. });
  184. } else {
  185. logging.info(`Retrieving '${key}' from localStorage`);
  186. return Promise.resolve(localStorage.getItem(key) || defaultValue);
  187. }
  188. },
  189.  
  190. /**
  191. * Set a value in storage
  192. * @param {string} key - The key to set
  193. * @param {string} value - The value to set
  194. * @returns {Promise<void>} A promise that resolves when the value is set
  195. */
  196. set(key, value) {
  197. if (storage._isBrowserStorageAvailable()) {
  198. logging.info(`Storing '${key}' in browser.storage.local`);
  199. return browser.storage.local.set({ [key]: value });
  200. } else if (storage._isChromeStorageAvailable()) {
  201. logging.info(`Storing '${key}' in chrome.storage.local`);
  202. return new Promise((resolve) => {
  203. chrome.storage.local.set({ [key]: value }, resolve);
  204. });
  205. } else {
  206. logging.info(`Storing '${key}' in localStorage`);
  207. return Promise.resolve(localStorage.setItem(key, value));
  208. }
  209. },
  210.  
  211. /**
  212. * Return whether browser.storage is available
  213. * @returns {boolean} Whether browser.storage is available
  214. */
  215. _isBrowserStorageAvailable() {
  216. return typeof browser !== "undefined" && browser.storage;
  217. },
  218.  
  219. /**
  220. * Return whether chrome.storage is available
  221. * @returns {boolean} Whether chrome.storage is available
  222. */
  223. _isChromeStorageAvailable() {
  224. return typeof chrome !== "undefined" && chrome.storage;
  225. },
  226. };
  227.  
  228. /**
  229. * Parse the URL for the submission ID and comment ID if it exists.
  230. * @returns {{submissionId: string|null, commentId: string|null}}
  231. */
  232. function parseURL() {
  233. const match = window.location.href.match(/\/comments\/([A-Za-z0-9]+)\/(?:.*?\/([A-Za-z0-9]+))?/);
  234. return {
  235. submissionId: (match && match[1]) || null,
  236. commentId: (match && match[2]) || null,
  237. };
  238. }
  239.  
  240. /**
  241. * Find the ID of a comment or submission.
  242. * @param {Element} innerEl An element inside the comment.
  243. * @returns {string} The Reddit ID of the comment.
  244. */
  245. function getPostId(innerEl) {
  246. let postId = "";
  247. // redesign
  248. if (!isOldReddit) {
  249. const post = innerEl?.closest("[class*='t1_'], [class*='t3_']");
  250. if (post) {
  251. postId = Array.from(post.classList).filter(function (el) {
  252. return el.indexOf("t1_") > -1 || el.indexOf("t3_") > -1;
  253. })[0];
  254. } else {
  255. // if post not found, try to find the post id in the URL
  256. const parsedURL = parseURL();
  257. postId = parsedURL.commentId || parsedURL.submissionId || postId;
  258. }
  259. }
  260. // old reddit
  261. else if (!isCompact) {
  262. // old reddit comment
  263. postId = innerEl?.closest(".thing")?.id.replace("thing_", "");
  264. // old reddit submission
  265. if (!postId && isInSubmission(innerEl)) {
  266. const match = window.location.href.match(/comments\/([A-Za-z0-9]{5,8})\//);
  267. postId = match ? match[1] : null;
  268. // submission in list view
  269. if (!postId) {
  270. const thing = innerEl.closest(".thing");
  271. postId = thing?.id.replace("thing_", "");
  272. }
  273. }
  274. // if still not found, check for the .reportform element
  275. if (!postId) {
  276. postId = innerEl?.closest(".entry")?.querySelector(".reportform")?.className.replace(/.*t1/, "t1");
  277. }
  278. // if still not found check the url
  279. if (!postId) {
  280. const parsedURL = parseURL();
  281. postId = parsedURL.commentId || parsedURL.submissionId || postId;
  282. }
  283. // otherwise log an error
  284. if (!postId) {
  285. logging.error("Could not find post id", innerEl);
  286. postId = "";
  287. }
  288. }
  289. // compact
  290. else {
  291. const thing = innerEl?.closest(".thing");
  292. if (thing) {
  293. const idClass = [...thing.classList].find((c) => c.startsWith("id-"));
  294. postId = idClass ? idClass.replace("id-", "") : "";
  295. }
  296. // if not found, check the url
  297. if (!postId) {
  298. const parsedURL = parseURL();
  299. postId = parsedURL.commentId || parsedURL.submissionId || postId;
  300. }
  301. }
  302. // if the post appears on the page after the last 3 characters are removed, remove them
  303. const reMatch = postId.match(/(t1_\w+)\w{3}/) || postId.match(/(t3_\w+)\w{3}/);
  304. if (reMatch && document.querySelector(`.${reMatch[1]}, #thing_${reMatch[1]}`)) {
  305. postId = reMatch[1];
  306. }
  307. return postId;
  308. }
  309.  
  310. /**
  311. * Get the container of the comment or submission body for appending the original comment to.
  312. * @param {string} postId The ID of the comment or submission
  313. * @returns {Element} The container element of the comment or submission body.
  314. */
  315. function getPostBodyElement(postId) {
  316. let bodyEl = null,
  317. baseEl = null;
  318. // redesign
  319. if (!isOldReddit) {
  320. baseEl = document.querySelector(`#${postId}, .Comment.${postId}`);
  321. // in post preview popups, the id will appear again but in #overlayScrollContainer
  322. const popupEl = document.querySelector(`#overlayScrollContainer .Post.${postId}`);
  323. baseEl = popupEl ? popupEl : baseEl;
  324. if (baseEl) {
  325. if (baseEl.getElementsByClassName("RichTextJSON-root").length > 0) {
  326. bodyEl = baseEl.getElementsByClassName("RichTextJSON-root")[0];
  327. } else if (isInSubmission(baseEl) && baseEl?.firstElementChild?.lastElementChild) {
  328. const classicBodyEl = baseEl.querySelector(`div[data-adclicklocation="background"]`);
  329. if (classicBodyEl) {
  330. bodyEl = classicBodyEl;
  331. } else {
  332. bodyEl = baseEl.firstElementChild.lastElementChild;
  333. if (bodyEl.childNodes.length === 1) {
  334. bodyEl = bodyEl.firstElementChild;
  335. }
  336. }
  337. } else {
  338. bodyEl = baseEl;
  339. }
  340. } else {
  341. // check for a paragraph with the text "That Comment Is Missing"
  342. const missingCommentEl = document.querySelectorAll(`div > div > svg:first-child + p`);
  343. [...missingCommentEl].some(function (el) {
  344. if (el.innerText === "That Comment Is Missing") {
  345. bodyEl = el.parentElement;
  346. return true;
  347. }
  348. });
  349. }
  350. }
  351. // old reddit
  352. else if (!isCompact) {
  353. // old reddit comments
  354. baseEl = document.querySelector(`form[id*='${postId}'] .md`);
  355. if (baseEl?.closest(".entry")) {
  356. bodyEl = baseEl;
  357. } else {
  358. baseEl = document.querySelector(".report-" + postId);
  359. bodyEl = baseEl
  360. ? baseEl.closest(".entry").querySelector(".usertext")
  361. : document.querySelector("p#noresults");
  362. }
  363. // old reddit submissions
  364. if (!bodyEl) {
  365. bodyEl =
  366. document.querySelector("div[data-url] .entry form .md") ||
  367. document.querySelector("div[data-url] .entry form .usertext-body") ||
  368. document.querySelector("div[data-url] .entry .top-matter");
  369. }
  370. // link view
  371. if (!bodyEl) {
  372. bodyEl = document.querySelector(`.id-${postId}`);
  373. }
  374. }
  375. // compact view
  376. else {
  377. bodyEl = document.querySelector(`.id-${postId} .md, .id-${postId} form.usertext`);
  378. // if not found, check for the .usertext element containing it as part of its id
  379. if (!bodyEl) {
  380. bodyEl = document.querySelector(".showOriginal")?.parentElement;
  381. }
  382. }
  383. return bodyEl;
  384. }
  385.  
  386. /**
  387. * Check if surrounding elements imply element is in a selftext submission.
  388. * @param {Element} innerEl An element inside the post to check.
  389. * @returns {boolean} Whether or not the element is in a selftext submission
  390. */
  391. function isInSubmission(innerEl) {
  392. const selectors = [
  393. "a.thumbnail", // old reddit on profile page or list view
  394. "div[data-url]", // old reddit on submission page
  395. ".Post", // redesign
  396. ];
  397. // class list of .thing contains id-t3_...
  398. const thing = innerEl?.closest(".thing");
  399. if (thing) {
  400. const idClass = [...thing.classList].find((c) => c.startsWith("id-"));
  401. if (idClass) {
  402. return idClass.startsWith("id-t3_");
  403. }
  404. }
  405. return Boolean(innerEl.closest(selectors.join(", ")));
  406. }
  407.  
  408. /**
  409. * Check if the element bounds are within the window bounds.
  410. * @param {Element} element The element to check
  411. * @returns {boolean} Whether or not the element is within the window
  412. */
  413. function isInViewport(element) {
  414. const rect = element.getBoundingClientRect();
  415. return (
  416. rect.top >= 0 &&
  417. rect.left >= 0 &&
  418. rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  419. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  420. );
  421. }
  422.  
  423. /**
  424. * Generate HTML from markdown for a comment or submission.
  425. * @param {string} postType The type of post - "comment" or "post" (submission)
  426. * @param {string} original The markdown to convert
  427. * @returns {string} The HTML of the markdown
  428. */
  429. function redditPostToHTML(postType, original) {
  430. // fix Reddit tables to have at least two dashes per cell in the alignment row
  431. let body = original.replace(/(?<=^\s*|\|\s*)(:?)-(:?)(?=\s*\|[-|\s:]*$)/gm, "$1--$2");
  432. // convert superscripts in the form "^(some text)" or "^text" to <sup>text</sup>
  433. const multiwordSuperscriptRegex = /\^\((.+?)\)/gm;
  434. while (multiwordSuperscriptRegex.test(body)) {
  435. body = body.replace(multiwordSuperscriptRegex, "<sup>$1</sup>");
  436. }
  437. const superscriptRegex = /\^(\S+)/gm;
  438. while (superscriptRegex.test(body)) {
  439. body = body.replace(superscriptRegex, "<sup>$1</sup>");
  440. }
  441. // convert user and subreddit mentions to links (can be /u/, /r/, u/, or r/)
  442. body = body.replace(/(?<=^|[^\w\/])(\/?)([ur]\/\w+)/gm, "[$1$2](/$2)");
  443. // add spaces after '>' to keep blockquotes (if it has '>!' ignore since that is spoilertext)
  444. body = body.replace(/^((?:&gt;|>)+)(?=[^!\s])/gm, function (match, p1) {
  445. return p1.replace(/&gt;/g, ">") + " ";
  446. });
  447. // convert markdown to HTML
  448. let html = mdConverter.makeHtml("\n\n### Original " + postType + ":\n\n" + body);
  449. // convert Reddit spoilertext
  450. html = html.replace(
  451. /(?<=^|\s|>)&gt;!(.+?)!&lt;(?=$|\s|<)/gm,
  452. "<span class='md-spoiler-text' title='Reveal spoiler'>$1</span>"
  453. );
  454. // replace &#x200B; with a zero-width space
  455. return html.replace(/&amp;#x200B;/g, "\u200B");
  456. }
  457.  
  458. /**
  459. * Create a new paragraph containing the body of the original comment/post.
  460. * @param {Element} commentBodyElement The container element of the comment/post body.
  461. * @param {string} postType The type of post - "comment" or "post" (submission)
  462. * @param {object} postData The archived data of the original comment/post.
  463. * @param {Boolean} includeBody Whether or not to include the body of the original comment/post.
  464. */
  465. function showOriginalComment(commentBodyElement, postType, postData, includeBody) {
  466. const originalBody = typeof postData?.body === "string" ? postData.body : postData?.selftext;
  467. // create paragraph element
  468. const origBodyEl = document.createElement("p");
  469. origBodyEl.className = "og";
  470. // set text
  471. origBodyEl.innerHTML = includeBody ? redditPostToHTML(postType, originalBody) : "";
  472. // author and date details
  473. const detailsEl = document.createElement("div");
  474. detailsEl.style.fontSize = "12px";
  475. detailsEl.appendChild(document.createTextNode("Posted by "));
  476. const authorEl = document.createElement("a");
  477. authorEl.href = `/user/${postData.author}`;
  478. authorEl.innerText = postData.author;
  479. detailsEl.appendChild(authorEl);
  480. detailsEl.appendChild(document.createTextNode(" · "));
  481. const dateEl = document.createElement("a");
  482. dateEl.href = postData.permalink;
  483. dateEl.title = new Date(postData.created_utc * 1000).toString();
  484. dateEl.innerText = getRelativeTime(postData.created_utc);
  485. detailsEl.appendChild(dateEl);
  486. // append horizontal rule if the original body is shown
  487. if (includeBody) {
  488. origBodyEl.appendChild(document.createElement("hr"));
  489. }
  490. // append to original comment
  491. origBodyEl.appendChild(detailsEl);
  492. const existingOg = commentBodyElement.querySelector(".og");
  493. if (existingOg && includeBody) {
  494. // if there is an existing paragraph and this element contains the body, replace it
  495. existingOg.replaceWith(origBodyEl);
  496. } else if (!existingOg) {
  497. // if there is no existing paragraph, append it
  498. commentBodyElement.appendChild(origBodyEl);
  499. }
  500. // scroll into view
  501. setTimeout(function () {
  502. if (!isInViewport(origBodyEl)) {
  503. origBodyEl.scrollIntoView({ behavior: "smooth" });
  504. }
  505. }, 500);
  506. // Redesign
  507. if (!isOldReddit) {
  508. // Make sure collapsed submission previews are expanded to not hide the original comment.
  509. commentBodyElement.parentElement.style.maxHeight = "unset";
  510. }
  511. // Old reddit
  512. else {
  513. // If the comment is collapsed, expand it so the original comment is visible
  514. expandComment(commentBodyElement);
  515. }
  516. }
  517.  
  518. /**
  519. * Expand comment if it is collapsed (on old reddit only).
  520. * @param {Element} innerEl An element inside the comment.
  521. */
  522. function expandComment(innerEl) {
  523. const collapsedComment = innerEl.closest(".collapsed");
  524. if (collapsedComment) {
  525. collapsedComment.classList.remove("collapsed");
  526. collapsedComment.classList.add("noncollapsed");
  527. }
  528. }
  529.  
  530. /**
  531. * Handle show original event given the post to show content for.
  532. * @param {Element} linkEl The link element for showing the status.
  533. * @param {object} out The response from the API.
  534. * @param {object} post The archived data of the original comment/post.
  535. * @param {string} postId The ID of the original comment/post.
  536. * @param {Boolean} includeBody Whether or not to include the body of the original comment/post.
  537. */
  538. function handleShowOriginalEvent(linkEl, out, post, postId, includeBody) {
  539. // locate comment body
  540. const commentBodyElement = getPostBodyElement(postId);
  541. // check that comment was fetched and body element exists
  542. if (!commentBodyElement) {
  543. // the comment body element was not found
  544. linkEl.innerText = "body element not found";
  545. linkEl.title = "Please report this issue to the developer on GitHub.";
  546. logging.error("Body element not found:", out);
  547. } else if (typeof post?.body === "string") {
  548. // create new paragraph containing the body of the original comment
  549. showOriginalComment(commentBodyElement, "comment", post, includeBody);
  550. // remove loading status from comment
  551. linkEl.innerText = "";
  552. linkEl.removeAttribute("title");
  553. logging.info("Successfully loaded comment.");
  554. } else if (typeof post?.selftext === "string") {
  555. // check if result has selftext instead of body (it is a submission post)
  556. // create new paragraph containing the selftext of the original submission
  557. showOriginalComment(commentBodyElement, "post", post, includeBody);
  558. // remove loading status from post
  559. linkEl.innerText = "";
  560. linkEl.removeAttribute("title");
  561. logging.info("Successfully loaded post.");
  562. } else if (out?.data?.length === 0) {
  563. // data was returned empty
  564. linkEl.innerText = "not found";
  565. linkEl.title = "No matching results were found in the Pushshift archive.";
  566. logging.warn("No results:", out);
  567. } else if (out?.data?.length > 0) {
  568. // no matching comment/post was found in the data
  569. linkEl.innerText = "not found";
  570. linkEl.title = "The comment/post was not found in the Pushshift archive.";
  571. logging.warn("No matching post:", out);
  572. } else {
  573. // other issue occurred with displaying comment
  574. if (linkEl.innerText === "fetch failed") {
  575. const errorLink = linkEl.parentElement.querySelector(".error-link");
  576. const linkToPushshift = errorLink || document.createElement("a");
  577. linkToPushshift.target = "_blank";
  578. linkToPushshift.style = `text-decoration: underline;
  579. cursor: pointer;
  580. margin-left: 6px;
  581. font-style: normal;
  582. font-weight: bold;
  583. color: #e5766e;`;
  584. linkToPushshift.className = linkEl.className;
  585. linkToPushshift.classList.add("error-link");
  586. linkToPushshift.href = out?.detail
  587. ? "https://api.pushshift.io/signup"
  588. : "https://www.reddit.com/r/pushshift/";
  589. linkToPushshift.innerText = out?.detail || "CHECK r/PUSHSHIFT FOR MORE INFO";
  590. if (errorLink === null) {
  591. linkEl.parentElement.appendChild(linkToPushshift);
  592. }
  593. // unhide token container if token is missing or invalid
  594. if (out?.detail) {
  595. const tokenContainer = document.querySelector("#tokenContainer");
  596. tokenContainer.style.display = "block";
  597. storage.set("hideTokenContainer", "false");
  598. }
  599. }
  600. linkEl.innerText = "fetch failed";
  601. linkEl.title = "A Pushshift error occurred. Please check r/pushshift for updates.";
  602. logging.error("Fetch failed:", out);
  603. }
  604. }
  605.  
  606. /**
  607. * Fetch alternative that runs fetch from the window context using a helper element.
  608. *
  609. * This is necessary because in Firefox the headers are not sent when running fetch from the addon context.
  610. *
  611. * @param {string} url The URL to fetch.
  612. * @param {object} options The options to pass to fetch.
  613. * @returns {Promise} The fetch promise.
  614. */
  615. function inlineFetch(url, options) {
  616. const outputContainer = document.createElement("div");
  617. outputContainer.id = "outputContainer" + Math.floor(Math.random() * Math.pow(10, 10));
  618. outputContainer.style.display = "none";
  619. document.body.appendChild(outputContainer);
  620. const responseContainer = document.createElement("div");
  621. responseContainer.id = "responseContainer" + Math.floor(Math.random() * Math.pow(10, 10));
  622. responseContainer.style.display = "none";
  623. document.body.appendChild(responseContainer);
  624. const temp = document.createElement("button");
  625. temp.setAttribute("type", "button");
  626. temp.setAttribute(
  627. "onclick",
  628. `fetch("${url}", ${JSON.stringify(options)})
  629. .then(r => {
  630. document.querySelector("#${responseContainer.id}").innerText = JSON.stringify({
  631. ok: r.ok,
  632. status: r.status,
  633. statusText: r.statusText,
  634. headers: Object.fromEntries(r.headers.entries()),
  635. });
  636. return r.text();
  637. })
  638. .then(t => document.querySelector("#${outputContainer.id}").innerText = t)`
  639. );
  640. temp.style.display = "none";
  641. document.body.appendChild(temp);
  642. temp.click();
  643. // wait for fetch to complete and return a promise
  644. return new Promise((resolve) => {
  645. const interval = setInterval(() => {
  646. if (outputContainer.innerText && responseContainer.innerText) {
  647. clearInterval(interval);
  648. const responseData = JSON.parse(responseContainer.innerText);
  649. const mockResponse = {
  650. text: () => outputContainer.innerText,
  651. json: () => JSON.parse(outputContainer.innerText),
  652. ok: responseData.ok,
  653. status: responseData.status,
  654. statusText: responseData.statusText,
  655. headers: {
  656. get: (header) => responseData.headers[header],
  657. },
  658. };
  659. resolve(mockResponse);
  660. outputContainer.remove();
  661. responseContainer.remove();
  662. temp.remove();
  663. }
  664. }, 100);
  665. });
  666. }
  667.  
  668. /**
  669. * Create a link to view the original comment/post.
  670. * @param {Element} innerEl An element inside the comment or post to create a link for.
  671. */
  672. function createLink(innerEl) {
  673. // if there is already a link, don't create another unless the other was a show author link
  674. if (innerEl.parentElement.querySelector("a.showOriginal:not(.showAuthorOnly)")) {
  675. return;
  676. }
  677. // create link to "Show orginal" or "Show author"
  678. const showAuthor = innerEl.classList.contains("showAuthorOnly");
  679. const showLinkEl = document.createElement("a");
  680. showLinkEl.innerText = showAuthor ? "Show author" : "Show original";
  681. showLinkEl.className = innerEl.className + " showOriginal";
  682. showLinkEl.classList.remove("error");
  683. showLinkEl.style.textDecoration = "underline";
  684. showLinkEl.style.cursor = "pointer";
  685. showLinkEl.style.marginLeft = "6px";
  686. showLinkEl.title = "Click to show data from the original post or comment";
  687. innerEl.parentElement.appendChild(showLinkEl);
  688. innerEl.classList.add("match");
  689. // find id of selected comment or submission
  690. const postId = getPostId(showLinkEl);
  691. showLinkEl.alt = `View original post for ID ${postId}`;
  692. if (!postId) {
  693. showLinkEl.parentElement.removeChild(showLinkEl);
  694. }
  695. // click event
  696. showLinkEl.addEventListener(
  697. "click",
  698. async function () {
  699. // allow only 1 request at a time
  700. if (typeof currentLoading != "undefined" && currentLoading !== null) {
  701. return;
  702. }
  703. // create url for getting comment/post from pushshift api
  704. const URLs = [];
  705. const idURL = isInSubmission(this)
  706. ? `https://api.pushshift.io/reddit/search/submission/?ids=${postId}&fields=selftext,author,id,created_utc,permalink`
  707. : `https://api.pushshift.io/reddit/search/comment/?ids=${postId}&fields=body,author,id,link_id,created_utc,permalink`;
  708. URLs.push(idURL);
  709. // create url for getting author comments/posts from pushshift api
  710. const author = this.parentElement.querySelector("a[href*=user]")?.innerText;
  711. if (author) {
  712. const authorURL = isInSubmission(this)
  713. ? `https://api.pushshift.io/reddit/search/submission/?author=${author}&size=200&fields=selftext,author,id,created_utc,permalink`
  714. : `https://api.pushshift.io/reddit/search/comment/?author=${author}&size=200&fields=body,author,id,link_id,created_utc,permalink`;
  715. URLs.push(authorURL);
  716. }
  717. // if the author is unknown, check the parent post as an alternative instead
  718. else if (!isInSubmission(this)) {
  719. const parsedURL = parseURL();
  720. if (parsedURL.submissionId) {
  721. 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`;
  722. URLs.push(parentURL);
  723. }
  724. }
  725.  
  726. // set loading status
  727. currentLoading = this;
  728. this.innerText = "loading...";
  729. this.title = "Loading data from the original post or comment";
  730.  
  731. logging.info(`Fetching from ${URLs.join(" and ")}`);
  732.  
  733. const token = document.querySelector("#apiToken").value;
  734.  
  735. // request from pushshift api
  736. await Promise.all(
  737. URLs.map((url) =>
  738. fetch(url, {
  739. method: "GET",
  740. headers: {
  741. "Content-Type": "application/json",
  742. "User-Agent": "Unedit and Undelete for Reddit",
  743. accept: "application/json",
  744. Authorization: `Bearer ${token}`,
  745. },
  746. })
  747. .then((response) => {
  748. if (!response.ok) {
  749. logging.error("Response not ok:", response);
  750. }
  751. try {
  752. return response.json();
  753. } catch (e) {
  754. throw Error(`Invalid JSON Response: ${response}`);
  755. }
  756. })
  757. .catch((error) => {
  758. logging.error("Error:", error);
  759. })
  760. )
  761. )
  762. .then((responses) => {
  763. responses.forEach((out) => {
  764. // locate the comment that was being loaded
  765. const loading = currentLoading;
  766. // exit if already found
  767. if (loading.innerText === "") {
  768. return;
  769. }
  770. const post = out?.data?.find((p) => p?.id === postId?.split("_").pop());
  771. logging.info("Response:", { author, id: postId, post, data: out?.data });
  772. const includeBody = !loading.classList.contains("showAuthorOnly");
  773. handleShowOriginalEvent(loading, out, post, postId, includeBody);
  774. });
  775. })
  776. .catch(function (err) {
  777. throw err;
  778. });
  779.  
  780. // reset status
  781. currentLoading = null;
  782. },
  783. false
  784. );
  785. }
  786.  
  787. /**
  788. * Convert unix timestamp in seconds to a relative time string (e.g. "2 hours ago").
  789. * @param {number} timestamp A unix timestamp in seconds.
  790. * @returns {string} A relative time string.
  791. */
  792. function getRelativeTime(timestamp) {
  793. const time = new Date(timestamp * 1000);
  794. const now = new Date();
  795. const seconds = Math.round((now.getTime() - time.getTime()) / 1000);
  796. const minutes = Math.round(seconds / 60);
  797. const hours = Math.round(minutes / 60);
  798. const days = Math.round(hours / 24);
  799. const months = Math.round(days / 30.5);
  800. const years = Math.round(days / 365);
  801. if (years > 0 && months >= 12) {
  802. return `${years} ${years === 1 ? "year" : "years"} ago`;
  803. }
  804. if (months > 0 && days >= 30) {
  805. return `${months} ${months === 1 ? "month" : "months"} ago`;
  806. }
  807. if (days > 0 && hours >= 24) {
  808. return `${days} ${days === 1 ? "day" : "days"} ago`;
  809. }
  810. if (hours > 0 && minutes >= 60) {
  811. return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
  812. }
  813. if (minutes > 0 && seconds >= 60) {
  814. return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
  815. }
  816. return "just now";
  817. }
  818.  
  819. /**
  820. * Locate comments and add links to each.
  821. */
  822. function findEditedComments() {
  823. // when function runs, cancel timeout
  824. if (scriptTimeout) {
  825. scriptTimeout = null;
  826. }
  827. // list elements to check for edited or deleted status
  828. let selectors = [],
  829. elementsToCheck = [],
  830. editedComments = [];
  831. // redesign
  832. if (!isOldReddit) {
  833. // check for edited/deleted comments and deleted submissions
  834. selectors = [
  835. ".Comment div:first-of-type span:not([data-text]):not(.found)", // Comments "edited..." or "Comment deleted/removed..."
  836. ".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
  837. ".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
  838. ".Comment div.RichTextJSON-root > p:only-child:not([data-text]):not(.found)", // Comments "[unavailable]" message
  839. "div > div > svg:first-child + p:not(.found)", // "That Comment Is Missing" page
  840. ];
  841. elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", ")));
  842. editedComments = elementsToCheck.filter(function (el) {
  843. el.classList.add("found");
  844. // we only care about the element if it has no children
  845. if (el.children.length) {
  846. return false;
  847. }
  848. // there are only specific phrases we care about in a P element
  849. if (
  850. el.tagName === "P" &&
  851. el.innerText !== "[unavailable]" &&
  852. el.innerText !== "[ Removed by Reddit ]" &&
  853. el.innerText !== "That Comment Is Missing"
  854. ) {
  855. return false;
  856. }
  857. // include "[unavailable]" comments (blocked by user) if from a deleted user
  858. const isUnavailable =
  859. el.innerText === "[unavailable]" &&
  860. el?.parentElement?.parentElement?.parentElement
  861. ?.querySelector("div")
  862. ?.innerText?.includes("[deleted]");
  863. const isEditedOrRemoved =
  864. el.innerText.substring(0, 6) === "edited" || // include edited comments
  865. el.innerText.substring(0, 15) === "Comment deleted" || // include comments deleted by user
  866. el.innerText.substring(0, 15) === "Comment removed" || // include comments removed by moderator
  867. el.innerText.substring(0, 30) === "It doesn't appear in any feeds" || // include deleted submissions
  868. el.innerText.substring(0, 23) === "Moderators remove posts" || // include submissions removed by moderators
  869. isUnavailable || // include unavailable comments (blocked by user)
  870. el.innerText === "[ Removed by Reddit ]" || // include comments removed by Reddit
  871. el.innerText === "That Comment Is Missing" || // include comments not found in comment tree
  872. el.innerText.substring(0, 29) === "Sorry, this post is no longer"; // include unavailable submissions (blocked by user)
  873. const isDeletedAuthor = el.innerText === "[deleted]"; // include comments from deleted users
  874. // if the element has a deleted author, make a link to only show the deleted author
  875. if (isDeletedAuthor) {
  876. el.classList.add("showAuthorOnly");
  877. }
  878. // keep element if it is edited or removed or if it has a deleted author
  879. return isEditedOrRemoved || isDeletedAuthor;
  880. });
  881. // Edited submissions found using the Reddit API
  882. editedSubmissions.forEach((submission) => {
  883. let found = false;
  884. const postId = submission.id;
  885. const editedAt = submission.edited;
  886. const deletedAuthor = submission.deletedAuthor;
  887. const deletedPost = submission.deletedPost;
  888. selectors = [
  889. `#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
  890. `#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
  891. `#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
  892. `.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
  893. `.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
  894. `.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
  895. `.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
  896. ];
  897. Array.from(document.querySelectorAll(selectors.join(", "))).forEach((el) => {
  898. // add found class so that it won't be checked again in the future
  899. el.classList.add("found");
  900. // if this is the first time we've found this post, add it to the list of posts to add the link to
  901. if (!found) {
  902. found = true;
  903. editedComments.push(el);
  904. if (editedAt) {
  905. if (!el.parentElement.querySelector(".edited-date")) {
  906. // display when the post was edited
  907. const editedDateElement = document.createElement("span");
  908. editedDateElement.classList.add("edited-date");
  909. editedDateElement.style.fontStyle = "italic";
  910. editedDateElement.innerText = ` \u00b7 edited ${getRelativeTime(editedAt)}`; // middle-dot = \u00b7
  911. el.parentElement.appendChild(editedDateElement);
  912. }
  913. } else if (deletedAuthor && !deletedPost) {
  914. // if the post was not edited, make a link to only show the deleted author
  915. el.classList.add("showAuthorOnly");
  916. }
  917. }
  918. });
  919. });
  920. // If the url has changed, check for edited submissions again
  921. // This is an async fetch that will check for edited submissions again when it is done
  922. if (currentURL !== window.location.href) {
  923. logging.info(`URL changed from ${currentURL} to ${window.location.href}`);
  924. currentURL = window.location.href;
  925. checkForEditedSubmissions();
  926. }
  927. }
  928. // old Reddit and compact Reddit
  929. else {
  930. selectors = [
  931. ".entry p.tagline time:not(.found)", // Comment or Submission "last edited" timestamp
  932. ".entry p.tagline em:not(.found), .entry .tagline span:first-of-type:not(.flair):not(.found)", // Comment "[deleted]" author
  933. "div[data-url] p.tagline span:first-of-type:not(.flair):not(.found)", // Submission "[deleted]" author
  934. "div[data-url] .usertext-body em:not(.found), form.usertext em:not(.found)", // Submission "[removed]" body
  935. ".entry .usertext .usertext-body > div.md > p:only-child:not(.found)", // Comment "[unavailable]" body
  936. "p#noresults", // "there doesn't seem to be anything here" page
  937. ];
  938. elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", ")));
  939. editedComments = elementsToCheck.filter(function (el) {
  940. el.classList.add("found");
  941. // The only messages we care about in a P element right now is "[unavailable]" or #noresults
  942. if (
  943. el.tagName === "P" &&
  944. el.innerText !== "[unavailable]" &&
  945. el.innerText !== "[ Removed by Reddit ]" &&
  946. el.id !== "noresults"
  947. ) {
  948. return false;
  949. }
  950. // include "[unavailable]" comments (blocked by user) if from a deleted user
  951. const isUnavailable =
  952. el.innerText === "[unavailable]" &&
  953. el?.closest(".entry").querySelector(".tagline").innerText.includes("[deleted]");
  954. const isEditedRemovedOrDeletedAuthor =
  955. el.title.substring(0, 11) === "last edited" || // include edited comments or submissions
  956. el.innerText === "[deleted]" || // include comments or submissions deleted by user
  957. el.innerText === "[removed]" || // include comments or submissions removed by moderator
  958. el.innerText === "[ Removed by Reddit ]" || // include comments or submissions removed by Reddit
  959. el.id === "noresults" || // include "there doesn't seem to be anything here" page
  960. isUnavailable; // include unavailable submissions (blocked by user)
  961. // if the element is a deleted author and not edited or removed, only show the deleted author
  962. if (
  963. el.innerText === "[deleted]" &&
  964. el.tagName.toUpperCase() === "SPAN" && // tag name is span (not em as it appears for deleted comments)
  965. ["[deleted]", "[removed]"].indexOf(el.closest(".entry")?.querySelector(".md")?.innerText) === -1 // content of post is not deleted or removed
  966. ) {
  967. el.classList.add("showAuthorOnly");
  968. }
  969. // keep element if it is edited or removed or if it has a deleted author
  970. return isEditedRemovedOrDeletedAuthor;
  971. });
  972. }
  973. // create links
  974. editedComments.forEach(function (el) {
  975. // for removed submissions, add the link to an element in the tagline instead of the body
  976. if (el.closest(".usertext-body") && el.innerText === "[removed]") {
  977. el = el.closest(".entry")?.querySelector("p.tagline span:first-of-type") || el;
  978. }
  979. createLink(el);
  980. });
  981. }
  982.  
  983. /**
  984. * If the script timeout is not already set, set it and
  985. * run the findEditedComments in a second, otherwise do nothing.
  986. */
  987. function waitAndFindEditedComments() {
  988. if (!scriptTimeout) {
  989. scriptTimeout = setTimeout(findEditedComments, 1000);
  990. }
  991. }
  992.  
  993. /**
  994. * Check for edited submissions using the Reddit JSON API.
  995. *
  996. * Since the Reddit Redesign website does not show if a submission was edited,
  997. * we will check the data in the Reddit JSON API for the information.
  998. */
  999. function checkForEditedSubmissions() {
  1000. // don't need to check if we're not on a submission page or list view
  1001. if (!document.querySelector(".Post, .ListingLayout-backgroundContainer")) {
  1002. return;
  1003. }
  1004. // append .json to the page URL but before the ?
  1005. const [url, query] = window.location.href.split("?");
  1006. const jsonUrl = `${url}.json` + (query ? `?${query}` : "");
  1007. logging.info(`Fetching additional info from ${jsonUrl}`);
  1008. fetch(jsonUrl, {
  1009. method: "GET",
  1010. headers: {
  1011. "Content-Type": "application/json",
  1012. "User-Agent": "Unedit and Undelete for Reddit",
  1013. },
  1014. })
  1015. .then(function (response) {
  1016. if (!response.ok) {
  1017. throw new Error(`${response.status} ${response.statusText}`);
  1018. }
  1019. return response.json();
  1020. })
  1021. .then(function (data) {
  1022. logging.info("Response:", data);
  1023. const out = data?.length ? data[0] : data;
  1024. const children = out?.data?.children;
  1025. if (children) {
  1026. editedSubmissions = children
  1027. .filter(function (post) {
  1028. return post.kind === "t3" && (post.data.edited || post.data.author === "[deleted]");
  1029. })
  1030. .map(function (post) {
  1031. return {
  1032. id: post.data.id,
  1033. edited: post.data.edited,
  1034. deletedAuthor: post.data.author === "[deleted]",
  1035. deletedPost: post.data.selftext === "[deleted]" || post.data.selftext === "[removed]",
  1036. };
  1037. });
  1038. logging.info("Edited submissions:", editedSubmissions);
  1039. setTimeout(findEditedComments, 1000);
  1040. }
  1041. })
  1042. .catch(function (error) {
  1043. logging.error(`Error fetching additional info from ${jsonUrl}`, error);
  1044. });
  1045. }
  1046.  
  1047. // check for new comments when you scroll
  1048. window.addEventListener("scroll", waitAndFindEditedComments, true);
  1049.  
  1050. // check for new comments when you click
  1051. document.body.addEventListener("click", waitAndFindEditedComments, true);
  1052.  
  1053. // add additional styling, find edited comments, and set old reddit status on page load
  1054. function init() {
  1055. // output the version number to the console
  1056. logging.info(`Unedit and Undelete for Reddit v${VERSION}`);
  1057. // determine if reddit is old or redesign
  1058. isOldReddit = /old\.reddit/.test(window.location.href) || !!document.querySelector("#header-img");
  1059. isCompact = document.querySelector("#header-img-a")?.href?.endsWith(".compact") || false;
  1060. // upgrade insecure requests
  1061. document.head.insertAdjacentHTML(
  1062. "beforeend",
  1063. `<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">`
  1064. );
  1065. // Reddit redesign
  1066. if (!isOldReddit) {
  1067. // fix styling of created paragraphs in new reddit
  1068. document.head.insertAdjacentHTML(
  1069. "beforeend",
  1070. `<style>
  1071. p.og {
  1072. background: rgb(255, 245, 157) !important;
  1073. color: black !important;
  1074. opacity: 0.96;
  1075. font-size: 14px;
  1076. padding: 16px;
  1077. line-height: 20px;
  1078. border-radius: 4px;
  1079. width: auto;
  1080. width: -moz-available;
  1081. width: -webkit-fill-available;
  1082. }
  1083. p.og pre {
  1084. font-family: monospace;
  1085. background: #fff59d;
  1086. padding: 6px;
  1087. margin: 6px 0;
  1088. color: black;
  1089. }
  1090. p.og h1, p.og h2, p.og h3, p.og h4, p.og h5, p.og h6, p.og p, p.og div {
  1091. margin: 1em 0 0.5em 0;
  1092. }
  1093. p.og h1 {
  1094. font-size: 2em;
  1095. }
  1096. p.og h2 {
  1097. font-size: 1.5em;
  1098. }
  1099. p.og>h3:first-child {
  1100. font-weight: bold;
  1101. margin-bottom: 0.5em;
  1102. }
  1103. p.og h3 {
  1104. font-size: 1.17em;
  1105. }
  1106. p.og h4 {
  1107. font-size: 1em;
  1108. }
  1109. p.og h5 {
  1110. font-size: 0.83em;
  1111. }
  1112. p.og h6 {
  1113. font-size: 0.67em;
  1114. }
  1115. p.og a {
  1116. color: #3e88a0;
  1117. text-decoration: underline;
  1118. }
  1119. p.og pre {
  1120. background: #d7d085 !important;
  1121. }
  1122. p.og :not(pre)>code {
  1123. font-family: monospace;
  1124. background: #d7d085 !important;
  1125. padding: 1px !important;
  1126. }
  1127. p.og summary {
  1128. cursor: pointer;
  1129. }
  1130. p.og hr {
  1131. border: none;
  1132. border-bottom: 1px solid #666;
  1133. background: transparent;
  1134. }
  1135. p.og table {
  1136. border: 2px solid black;
  1137. }
  1138. p.og table td, p.og table th {
  1139. border: 1px solid black;
  1140. padding: 4px;
  1141. }
  1142. p.og sup {
  1143. position: relative;
  1144. font-size: .7em;
  1145. line-height: .7em;
  1146. top: -0.4em;
  1147. }
  1148. span.md-spoiler-text {
  1149. background: #545452;
  1150. border-radius: 2px;
  1151. transition: background 1s ease-out;
  1152. cursor: pointer;
  1153. color: #545452;
  1154. }
  1155. span.md-spoiler-text.revealed {
  1156. background: rgba(84,84,82,.1);
  1157. color: inherit;
  1158. }
  1159. p.og em {
  1160. font-style: italic;
  1161. }
  1162. p.og strong {
  1163. font-weight: bold;
  1164. }
  1165. p.og blockquote {
  1166. border-left: 4px solid #c5c1ad;
  1167. padding: 0 8px;
  1168. margin-left: 5px;
  1169. margin-top: 0.35714285714285715em;
  1170. margin-bottom: 0.35714285714285715em;
  1171. }
  1172. p.og ol {
  1173. list-style: auto;
  1174. margin-left: 1.5em;
  1175. }
  1176. p.og ul {
  1177. list-style: initial;
  1178. margin-left: 1.5em;
  1179. }
  1180. span.edited-date, a.showOriginal {
  1181. font-size: small;
  1182. }
  1183. /* Add some space under the View all comments button on "That Comment Is Missing" page */
  1184. div:first-child > div:first-child > svg + p + a[role="button"] {
  1185. margin-bottom: 1em;
  1186. }
  1187. </style>`
  1188. );
  1189. // listen for spoilertext in original body to be revealed
  1190. window.addEventListener(
  1191. "click",
  1192. function (e) {
  1193. /**
  1194. * @type {HTMLSpanElement}
  1195. */
  1196. const spoiler = e.target.closest("span.md-spoiler-text");
  1197. if (spoiler) {
  1198. spoiler.classList.add("revealed");
  1199. spoiler.removeAttribute("title");
  1200. spoiler.style.cursor = "auto";
  1201. }
  1202. },
  1203. false
  1204. );
  1205. // check for edited submissions
  1206. checkForEditedSubmissions();
  1207. }
  1208. // Old Reddit
  1209. else {
  1210. // fix styling of created paragraphs in old reddit
  1211. document.head.insertAdjacentHTML(
  1212. "beforeend",
  1213. `<style>
  1214. div p.og {
  1215. background: rgb(255, 245, 157) !important;
  1216. color: black !important;
  1217. opacity: 0.96;
  1218. font-size: 14px;
  1219. padding: 16px;
  1220. line-height: 20px;
  1221. border-radius: 7px;
  1222. }
  1223. 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 {
  1224. color: black !important;
  1225. margin: 0.4em 0 0.2em 0;
  1226. }
  1227. p.og :not(pre)>code {
  1228. background: #d7d085 !important;
  1229. padding: 1px !important;
  1230. }
  1231. div p.og a {
  1232. color: #0079d3 !important;
  1233. }
  1234. div p.og a:visited {
  1235. color: #469ad8!important;
  1236. }
  1237. p.og table {
  1238. border: 2px solid black;
  1239. }
  1240. p.og table td, p.og table th {
  1241. border: 1px solid black;
  1242. padding: 4px;
  1243. }
  1244. p.og table tr {
  1245. background: none !important;
  1246. }
  1247. p.og strong {
  1248. font-weight: 600;
  1249. }
  1250. p.og em {
  1251. font-style: italic;
  1252. }
  1253. /* Override for RES Night mode */
  1254. .res-nightmode .entry.res-selected .md-container > .md p.og,
  1255. .res-nightmode .entry.res-selected .md-container > .md p.og p {
  1256. color: black !important;
  1257. }
  1258. /* Override RES title text display */
  1259. .res-betteReddit-showLastEditedTimestamp .edited-timestamp.showOriginal[title]::after {
  1260. content: "";
  1261. }
  1262. </style>`
  1263. );
  1264. }
  1265. // find edited comments
  1266. findEditedComments();
  1267.  
  1268. // create an input field in the bottom right corner of the screen for the api token
  1269. document.head.insertAdjacentHTML(
  1270. "beforeend",
  1271. `<style>
  1272. #apiToken {
  1273. width: 300px;
  1274. padding: 5px;
  1275. }
  1276. #requestTokenLink {
  1277. color: white;
  1278. border-radius: 3px;
  1279. margin-left: 5px;
  1280. }
  1281. #saveButton {
  1282. background: #2D3133;
  1283. color: white;
  1284. border-radius: 3px;
  1285. padding: 5px;
  1286. margin-left: 5px;
  1287. border: none;
  1288. cursor: pointer;
  1289. }
  1290. #saveButton:hover {
  1291. background: #545452;
  1292. }
  1293. #saveButton:active {
  1294. background: #2D3133;
  1295. }
  1296. #closeButton {
  1297. margin-left: 5px;
  1298. border-radius: 5px;
  1299. color: rgb(255, 255, 255);
  1300. padding: 5px;
  1301. background: transparent;
  1302. border: 0;
  1303. cursor: pointer;
  1304. }
  1305. #tokenContainer {
  1306. position: fixed;
  1307. bottom: 0;
  1308. right: 0;
  1309. z-index: 999999999;
  1310. padding: 6px;
  1311. background: #CC3700;
  1312. border-radius: 5px;
  1313. }
  1314. </style>`
  1315. );
  1316. const tokenInput = document.createElement("input");
  1317. tokenInput.type = "text";
  1318. tokenInput.id = "apiToken";
  1319. tokenInput.placeholder = "Pushshift API Token";
  1320. // if there is a token saved in local storage, use it
  1321. storage.get("apiToken").then((token) => {
  1322. if (token) {
  1323. tokenInput.value = token;
  1324. }
  1325. });
  1326. const requestTokenLink = document.createElement("a");
  1327. requestTokenLink.href = "https://api.pushshift.io/signup";
  1328. requestTokenLink.target = "_blank";
  1329. requestTokenLink.rel = "noopener noreferrer";
  1330. requestTokenLink.textContent = "Request Token";
  1331. requestTokenLink.id = "requestTokenLink";
  1332. const saveButton = document.createElement("button");
  1333. saveButton.textContent = "Save";
  1334. saveButton.id = "saveButton";
  1335. saveButton.addEventListener("click", function () {
  1336. // save in local storage
  1337. storage.set("apiToken", tokenInput.value);
  1338. });
  1339. tokenInput.addEventListener("keydown", function (e) {
  1340. if (e.key === "Enter") {
  1341. saveButton.click();
  1342. }
  1343. });
  1344. const closeButton = document.createElement("button");
  1345. closeButton.textContent = "\u00D7"; // times symbol
  1346. closeButton.id = "closeButton";
  1347. const tokenContainer = document.createElement("div");
  1348. tokenContainer.id = "tokenContainer";
  1349. tokenContainer.appendChild(tokenInput);
  1350. tokenContainer.appendChild(saveButton);
  1351. tokenContainer.appendChild(requestTokenLink);
  1352. tokenContainer.appendChild(closeButton);
  1353. closeButton.addEventListener("click", function () {
  1354. // set the token container to display none
  1355. tokenContainer.style.display = "none";
  1356. // save preference in local storage
  1357. storage.set("hideTokenContainer", "true");
  1358. });
  1359. // if the user has hidden the token container before, hide it again
  1360. storage.get("hideTokenContainer").then((hideTokenContainer) => {
  1361. if (hideTokenContainer === "true") {
  1362. tokenContainer.style.display = "none";
  1363. }
  1364. });
  1365. document.body.appendChild(tokenContainer);
  1366.  
  1367. // switch from fetch to inlineFetch if browser is Firefox
  1368. if (navigator.userAgent.includes("Firefox")) {
  1369. fetch = inlineFetch;
  1370. }
  1371. }
  1372.  
  1373. // if the window is loaded, run init(), otherwise wait for it to load
  1374. if (document.readyState === "complete") {
  1375. init();
  1376. } else {
  1377. window.addEventListener("load", init, false);
  1378. }
  1379. })();