// ==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.9
// @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
// @connect instagram.com
// ==/UserScript==
const CLASS = 'reddit-inline-media';
const MORE_SELECTOR = '[id^="moreComments-"] p';
const RULES = [
{r:/^https?:\/\/(i\.)?imgur\.com\/a\/(\w+)$/i,
s:'https://imgur.com/ajaxalbums/getimages/$2/hit.json?all=true',
q:(json, text) =>
Array.from(((json || {}).data || {}).images || [])
.map(img => img && `https://i.imgur.com/${img.hash}${img.ext}`),
},
{r:/^https?:\/\/(i\.)?imgur\.com\/\w+$/i, q:'link[rel="image_src"], meta[name="twitter:player:stream"]'},
{r:/^https?:\/\/streamable\.com\/.+/i, q:'video'},
{r:/^https?:\/\/gfycat\.com\/.+/i, q:'source[src*=".webm"]'},
{r:/^https?:\/\/(www\.)?instagram\.com\/p\/[^/]+\/?$/i, q:'meta[property="og:image"]'},
{r:/\.gifv$/i, s:'.mp4'},
{r:/\.(jpe?g|png|gif|webm|mp4)$/i},
];
GM_addStyle(`
.${CLASS} {
max-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;
const text = a.textContent.trim();
if (
!/^https?:\/\/\S+?\.{3}$/.test(text) && (
a.parentNode.localName === 'p' &&
a.parentNode.textContent.trim().length > text.length ||
!a.closest('.scrollerItem,' +
'[data-test-id="post-content"],' +
`img[src="${url}"] + * a[href="${url}"]`)
)
) {
(q ? expandRemote : expand)(item);
}
}
}
function expandRemote({a, url, q}) {
GM_xmlhttpRequest({
url,
method: 'GET',
onload: r => {
const isJSON = /^content-type:.*?json\s*$/mi.test(r.responseHeaders);
const doc = isJSON ?
tryJSONparse(r.response) :
new DOMParser().parseFromString(r.response, 'text/html');
if (typeof q === 'string') {
if (isJSON) return;
const el = doc && doc.querySelector(q);
const url = el && (el.href || el.src || el.content);
if (url) expand({a, url});
return;
}
let urls;
if (typeof q === 'function') {
try {
urls = q(doc, r.response);
} catch (e) {}
}
if (!urls || !urls.length) return;
for (const url of Array.isArray(urls) ? urls : [urls]) {
if (!url) continue;
a = expand({a, url});
}
},
});
}
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);
}
function tryJSONparse(str) {
try {
return JSON.parse(str);
} catch (e) {
return undefined;
}
}