Greasy Fork is available in English.

Reddit - Load 'Continue this thread' inline

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

// ==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 <spiralx@gmail.com> (http://github.com/spiralx)
// @namespace      http://spiralx.org/
// @version        2.3.3
// @license        MIT
// @icon           data:image/png;base64,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
// @match          *://*.reddit.com/r/*/comments/*
// @match          *://*.reddit.com/user/*/comments/*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_setValue
// @grant          GM_registerMenuCommand
// @grant          GM_addStyle
// @grant          GM_addValueChangeListener
// @grant          GM.getValue
// @grant          GM.setValue
// @grant          GM.deleteValue
// @grant          GM.registerMenuCommand
// @grant          GM.addStyle
// @grant          GM.addValueChangeListener
// @run-at         document-end
// @require        https://unpkg.com/jquery@3/dist/jquery.min.js
// @require        https://unpkg.com/mutation-summary@1/dist/umd/mutation-summary.js
// @require        https://greasyfork.org/scripts/371339-gm-webextpref/code/GM_webextPref.js?version=961539
// ==/UserScript==

/* jshint asi: true, esnext: true, laxbreak: true */
/* global jQuery, MutationSummary, GM_webextPref */

/*
==== 2.3.3 (2022.10.13) ====
* Actually fix bug supposedly fixed by previous version...

==== 2.3.2 (2022.08.28) ====
* Fix bug where clicking on "Continue this thread" after hover loading was triggered would open the comment's page

==== 2.3.1 (2022.06.26) ====
* Use GM_webextPref library to support Greasemonkey 4 users

==== 2.3.0 (2022.05.03) ====
* Fix centred text in expand links
* Add configuration for expanding links by moving the mouse over the text "Continue this thread" or "Load more comments"

==== 2.2.1 (2022.05.02) ====
* Make expand links a block again so they stretch across whole width

==== 2.2.0 (2022.05.01) ====
* Use MonkeyConfig library to provide settings for intersection observer behaviour
* CHanged styling of expandos and replaced icon with emoji ↘️

==== 2.1.0 (2022.04.17) ====
* Use IntersectionObserver to automatically open "Load more comments" when they scroll into view
* Put above behaviour behind USE_INTERSECTION_OBSERVER feature flag

==== 2.0.0 (2022.04.02) ====
* Added MIT license
* Expand non-top level collapsed comments on load
* Expand collapsed comments inserted from clicking "Load more comments" or "Continue this thread"
* Script now also runs on posts made to a user's homepage
* Remove old code handling "Load more comments" links
* Tidied up old code and updated to use current JS features

==== 1.9.7 (2021.11.05) ====
* Use MutationSummary from unpkg.com instead of Greasyfork

==== 1.9.6 (2020.08.08) ====
* Reduced size of load more links compared to comment text
* Fixed script icon
* Removed some unnecessary code

==== 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

*/

