Reddit - Load 'Continue this thread' inline

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

当前为 2022-06-26 提交的版本,查看 最新版本

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