Reddit - Load 'Continue this thread' inline

Changes 'Continue this thread' links to insert the linked comments into the current page

Install this script?
Author's suggested script

You may also like Reddit - Quick RES user tagging.

Install this script
  1. // ==UserScript==
  2. // @name Reddit - Load 'Continue this thread' inline
  3. // @description Changes 'Continue this thread' links to insert the linked comments into the current page
  4. // @author James Skinner <spiralx@gmail.com> (http://github.com/spiralx)
  5. // @namespace http://spiralx.org/
  6. // @version 2.3.3
  7. // @license MIT
  8. // @icon data:image/png;base64,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
  9. // @match *://*.reddit.com/r/*/comments/*
  10. // @match *://*.reddit.com/user/*/comments/*
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_setValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_addStyle
  16. // @grant GM_addValueChangeListener
  17. // @grant GM.getValue
  18. // @grant GM.setValue
  19. // @grant GM.deleteValue
  20. // @grant GM.registerMenuCommand
  21. // @grant GM.addStyle
  22. // @grant GM.addValueChangeListener
  23. // @run-at document-end
  24. // @require https://unpkg.com/jquery@3/dist/jquery.min.js
  25. // @require https://unpkg.com/mutation-summary@1/dist/umd/mutation-summary.js
  26. // @require https://greasyfork.org/scripts/371339-gm-webextpref/code/GM_webextPref.js?version=961539
  27. // ==/UserScript==
  28.  
  29. /* jshint asi: true, esnext: true, laxbreak: true */
  30. /* global jQuery, MutationSummary, GM_webextPref */
  31.  
  32. /*
  33. ==== 2.3.3 (2022.10.13) ====
  34. * Actually fix bug supposedly fixed by previous version...
  35.  
  36. ==== 2.3.2 (2022.08.28) ====
  37. * Fix bug where clicking on "Continue this thread" after hover loading was triggered would open the comment's page
  38.  
  39. ==== 2.3.1 (2022.06.26) ====
  40. * Use GM_webextPref library to support Greasemonkey 4 users
  41.  
  42. ==== 2.3.0 (2022.05.03) ====
  43. * Fix centred text in expand links
  44. * Add configuration for expanding links by moving the mouse over the text "Continue this thread" or "Load more comments"
  45.  
  46. ==== 2.2.1 (2022.05.02) ====
  47. * Make expand links a block again so they stretch across whole width
  48.  
  49. ==== 2.2.0 (2022.05.01) ====
  50. * Use MonkeyConfig library to provide settings for intersection observer behaviour
  51. * CHanged styling of expandos and replaced icon with emoji ↘️
  52.  
  53. ==== 2.1.0 (2022.04.17) ====
  54. * Use IntersectionObserver to automatically open "Load more comments" when they scroll into view
  55. * Put above behaviour behind USE_INTERSECTION_OBSERVER feature flag
  56.  
  57. ==== 2.0.0 (2022.04.02) ====
  58. * Added MIT license
  59. * Expand non-top level collapsed comments on load
  60. * Expand collapsed comments inserted from clicking "Load more comments" or "Continue this thread"
  61. * Script now also runs on posts made to a user's homepage
  62. * Remove old code handling "Load more comments" links
  63. * Tidied up old code and updated to use current JS features
  64.  
  65. ==== 1.9.7 (2021.11.05) ====
  66. * Use MutationSummary from unpkg.com instead of Greasyfork
  67.  
  68. ==== 1.9.6 (2020.08.08) ====
  69. * Reduced size of load more links compared to comment text
  70. * Fixed script icon
  71. * Removed some unnecessary code
  72.  
  73. ==== 1.9.5 (2018.07.11) ====
  74. * Updated jQuery to v3 and source from unpkg.com
  75. * Add downloadURL to update from Gist
  76.  
  77. ==== 1.9.4 (2018.02.11) ====
  78. * Added @icon field in metadata as SVG wasn't displaying on the installed userscript page
  79.  
  80. ==== 1.9.3 (2017.12.03) ====
  81. * Changed base-64 encoded PNG icons to an SVG icon
  82.  
  83. ==== 1.9.2 (2017.10.11) ====
  84. * Gets correct comment ID for links
  85. * Changed location in comment HTML to use as its root
  86. * Get children of first comment when it is already on the page
  87.  
  88. ==== 1.9.1 (2017.10.11) ====
  89. * Fix broken $target selector
  90.  
  91. ==== 1.9.0 ====
  92. * Catch failed loads, log them to the console and then restore original load link
  93.  
  94. */
  95.  
  96. ; (async ($, MutationSummary) => {
  97.  
  98. const config = GM_webextPref({
  99. navbar: false,
  100. default: {
  101. autoExpandWhenVisible: false,
  102. expandOnMouseOver: false,
  103. expandOnMouseOverDelay: 500,
  104. },
  105. body: [
  106. {
  107. key: 'autoExpandWhenVisible',
  108. label: 'Automatically expand any links when they come into view?',
  109. type: 'checkbox',
  110. },
  111. {
  112. key: 'expandOnMouseOver',
  113. label: 'Expand links when you move the mouse over them?',
  114. type: 'checkbox',
  115. },
  116. {
  117. key: 'expandOnMouseOverDelay',
  118. label: 'Delay between when you move the mouse over a link and it expands (ms)',
  119. type: 'number',
  120. },
  121. ],
  122. onSave(newSettings) {
  123. settings = newSettings
  124. createOrDestroyIntersectionObserver()
  125. addOrRemoveMouseoverHandler()
  126. },
  127. })
  128.  
  129. await config.ready()
  130.  
  131. config.on('change', changedSettings => {
  132. settings = { ...settings, ...changedSettings }
  133. createOrDestroyIntersectionObserver()
  134. addOrRemoveMouseoverHandler()
  135. })
  136.  
  137. let settings = config.getAll()
  138.  
  139. // --------------------------------------------------------------------
  140.  
  141. $.fn.extend({
  142. spinner(options) {
  143. options = {
  144. replace: true,
  145. mode: 'append',
  146. steps: 3,
  147. size: 24,
  148. colour: '#28f',
  149. step_duration: 0.25,
  150. ...options
  151. }
  152.  
  153. const $spinner = $('<div class="pulsar-horizontal"></div>')
  154. .css({
  155. padding: `${options.size * 0.25}px`,
  156. height: `${options.size}px`
  157. })
  158.  
  159. const total_duration = (options.steps + 1) * options.step_duration
  160.  
  161. for (let i = 0; i < options.steps; i++) {
  162. const delay = i * options.step_duration
  163.  
  164. $('<div></div>')
  165. .css({
  166. width: `${options.size}px`,
  167. height: `${options.size}px`,
  168. backgroundColor: options.colour,
  169. animationDuration: `${total_duration}s`,
  170. animationDelay: `${delay}s`
  171. })
  172. .appendTo($spinner)
  173. }
  174.  
  175. if (options.replace) {
  176. this.empty()
  177. }
  178.  
  179. return options.mode === 'prepend'
  180. ? this.prepend($spinner)
  181. : this.append($spinner)
  182. },
  183.  
  184. log(name = '$') {
  185. const title = [ `%c${name}%c : %c${this.length}%c ${this.length > 1 ? 'items' : 'item'}`, 'font-weight: bold', '', 'color: #05f', '' ]
  186.  
  187. if (this.length > 0) {
  188. console.group(...title)
  189. console.info(this)
  190. console.groupEnd()
  191. } else {
  192. console.info(...title)
  193. }
  194.  
  195. return this
  196. }
  197. })
  198.  
  199. // --------------------------------------------------------------------
  200.  
  201. async function loadAndInsertComments(cid, $span, $target) {
  202. $target.data('loading', 'true')
  203. $span.spinner()
  204.  
  205. const data = await $.get(postUrl + cid)
  206.  
  207. const $comments = $('.nestedlisting > .thing > .child > .sitetable', data)
  208.  
  209. $target
  210. .empty()
  211. .append($comments)
  212. .find('.usertext.border .usertext-body')
  213. .css('animation', 'fadenewpost 4s ease-out 4s both')
  214. }
  215.  
  216. // --------------------------------------------------------------------
  217.  
  218. function getCommentId(linkElem) {
  219. const m = linkElem.pathname.match(/\/([a-z0-9]+)\/?$/)
  220. if (!m) {
  221. throw new Error(`No comment ID parsed from link URL "${linkElem.href}"`)
  222. }
  223. return m[1]
  224. }
  225.  
  226. // --------------------------------------------------------------------
  227.  
  228. function processDeepThreadSpans(deepThreadSpans) {
  229. const $deepThreadSpans = $(deepThreadSpans)
  230. .filter(':not([data-comment-ids])')
  231.  
  232. // console.info(`processDeepThreadSpans: processing ${$deepThreadSpans.length}/${deepThreadSpans.length} deep thread spans`)
  233.  
  234. $deepThreadSpans.each(function () {
  235. const $span = $(this)
  236. const $target = $span.closest('.child')
  237.  
  238. const $a = $span.children('a')
  239. const cid = getCommentId($a[ 0 ])
  240.  
  241. let first = true
  242.  
  243. $span
  244. .attr('data-comment-ids', cid)
  245. .addClass('expand-inline')
  246.  
  247. $a
  248. .wrapInner('<span class="expand-text"></span>')
  249. .on('click', event => {
  250. const loading = $target.data('loading')
  251.  
  252. if (first && !loading) {
  253. first = false
  254. loadAndInsertComments(cid, $span, $target)
  255. }
  256.  
  257. return false
  258. })
  259. })
  260. }
  261.  
  262. // --------------------------------------------------------------------
  263.  
  264. function uncollapseComments($collapsedComments) {
  265. $collapsedComments
  266. .removeClass('collapsed')
  267. .addClass('noncollapsed')
  268. .find('> .entry .tagline .expand')
  269. .text('[-]')
  270. }
  271.  
  272. function uncollapseAllComments($collapsedComments, depth = 3) {
  273. // console.log($collapsedComments, depth)
  274.  
  275. if ($collapsedComments.length > 0 && depth > 0) {
  276. uncollapseComments($collapsedComments)
  277.  
  278. requestAnimationFrame(() => {
  279. uncollapseAllComments($collapsedComments.find('.thing.comment.collapsed'), depth - 1)
  280. })
  281. }
  282. }
  283.  
  284. // --------------------------------------------------------------------
  285.  
  286. const rootUrl = `https://${location.hostname}/`
  287. const postUrl = $('.thing.link > .entry a.comments').prop('href')
  288.  
  289. // console.info(`%cSite:%c ${rootUrl}\n%cPost:%c ${postUrl}`, 'font-weight: bold', '', 'font-weight: bold', '')
  290.  
  291. // --------------------------------------------------------------------
  292.  
  293. let intersectionObserver = null
  294.  
  295. function createOrDestroyIntersectionObserver() {
  296. if (settings.autoExpandWhenVisible && !intersectionObserver) {
  297. intersectionObserver = new IntersectionObserver(
  298. (entries, observer) => {
  299. for (const entry of entries) {
  300. if (entry.isIntersecting) {
  301. entry.target.click()
  302. observer.unobserve(entry.target)
  303. }
  304. }
  305. },
  306. {
  307. threshold: 0.5
  308. }
  309. )
  310.  
  311. $('span.morecomments, span.deepthread').each(function() {
  312. intersectionObserver.observe(this.firstElementChild)
  313. })
  314.  
  315. console.log('IntersectionObserver created')
  316. } else if (!settings.autoExpandWhenVisible && intersectionObserver) {
  317. intersectionObserver.disconnect()
  318. intersectionObserver = null
  319. console.log('IntersectionObserver destroyed')
  320. }
  321. }
  322.  
  323. createOrDestroyIntersectionObserver()
  324.  
  325. // --------------------------------------------------------------------
  326.  
  327. function addOrRemoveMouseoverHandler() {
  328. $('.commentarea').off('mouseenter.spiralx')
  329.  
  330. if (settings.expandOnMouseOver) {
  331. const hoveredElems = new WeakMap()
  332.  
  333. $('.commentarea')
  334. .on('mouseenter.spiralx', '.expand-text', function() {
  335. const elem = this
  336.  
  337. const timeoutId = setTimeout(() => {
  338. hoveredElems.delete(elem)
  339. elem.click()
  340. }, settings.expandOnMouseOverDelay)
  341.  
  342. hoveredElems.set(elem, timeoutId)
  343. })
  344. .on('mouseleave.spiralx', '.expand-text', function() {
  345. const timeoutId = hoveredElems.get(this)
  346.  
  347. if (timeoutId) {
  348. clearTimeout(timeoutId)
  349. hoveredElems.delete(this)
  350. }
  351. })
  352. }
  353. }
  354.  
  355. addOrRemoveMouseoverHandler()
  356.  
  357. // --------------------------------------------------------------------
  358.  
  359. function markAsExpand(selectorOrElements, observe = true) {
  360. const $elems = $(selectorOrElements)
  361. .addClass('expand-inline')
  362. .children('a')
  363. .wrapInner('<span class="expand-text"></span>')
  364.  
  365. if (intersectionObserver) {
  366. $elems.each(function() {
  367. intersectionObserver.observe(this.firstElementChild)
  368. })
  369. }
  370. }
  371.  
  372. // --------------------------------------------------------------------
  373.  
  374. // Uncollapse non-top level comments on page load
  375. uncollapseAllComments($('.thing.comment .thing.comment.collapsed'))
  376.  
  377. const observer = new MutationSummary({
  378. callback([ deepThreadSpans, moreCommentsSpans, comments ]) {
  379. // console.log(`Added ${deepThreadSpans.added.length} deep thread spans and ${moreCommentsSpans.added.length} more comment spans`)
  380.  
  381. markAsExpand(moreCommentsSpans.added)
  382.  
  383. processDeepThreadSpans(deepThreadSpans.added)
  384.  
  385. const $collapsedComments = $(comments.added).filter('.collapsed')
  386. uncollapseAllComments($collapsedComments)
  387. },
  388. rootNode: document.body,
  389. queries: [
  390. { element: 'span.deepthread' },
  391. { element: 'span.morecomments' },
  392. { element: '.thing.comment' },
  393. ]
  394. })
  395.  
  396. // To process spans in the HTML source
  397. markAsExpand('span.morecomments', false)
  398.  
  399. processDeepThreadSpans($('span.deepthread'))
  400.  
  401. // --------------------------------------------------------------------
  402.  
  403. $(document.body).append(`<style type="text/css">
  404. .expand-inline {
  405. display: block;
  406. padding: 0;
  407. }
  408. .expand-inline:after {
  409. display: none !important;
  410. }
  411. .expand-inline a {
  412. display: block;
  413. text-align: left;
  414. }
  415. .expand-inline a:before {
  416. content: "↘️";
  417. padding-right: 0.4em;
  418. }
  419. .expand-inline a:hover {
  420. background-color: rgba(0, 105, 255, 0.05);
  421. text-decoration: none;
  422. }
  423. .pulsar-horizontal {
  424. display: inline-block;
  425. }
  426. .pulsar-horizontal > div {
  427. display: inline-block;
  428. border-radius: 100%;
  429. animation-name: pulsing;
  430. animation-timing-function: ease-in-out;
  431. animation-iteration-count: infinite;
  432. animation-fill-mode: both;
  433. }
  434. @keyframes pulsing {
  435. 0%, 100% {
  436. transform: scale(0);
  437. opacity: 0.5;
  438. }
  439. 50% {
  440. transform: scale(1);
  441. opacity: 1;
  442. }
  443. }
  444. @keyframes fadenewpost {
  445. 0% {
  446. background-color: #ffc;
  447. padding-left: 5px;
  448. }
  449. 100% {
  450. background-color: transparent;
  451. padding-left: 0;
  452. }
  453. }
  454. </style>`)
  455.  
  456. })(jQuery, MutationSummary?.MutationSummary)
  457.  
  458. jQuery.noConflict(true)