Reddit - Load 'Continue this thread' inline

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

当前为 2021-01-25 提交的版本,查看 最新版本

  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 1.9.6
  7. // @icon data:image/png;base64,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
  8. // @match *://*.reddit.com/r/*/comments/*
  9. // @grant none
  10. // @run-at document-end
  11. // @require https://unpkg.com/jquery@3/dist/jquery.min.js
  12. // @require https://greasyfork.org/scripts/7602-mutation-observer/code/mutation-observer.js
  13. // ==/UserScript==
  14.  
  15. /* jshint asi: true, es6: true, laxbreak: true */
  16. /* global jQuery, MutationSummary */
  17.  
  18. /*
  19.  
  20. ==== 1.9.6 (2020.08.08) ====
  21. * Reduced size of load more links compared to comment text
  22. * Fixed script icon
  23. * Removed some unnecessary code
  24.  
  25. ==== 1.9.5 (2018.07.11) ====
  26. * Updated jQuery to v3 and source from unpkg.com
  27. * Add downloadURL to update from Gist
  28.  
  29. ==== 1.9.4 (2018.02.11) ====
  30. * Added @icon field in metadata as SVG wasn't displaying on the installed userscript page
  31.  
  32. ==== 1.9.3 (2017.12.03) ====
  33. * Changed base-64 encoded PNG icons to an SVG icon
  34.  
  35. ==== 1.9.2 (2017.10.11) ====
  36. * Gets correct comment ID for links
  37. * Changed location in comment HTML to use as its root
  38. * Get children of first comment when it is already on the page
  39.  
  40. ==== 1.9.1 (2017.10.11) ====
  41. * Fix broken $target selector
  42.  
  43. ==== 1.9.0 ====
  44. * Catch failed loads, log them to the console and then restore original load link
  45.  
  46. */
  47.  
  48. ; ($ => {
  49. 'use strict'
  50.  
  51. 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'
  52.  
  53. // --------------------------------------------------------------------
  54.  
  55. const units = (v, s) => `${v}${s}`
  56.  
  57. const pluralise = (w, n) => w + (n !== 1 ? 's' : '')
  58.  
  59. const capitalise = s => typeof s === 'string' && s && s.split(/\s+/g).map(w => w[0].toUpperCase() + w.substr(1).toLowerCase()).join(' ')
  60.  
  61. function* flatten (arr) {
  62. for (let x of arr) {
  63. if (Array.isArray(x)) {
  64. yield* (flatten(x))
  65. }
  66. else {
  67. yield x
  68. }
  69. }
  70. }
  71.  
  72. // --------------------------------------------------------------------
  73.  
  74. $.fn.extend({
  75. spinner (options) {
  76. options = Object.assign({}, $.fn.spinner.defaults, options)
  77.  
  78. const $spinner = $('<div class="pulsar-horizontal"></div>')
  79. .css({
  80. padding: units(options.size * 0.25, 'px'),
  81. height: units(options.size, 'px')
  82. })
  83.  
  84. const total_duration = (options.steps + 1) * options.step_duration
  85.  
  86. for (let i = 0; i < options.steps; i++) {
  87. const delay = i * options.step_duration
  88.  
  89. $('<div></div>')
  90. .css({
  91. width: units(options.size, 'px'),
  92. height: units(options.size, 'px'),
  93. backgroundColor: options.colour,
  94. animationDuration: units(total_duration, 's'),
  95. animationDelay: units(delay, 's')
  96. })
  97. .appendTo($spinner)
  98. }
  99.  
  100. if (options.replace) {
  101. this.empty()
  102. }
  103.  
  104. return options.mode === 'prepend'
  105. ? this.prepend($spinner)
  106. : this.append($spinner)
  107. },
  108.  
  109. log (name, ...extras) {
  110. const title = [ `%c${name || '$'}%c : %c${this.length}%c ${pluralise('item', this.length)}`, 'font-weight: bold', '', 'color: #05f', '' ]
  111.  
  112. if (this.length > 0 || extras.length > 0) {
  113. console.group.apply(console, title)
  114.  
  115. if (this.length > 0) {
  116. console.info(this)
  117. }
  118. extras.forEach(extra => {
  119. console.log(extra)
  120. })
  121.  
  122. console.groupEnd()
  123. }
  124. else {
  125. console.info.apply(console, title)
  126. }
  127.  
  128. return this
  129. }
  130. })
  131.  
  132. $.fn.spinner.defaults = {
  133. replace: true,
  134. mode: 'append',
  135. steps: 3,
  136. size: 24,
  137. colour: '#28f',
  138. step_duration: 0.25
  139. }
  140.  
  141. // --------------------------------------------------------------------
  142.  
  143. async function getCommentPage (id) {
  144. const data = await $.get(postUrl + id)
  145.  
  146. return $('.nestedlisting', data)
  147. }
  148.  
  149. // --------------------------------------------------------------------
  150.  
  151. function addComments ($target, $comments) {
  152. $target
  153. .empty()
  154. .append($comments)
  155. .find('.usertext.border .usertext-body')
  156. .css('animation', 'fadenewpost 4s ease-out 4s both')
  157. }
  158.  
  159. // --------------------------------------------------------------------
  160.  
  161. function loadComments ($span, $target, ids) {
  162. let insertChildren = false
  163.  
  164. if (!Array.isArray(ids)) {
  165. ids = [ ids ]
  166. insertChildren = true
  167. }
  168.  
  169. const urls = ids.map(id => postUrl + id)
  170.  
  171. const original = $span.parent().html()
  172.  
  173. $span.spinner()
  174.  
  175. const pageRequests = urls.map(url => {
  176. return $.get(url)
  177. .then(
  178. data => $('.nestedlisting', data).get(),
  179. (xhr, textStatus, errorThrown) => {
  180. console.warn(`%c${capitalise(textStatus)}: ${xhr.status} ${xhr.statusText}%c ${url}`, 'font-weight: bold; color: #f4f', '')
  181. }
  182. )
  183. })
  184.  
  185. $.when(...pageRequests)
  186. .then((...children) => {
  187. let $children = $([...flatten(children)])
  188.  
  189. if (insertChildren) {
  190. $children = $children.find('> .thing > .child > .sitetable')
  191.  
  192. $children
  193. .find('> .entry > .usertext.border')
  194. .removeClass('border')
  195. }
  196.  
  197. $target
  198. .empty()
  199. .append($children)
  200. .find('.usertext.border .usertext-body')
  201. .css('animation', 'fadenewpost 4s ease-out 4s both')
  202. })
  203. .fail((xhr, textStatus, errorThrown) => {
  204. $span.parent().html(original)
  205. })
  206. }
  207.  
  208. // --------------------------------------------------------------------
  209.  
  210. function getCommentId (linkElem) {
  211. const m = linkElem.pathname.match(/\/([a-z0-9]+)\/?$/)
  212. if (!m) {
  213. throw new Error(`No comment ID parsed from link URL "${linkElem.href}"`)
  214. }
  215. return m[1]
  216. }
  217.  
  218. // --------------------------------------------------------------------
  219.  
  220. function processDeepThreadSpans (deepThreadSpans) {
  221. const $deepThreadSpans = $(deepThreadSpans)
  222. .filter(':not([data-comment-ids])')
  223.  
  224. // console.info(`processDeepThreadSpans: processing ${$deepThreadSpans.length}/${deepThreadSpans.length} deep thread spans`)
  225.  
  226. $deepThreadSpans.each(function () {
  227. const $span = $(this)
  228. const $target = $span.closest('.child')
  229.  
  230. const $a = $span.children('a')
  231. const cid = getCommentId($a[ 0 ])
  232.  
  233. $span
  234. .attr('data-comment-ids', cid)
  235. .addClass('expand-inline')
  236.  
  237. async function load () {
  238. $span.spinner()
  239.  
  240. const $listing = await getCommentPage(cid)
  241. const $children = $listing.find('> .thing > .child > .sitetable')
  242.  
  243. addComments($target, $children)
  244. }
  245.  
  246. $a.one('click', event => {
  247. load()
  248.  
  249. return false
  250. })
  251. })
  252. }
  253.  
  254. // --------------------------------------------------------------------
  255.  
  256. function processMoreCommentsSpans (moreCommentsSpans) {
  257. const $moreCommentsSpans = $(moreCommentsSpans)
  258. .filter(':not([data-comment-ids])')
  259.  
  260. // console.info(`processMoreCommentsSpans: processing ${$moreCommentsSpans.length}/${moreCommentsSpans.length} more comment spans`)
  261.  
  262. $moreCommentsSpans.each(function() {
  263. const $span = $(this)
  264. const $target = $span.closest('.child')
  265.  
  266. const $a = $span.children('a')
  267. const onclick = $a.attr('onclick')
  268. const cids = onclick.split(', ')[3].slice(1, -1).split(',')
  269.  
  270. $span
  271. .attr('data-comment-ids', cids.join(','))
  272. .addClass('expand-inline')
  273.  
  274. async function load () {
  275. $span.spinner()
  276.  
  277. const $listings = $(await Promise.all(cids.map(getCommentPage)))
  278.  
  279. addComments($target, $listings)
  280. }
  281.  
  282. $a
  283. .removeAttr('onclick')
  284. .attr('data-onclick', onclick)
  285. .one('click', event => {
  286. load()
  287.  
  288. return false
  289. })
  290. })
  291. }
  292.  
  293. function processMoreCommentsSpans2 (moreCommentsSpans) {
  294. $(moreCommentsSpans).addClass('expand-inline')
  295. }
  296.  
  297. // --------------------------------------------------------------------
  298.  
  299. const rootUrl = `https://${location.hostname}/`
  300. const postUrl = $('.thing.link > .entry a.comments').prop('href')
  301.  
  302. // console.info(`%cSite:%c ${rootUrl}\n%cPost:%c ${postUrl}`, 'font-weight: bold', '', 'font-weight: bold', '')
  303.  
  304. // --------------------------------------------------------------------
  305.  
  306. const observer = new MutationSummary({
  307. callback([ deepThreadSpans, moreCommentsSpans ]) {
  308. // console.log(`Added ${deepThreadSpans.added.length} deep thread spans and ${moreCommentsSpans.added.length} more comment spans`)
  309.  
  310. processDeepThreadSpans(deepThreadSpans.added)
  311. processMoreCommentsSpans2(moreCommentsSpans.added)
  312. },
  313. rootNode: document.body,
  314. queries: [
  315. { element: 'span.deepthread' },
  316. { element: 'span.morecomments' }
  317. ]
  318. })
  319.  
  320. // To process spans in the HTML source
  321. processDeepThreadSpans($('span.deepthread'))
  322. processMoreCommentsSpans2($('span.morecomments'))
  323.  
  324. // --------------------------------------------------------------------
  325.  
  326. $(document.body).append(`<style type="text/css">
  327. .expand-inline {
  328. display: block;
  329. padding: 0;
  330. }
  331. .expand-inline:after {
  332. display: none !important;
  333. }
  334. .expand-inline a {
  335. display: block;
  336. background: transparent url(${EXPAND_ICON}) no-repeat center left;
  337. padding-left: 40px;
  338. height: 32px;
  339. line-height: 32px;
  340. font-size: 1.2rem !important;
  341. font-weight: normal !important;
  342. vertical-align: middle;
  343. text-align: left;
  344. }
  345. .expand-inline a:hover {
  346. background-color: rgba(0, 105, 255, 0.05);
  347. text-decoration: none;
  348. }
  349. .pulsar-horizontal {
  350. display: inline-block;
  351. }
  352. .pulsar-horizontal > div {
  353. display: inline-block;
  354. border-radius: 100%;
  355. animation-name: pulsing;
  356. animation-timing-function: ease-in-out;
  357. animation-iteration-count: infinite;
  358. animation-fill-mode: both;
  359. }
  360. @keyframes pulsing {
  361. 0%, 100% {
  362. transform: scale(0);
  363. opacity: 0.5;
  364. }
  365. 50% {
  366. transform: scale(1);
  367. opacity: 1;
  368. }
  369. }
  370. @keyframes fadenewpost {
  371. 0% {
  372. background-color: #ffc;
  373. padding-left: 5px;
  374. }
  375. 100% {
  376. background-color: transparent;
  377. padding-left: 0;
  378. }
  379. }
  380. </style>`)
  381.  
  382. })(jQuery)
  383.  
  384. jQuery.noConflict(true)