; (async ($, MutationSummary) => {

  const config = GM_webextPref({
    navbar: false,
    default: {
      autoExpandWhenVisible: false,
      expandOnMouseOver: false,
      expandOnMouseOverDelay: 500,
    },
    body: [
      {
        key: 'autoExpandWhenVisible',
        label: 'Automatically expand any links when they come into view?',
        type: 'checkbox',
      },
      {
        key: 'expandOnMouseOver',
        label: 'Expand links when you move the mouse over them?',
        type: 'checkbox',
      },
      {
        key: 'expandOnMouseOverDelay',
        label: 'Delay between when you move the mouse over a link and it expands (ms)',
        type: 'number',
      },
    ],
    onSave(newSettings) {
      settings = newSettings
      createOrDestroyIntersectionObserver()
      addOrRemoveMouseoverHandler()
    },
  })

  await config.ready()

  config.on('change', changedSettings => {
    settings = { ...settings, ...changedSettings }
    createOrDestroyIntersectionObserver()
    addOrRemoveMouseoverHandler()
  })

  let settings = config.getAll()

  // --------------------------------------------------------------------

  $.fn.extend({
    spinner(options) {
      options = {
        replace: true,
        mode: 'append',
        steps: 3,
        size: 24,
        colour: '#28f',
        step_duration: 0.25,
        ...options
      }

      const $spinner = $('<div class="pulsar-horizontal"></div>')
        .css({
          padding: `${options.size * 0.25}px`,
          height: `${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: `${options.size}px`,
            height: `${options.size}px`,
            backgroundColor: options.colour,
            animationDuration: `${total_duration}s`,
            animationDelay: `${delay}s`
          })
          .appendTo($spinner)
      }

      if (options.replace) {
        this.empty()
      }

      return options.mode === 'prepend'
        ? this.prepend($spinner)
        : this.append($spinner)
    },

    log(name = '$') {
      const title = [ `%c${name}%c : %c${this.length}%c ${this.length > 1 ? 'items' : 'item'}`, 'font-weight: bold', '', 'color: #05f', '' ]

      if (this.length > 0) {
        console.group(...title)
        console.info(this)
        console.groupEnd()
      } else {
        console.info(...title)
      }

      return this
    }
  })

  // --------------------------------------------------------------------

  async function loadAndInsertComments(cid, $span, $target) {
    $target.data('loading', 'true')
    $span.spinner()

    const data = await $.get(postUrl + cid)

    const $comments = $('.nestedlisting > .thing > .child > .sitetable', data)

    $target
      .empty()
      .append($comments)
      .find('.usertext.border .usertext-body')
        .css('animation', 'fadenewpost 4s ease-out 4s both')
  }

  // --------------------------------------------------------------------

  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)
      const $target = $span.closest('.child')

      const $a = $span.children('a')
      const cid = getCommentId($a[ 0 ])

      let first = true

      $span
        .attr('data-comment-ids', cid)
        .addClass('expand-inline')

      $a
        .wrapInner('<span class="expand-text"></span>')
        .on('click', event => {
          const loading = $target.data('loading')

          if (first && !loading) {
            first = false
            loadAndInsertComments(cid, $span, $target)
          }

          return false
        })
    })
  }

  // --------------------------------------------------------------------

  function uncollapseComments($collapsedComments) {
    $collapsedComments
      .removeClass('collapsed')
      .addClass('noncollapsed')
      .find('> .entry .tagline .expand')
        .text('[-]')
  }

  function uncollapseAllComments($collapsedComments, depth = 3) {
    // console.log($collapsedComments, depth)

    if ($collapsedComments.length > 0 && depth > 0) {
      uncollapseComments($collapsedComments)

      requestAnimationFrame(() => {
        uncollapseAllComments($collapsedComments.find('.thing.comment.collapsed'), depth - 1)
      })
    }
  }

  // --------------------------------------------------------------------

  const rootUrl = `https://${location.hostname}/`
  const postUrl = $('.thing.link > .entry a.comments').prop('href')

  // console.info(`%cSite:%c ${rootUrl}\n%cPost:%c ${postUrl}`, 'font-weight: bold', '', 'font-weight: bold', '')

  // --------------------------------------------------------------------

  let intersectionObserver = null

  function createOrDestroyIntersectionObserver() {
    if (settings.autoExpandWhenVisible && !intersectionObserver) {
      intersectionObserver = new IntersectionObserver(
        (entries, observer) => {
          for (const entry of entries) {
            if (entry.isIntersecting) {
              entry.target.click()
              observer.unobserve(entry.target)
            }
          }
        },
        {
          threshold: 0.5
        }
      )

      $('span.morecomments, span.deepthread').each(function() {
        intersectionObserver.observe(this.firstElementChild)
      })

      console.log('IntersectionObserver created')
    } else if (!settings.autoExpandWhenVisible && intersectionObserver) {
      intersectionObserver.disconnect()
      intersectionObserver = null
      console.log('IntersectionObserver destroyed')
    }
  }

  createOrDestroyIntersectionObserver()

  // --------------------------------------------------------------------

  function addOrRemoveMouseoverHandler() {
    $('.commentarea').off('mouseenter.spiralx')

    if (settings.expandOnMouseOver) {
      const hoveredElems = new WeakMap()

      $('.commentarea')
        .on('mouseenter.spiralx', '.expand-text', function() {
          const elem = this

          const timeoutId = setTimeout(() => {
            hoveredElems.delete(elem)
            elem.click()
          }, settings.expandOnMouseOverDelay)

          hoveredElems.set(elem, timeoutId)
        })
        .on('mouseleave.spiralx', '.expand-text', function() {
          const timeoutId = hoveredElems.get(this)

          if (timeoutId) {
            clearTimeout(timeoutId)
            hoveredElems.delete(this)
          }
        })
    }
  }

  addOrRemoveMouseoverHandler()

  // --------------------------------------------------------------------

  function markAsExpand(selectorOrElements, observe = true) {
    const $elems = $(selectorOrElements)
      .addClass('expand-inline')
      .children('a')
      .wrapInner('<span class="expand-text"></span>')

    if (intersectionObserver) {
      $elems.each(function() {
        intersectionObserver.observe(this.firstElementChild)
      })
    }
  }

  // --------------------------------------------------------------------

  // Uncollapse non-top level comments on page load
  uncollapseAllComments($('.thing.comment .thing.comment.collapsed'))

  const observer = new MutationSummary({
    callback([ deepThreadSpans, moreCommentsSpans, comments ]) {
      // console.log(`Added ${deepThreadSpans.added.length} deep thread spans and ${moreCommentsSpans.added.length} more comment spans`)

      markAsExpand(moreCommentsSpans.added)

      processDeepThreadSpans(deepThreadSpans.added)

      const $collapsedComments = $(comments.added).filter('.collapsed')
      uncollapseAllComments($collapsedComments)
    },
    rootNode: document.body,
    queries: [
      { element: 'span.deepthread' },
      { element: 'span.morecomments' },
      { element: '.thing.comment' },
    ]
  })

  // To process spans in the HTML source
  markAsExpand('span.morecomments', false)

  processDeepThreadSpans($('span.deepthread'))

  // --------------------------------------------------------------------

  $(document.body).append(`<style type="text/css">
    .expand-inline {
      display: block;
      padding: 0;
    }
    .expand-inline:after {
      display: none !important;
    }
    .expand-inline a {
      display: block;
      text-align: left;
    }
    .expand-inline a:before {
      content: "↘️";
      padding-right: 0.4em;
    }
    .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, MutationSummary?.MutationSummary)

jQuery.noConflict(true)