Skip to content

Instantly share code, notes, and snippets.

@spiralx
Last active November 30, 2022 13:04
Show Gist options
  • Save spiralx/7ab95c47df98a5e79ce482f3285250a0 to your computer and use it in GitHub Desktop.
Save spiralx/7ab95c47df98a5e79ce482f3285250a0 to your computer and use it in GitHub Desktop.
Reddit - Load 'Continue this thread' inline user script
// ==UserScript==
// @name Reddit - Load 'Continue this thread' inline
// @description Changes 'Continue this thread' links to insert the linked comments into the current page
// @author James Skinner <[email protected]> (http://github.com/spiralx)
// @namespace http://spiralx.org/
// @version 1.9.5
// @downloadURL https://gist.github.com/spiralx/7ab95c47df98a5e79ce482f3285250a0/raw/6408eac4608d837eb735c2ffcc5fda4f1439513d/reddit-load-comments-inline.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiAAABYgAWToQQYAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAF3SURBVDhP1ZI/SAJhGMZN3WpKOAgsKGiIoKWg0K2LXIJWj6DAhpAgKMiW7irBqziHbhYE12sLGtWGKHBx0kHcQrBJXNIz9e35vvsQoj841g9+fHy8z/PyHZzrXzANNbgKF2ASHsOhOYGTUIb3cBSq4hyKa3Ey2GsMeMRvvzALd+AVvIPbcAb+yAq8hBehUOgxnU5TtVol27ap3W5TpVKhVCpFsiw/IHMusstwgOHxeDZM07T7/T5Rr0vUfacBvR7uHWIzwzBayK+zDm8KktFo9FXEifaXiHbnxAXEZKKtqcHSSCTygs6nBYuWZfEhR1eIzjbFBdzsYcma8xKQyWQInXmn6jCeSCQ6fDoEqqqyzxhzqgK/368Xi0UeKJfLpGka5fN5yuVyrEClUonPCoUCSZLE/oMvjGBwqut6q16vU6PRoGw2yxc0m02q1WoUj8fffD5fjGWdyvdMeL3ew2AweKsoynM4HH4KBAKW2+0+wExyIn8Hl+sDt5ENCrpr91QAAAAASUVORK5CYII=
// @icon64 data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2029%2025%22%3E%3Cg%20transform=%22translate(1%201)%22%20stroke-width=%221.1%22%20fill-rule=%22evenodd%22%20fill=%22none%22%3E%3Ccircle%20stroke=%22#000%22%20cx=%222.7%22%20r=%222.7%22%20cy=%2210.7%22%20fill=%22#fff%22/%3E%3Ccircle%20stroke=%22#000%22%20cx=%2224.7%22%20r=%222.7%22%20cy=%2210.7%22%20fill=%22#fff%22/%3E%3Cpath%20stroke-linejoin=%22round%22%20stroke=%22#000%22%20stroke-linecap=%22round%22%20d=%22M21.23%201.35L15.83.08l-2%207.28%22/%3E%3Ccircle%20stroke=%22#000%22%20cx=%2223.13%22%20r=%222.13%22%20cy=%222.13%22%20fill=%22#fff%22/%3E%3Cellipse%20cy=%2214.99%22%20rx=%2212.24%22%20ry=%227.99%22%20stroke=%22#000%22%20cx=%2213.24%22%20fill=%22#fff%22/%3E%3Cg%20transform=%22translate(8%2012)%22%3E%3Ccircle%20stroke=%22#FF4500%22%20cx=%221%22%20r=%221.43%22%20cy=%221.43%22%20fill=%22#FF4500%22/%3E%3Ccircle%20stroke=%22#FF4500%22%20cx=%2210%22%20r=%221.43%22%20cy=%221.43%22%20fill=%22#FF4500%22/%3E%3Cpath%20stroke=%22#000%22%20d=%22M1.5%206.23C2.58%207.3%204.3%207.5%205.73%207.5m4.24-1.27C8.9%207.3%207.17%207.5%205.77%207.5%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E
// @match *://*.reddit.com/r/*/comments/*
// @grant none
// @run-at document-end
// @require https://unpkg.com/jquery@3/dist/jquery.min.js
// @require https://greasyfork.org/scripts/7602-mutation-observer/code/mutation-observer.js
// ==/UserScript==
/* jshint asi: true, es6: true, laxbreak: true */
/* global jQuery, MutationSummary */
/*
==== 1.9.5 (2018.07.11) ====
* Updated jQuery to v3 and source from unpkg.com
* Add downloadURL to update from Gist
==== 1.9.4 (2018.02.11) ====
* Added @icon field in metadata as SVG wasn't displaying on the installed userscript page
==== 1.9.3 (2017.12.03) ====
* Changed base-64 encoded PNG icons to an SVG icon
==== 1.9.2 (2017.10.11) ====
* Gets correct comment ID for links
* Changed location in comment HTML to use as its root
* Get children of first comment when it is already on the page
==== 1.9.1 (2017.10.11) ====
* Fix broken $target selector
==== 1.9.0 ====
* Catch failed loads, log them to the console and then restore original load link
*/
; (function userScript($) {
'use strict'
const NORMAL = 'font-weight: normal; text-decoration: none; color: black'
const ERROR = 'font-weight: bold; color: #f4f'
const LINK = [ 'color: #05f; text-decoration: underline', NORMAL ]
const BOLD = [ 'font-weight: bold', NORMAL ]
const BLUE = [ 'color: #05f', NORMAL ]
const RED = [ 'color: #e32636', NORMAL ]
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'
// --------------------------------------------------------------------
const units = (v, s) => `${v}${s}`
const pluralise = (w, n) => w + (n !== 1 ? 's' : '')
const capitalise = s => typeof s === 'string' && s && s.split(/\s+/g).map(w => w[0].toUpperCase() + w.substr(1).toLowerCase()).join(' ')
function* flatten (arr) {
for (let x of arr) {
if (Array.isArray(x)) {
yield* (flatten(x))
}
else {
yield x
}
}
}
// --------------------------------------------------------------------
$.fn.extend({
spinner (options) {
options = Object.assign({}, $.fn.spinner.defaults, options)
const $spinner = $('<div class="pulsar-horizontal"></div>')
.css({
padding: units(options.size * 0.25, 'px'),
height: units(options.size, 'px')
})
const total_duration = (options.steps + 1) * options.step_duration
for (let i = 0; i < options.steps; i++) {
const delay = i * options.step_duration
$('<div></div>')
.css({
width: units(options.size, 'px'),
height: units(options.size, 'px'),
backgroundColor: options.colour,
animationDuration: units(total_duration, 's'),
animationDelay: units(delay, 's')
})
.appendTo($spinner)
}
if (options.replace) {
this.empty()
}
return options.mode === 'prepend'
? this.prepend($spinner)
: this.append($spinner)
},
log (name, ...extras) {
const title = [ `%c${name || '$'}%c : %c${this.length}%c ${pluralise('item', this.length)}`, ...BOLD, ...BLUE ]
if (this.length > 0 || extras.length > 0) {
console.group.apply(console, title)
if (this.length > 0) {
console.info(this)
}
extras.forEach(extra => {
console.log(extra)
})
console.groupEnd()
}
else {
console.info.apply(console, title)
}
return this
}
})
$.fn.spinner.defaults = {
replace: true,
mode: 'append',
steps: 3,
size: 24,
colour: '#28f',
step_duration: 0.25
}
// --------------------------------------------------------------------
async function getCommentPage (id) {
const url = postUrl + id
const data = await $.get(url)
const $listing = $('.nestedlisting', data)
console.groupCollapsed(`getCommentPage(%c${id}%c, %c${url}%c)`, ...RED, ...LINK)
// console.log(data)
console.log($listing.get(0).outerHTML)
console.groupEnd()
return $listing
}
// --------------------------------------------------------------------
function addComments($target, $comments) {
$target
.empty()
.append($comments)
.find('.usertext.border .usertext-body')
.css('animation', 'fadenewpost 4s ease-out 4s both')
}
// --------------------------------------------------------------------
function loadComments ($span, $target, ids) {
let insertChildren = false
if (!Array.isArray(ids)) {
ids = [ ids ]
insertChildren = true
}
const urls = ids.map(id => postUrl + id)
/*
console.group(`%cloadComments%c(${ids.length} ${ids.length > 1 ? 'ids' : 'id'}: ${ids.join(', ')})`, ...BOLD)
console.info($span[0].outerHTML)
console.log(`%c${urls.join('\n')}%c`, ...LINK)
console.groupEnd()
*/
const original = $span.parent().html()
$span.spinner({
colour: '#28f',
size: 24,
step_duration: 0.25,
replace: true
})
const pageRequests = urls.map(url => {
return $.get(url)
.then(
// data => $('.nestedlisting > .thing', data).next().andSelf().get(),
data => $('.nestedlisting', data).get(),
(xhr, textStatus, errorThrown) => {
console.warn(`%c${capitalise(textStatus)}: ${xhr.status} ${xhr.statusText}%c %c${url}%c`, ERROR, NORMAL, LINK, NORMAL)
}
)
})
$.when(...pageRequests)
.then((...children) => {
let $children = $([...flatten(children)])
// $children.log('$children')
if (insertChildren) {
$children = $children.find('> .thing > .child > .sitetable')
$children
.find('> .entry > .usertext.border')
.removeClass('border')
}
$target
.empty()
.append($children)
.find('.usertext.border .usertext-body')
.css('animation', 'fadenewpost 4s ease-out 4s both')
})
.fail((xhr, textStatus, errorThrown) => {
$span.parent().html(original)
})
}
// --------------------------------------------------------------------
function getCommentId (linkElem) {
const m = linkElem.pathname.match(/\/([a-z0-9]+)\/?$/)
if (!m) {
throw new Error(`No comment ID parsed from link URL "${linkElem.href}"`)
}
return m[1]
}
// --------------------------------------------------------------------
function processDeepThreadSpans (deepThreadSpans) {
const $deepThreadSpans = $(deepThreadSpans)
.filter(':not([data-comment-ids])')
// console.info(`processDeepThreadSpans: processing ${$deepThreadSpans.length}/${deepThreadSpans.length} deep thread spans`)
$deepThreadSpans.each(function() {
const $span = $(this),
$target = $span.closest('.child'),
$a = $span.children('a'),
cid = getCommentId($a[0])
// $span.log('$span')
// $target.log('$target')
$span
.attr('data-comment-ids', cid)
.addClass('expand-inline')
async function load () {
$span.spinner()
const $listing = await getCommentPage(cid)
const $children = $listing.find('> .thing > .child > .sitetable')
addComments($target, $children)
}
$a.one('click', event => {
load()
return false
})
})
}
// --------------------------------------------------------------------
function processMoreCommentsSpans(moreCommentsSpans) {
const $moreCommentsSpans = $(moreCommentsSpans)
.filter(':not([data-comment-ids])')
// console.info(`processMoreCommentsSpans: processing ${$moreCommentsSpans.length}/${moreCommentsSpans.length} more comment spans`)
$moreCommentsSpans.each(function() {
const $span = $(this),
$target = $span.closest('.child'),
$a = $span.children('a'),
onclick = $a.attr('onclick'),
cids = onclick.split(', ')[3].slice(1, -1).split(',')
$span
.attr('data-comment-ids', cids.join(','))
.addClass('expand-inline')
async function load () {
$span.spinner()
const $listings = $(await Promise.all(cids.map(getCommentPage)))
addComments($target, $listings)
}
$a
.removeAttr('onclick')
.attr('data-onclick', onclick)
.one('click', event => {
// loadComments($span, $target, ...cids)
load()
return false
})
})
}
function processMoreCommentsSpans2 (moreCommentsSpans) {
$(moreCommentsSpans).addClass('expand-inline')
}
// --------------------------------------------------------------------
const rootUrl = `https://${location.hostname}/`
const postUrl = $('.thing.link > .entry a.comments').prop('href')
// console.info(`%cSite:%c %c${rootUrl}%c\n%cPost:%c %c${postUrl}%c`, ...BOLD, ...LINK, ...BOLD, ...LINK)
// --------------------------------------------------------------------
const observer = new MutationSummary({
callback(summaries) {
const deepThreadSpans = summaries.shift().added,
moreCommentsSpans = summaries.shift().added
// console.log(`Added ${deepThreadSpans.length} deep thread spans and ${moreCommentsSpans.length} more comment spans`)
processDeepThreadSpans(deepThreadSpans)
processMoreCommentsSpans2(moreCommentsSpans)
},
rootNode: document.body,
queries: [
{ element: 'span.deepthread' },
{ element: 'span.morecomments' }
]
})
// To process spans in the HTML source
processDeepThreadSpans($('span.deepthread'))
processMoreCommentsSpans2($('span.morecomments'))
// --------------------------------------------------------------------
$(document.body).append(`<style type="text/css">
.expand-inline {
display: block;
padding: 0;
}
.expand-inline:after {
display: none !important;
}
.expand-inline a {
display: block;
background: transparent url(${EXPAND_ICON}) no-repeat center left;
padding-left: 40px;
height: 40px;
line-height: 40px;
font-size: 1.4rem !important;
font-weight: normal !important;
vertical-align: middle;
text-align: left;
}
.expand-inline a:hover {
background-color: rgba(0, 105, 255, 0.05);
text-decoration: none;
}
.pulsar-horizontal {
display: inline-block;
}
.pulsar-horizontal > div {
display: inline-block;
border-radius: 100%;
animation-name: pulsing;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-fill-mode: both;
}
@keyframes pulsing {
0%, 100% {
transform: scale(0);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}
@keyframes fadenewpost {
0% {
background-color: #ffc;
padding-left: 5px;
}
100% {
background-color: transparent;
padding-left: 0;
}
}
</style>`)
})(jQuery)
jQuery.noConflict(true)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment