Reddit expand media and comments

Shows pictures and some videos right after the link, loads and expands comment threads.

Fra og med 23.08.2018. Se den nyeste version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           Reddit expand media and comments
// @description    Shows pictures and some videos right after the link, loads and expands comment threads.
// @version        0.0.3
// @author         wOxxOm
// @namespace      wOxxOm.scripts
// @license        MIT License
// @match          *://*.reddit.com/*
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @connect        imgur.com
// @connect        gfycat.com
// @connect        streamable.com
// ==/UserScript==

const CLASS = 'reddit-inline-media';
const MORE_SELECTOR = '[id^="moreComments-"] p';
const RULES = [
  {r:/^https?:\/\/imgur\.com\/a\/.+/i, q:'link[rel="image_src"]'},
  {r:/^https?:\/\/streamable\.com\/.+/i, q:'video'},
  {r:/^https?:\/\/gfycat\.com\/.+/i, q:'#webmSource'},
  {r:/\.gifv$/i, s:'.mp4'},
  {r:/\.(jpe?g|png|gif|webm|mp4)$/i},
];

GM_addStyle(`
  .${CLASS} {
    width: 100%;
    display: block;
  }
  .${CLASS}:hover {
    outline: 2px solid #3bbb62;
  }
`);

const isChrome = navigator.userAgent.includes('Chrom');

new MutationObserver(onMutation)
  .observe(document.body, {subtree: true, childList: true});

onMutation([{
  addedNodes: [document.body]
}]);

const scrollObserver = new IntersectionObserver(expandComments, {
  rootMargin: window.innerHeight + 'px',
});

function onMutation(mutations) {
  const items = [];
  let someElementsAdded = false;
  for (var i = 0, m; (m = mutations[i++]);) {
    for (var j = 0, added = m.addedNodes, node; (node = added[j++]);) {
      if (node.nodeType !== 1) continue; // Node.ELEMENT_NODE
      someElementsAdded = true;
      if (node.localName === 'a') {
        const data = preprocess(node);
        if (data) items.push(data);
        continue;
      }
      if (!node.children[0]) continue;
      var aa = node.getElementsByTagName('a');
      for (var k = 0, a; (a = aa[k++]);) {
        const data = preprocess(a);
        if (data) items.push(data);
      }
    }
  }
  if (someElementsAdded) debounce(observeShowMore);
  if (items.length) setTimeout(process, 0, items);
}

function preprocess(a) {
  let url = a.href;
  for (const {r, s, q} of RULES) {
    if (typeof r === 'string') {
      if (!url.includes(r)) continue;
    } else {
      if (!r.test(url)) continue;
      if (s) url = url.replace(r, s);
    }
    return {a, url, q};
  }
}

function process(items) {
  for (const item of items) {
    const {a, url, q} = item;
    if (!/^https?:\/\/\S+?\.{3}$/.test(a.textContent) &&
        !a.closest('[data-test-id="post-content"], .scrollerItem') &&
        !a.closest(`img[src="${url}"] + * a[href="${url}"]`)) {
      q ? expandRemote(item) : expand(item);
    }
  }
}

function expandRemote({a, url, q}) {
  GM_xmlhttpRequest({
    url,
    method: 'GET',
    onload: r => {
      const doc = new DOMParser().parseFromString(r.response, 'text/html');
      const el = doc && doc.querySelector(q);
      if (el) expand({a, url: el.href || el.src});
    },
  });
}

function expand({a, url = a.href}) {
  const isVideo = /(webm|gifv|mp4)(\?.*)?$/i.test(url);
  const el = document.createElement(isVideo ? 'video' : 'img');
  el.src = url;
  el.className = CLASS;
  a.insertAdjacentElement('afterend', el);
  if (isVideo) {
    el.controls = true;
    el.preload = 'metadata';
    if (isChrome) el.addEventListener('click', playOnClick);
  }
  return el;
}

function observeShowMore() {
  const more = document.querySelector(MORE_SELECTOR);
  if (!more) return;
  for (const el of document.querySelectorAll(MORE_SELECTOR)) {
    scrollObserver.observe(el);
  }
}

function expandComments(entries) {
  for (const e of entries) {
    if (!e.isIntersecting) continue;
    e.target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  }
}

function playOnClick(event, el, wasPaused) {
  if (!el) {
    setTimeout(playOnClick, 0, event, this, this.paused);
  } else if (el.paused === wasPaused) {
    wasPaused ? el.play() : el.pause();
  }
}

function debounce(fn, timeout = 0, ...args) {
  clearTimeout(fn.__timeout);
  fn.__timeout = setTimeout(fn, timeout, ...args);
}