Reddit - Load 'Continue this thread' inline

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

Fra og med 13.07.2018. Se den nyeste version.

// ==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
// @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)