// ==UserScript==
// @name Reddit Cache
// @namespace https://www.reddit.dynu.net
// @version 0.1
// @description Restores edits and deletes on Reddit
// @author /u/PortugalCache
// @match https://www.reddit.com/r/portugal/comments/*/*/
// @exclude https://www.reddit.com/r/portugal/comments/*/*/*/
// @grant GM_addStyle
// @require https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js
// ==/UserScript==
GM_addStyle('.ins, .ins * { background-color: #dfffd1!important; }');
GM_addStyle('.del, .del * { background-color: #ffd1d1!important; }');
(function() {
'use strict';
var postId = location.pathname.match(/comments\/(.+?)\//i)[1];
var postCacheUrl = 'https://www.reddit.dynu.net'+location.pathname;
// time ago
var timeSince = function(date) {
var format = function (intervalType) { return interval + ' ' + intervalType + (interval > 1 || interval === 0 ? 's' : '') + ' ago'; };
var seconds = Math.floor((new Date() - date) / 1000);
var interval = Math.floor(seconds / 31536000);
if (interval >= 1) return format('year');
if ((interval = Math.floor(seconds / 2592000)) >= 1) return format('month');
if ((interval = Math.floor(seconds / 86400)) >= 1) return format('day');
if ((interval = Math.floor(seconds / 3600)) >= 1) return format('hour');
if ((interval = Math.floor(seconds / 60)) >= 1) return format('minute');
interval = seconds;
return format('second');
};
// time html
var timeHtml = function (timestamp) {
var date = new Date(timestamp*1000);
return `<time title="${date.toString()}" datetime="${date.toISOString()}" class="">${timeSince(date)}</time>`;
};
// compare html strings
var compareHtml = function(html1, html2) {
var el1 = document.createElement('span'); el1.innerHTML = html1;
var el2 = document.createElement('span'); el2.innerHTML = html2;
return el1.innerHTML == el2.innerHTML;
};
// comment text html
var editsHtml = function (plainTexts, original) {
var html = '', text = '', lastHtml, edits = Object.keys(plainTexts), onlyIns = true, onlyDel = true, texts = {};
for (var i in plainTexts) texts[i] = marked(plainTexts[i]);
if (original && !compareHtml(original.replace(/\n/g, ''), texts[edits[edits.length-1]].replace(/\n/g, ''))) {
var i = Math.floor(Date.now()/1000);
texts[i] = original;
edits.push(i);
}
if (edits.length == 1) return texts[edits[0]];
for (var i in texts) {
if (edits.length != 1) html += '<p class="tagline" style="font-size: 11px; font-weight: bold;">'+(Number(i) ? 'Edited ' + timeHtml(i) : 'Original')+':</p>';
html += lastHtml = text ? htmlDiff(text, text = texts[i]) : text = texts[i];
//console.log(lastHtml);
if (/<span class="del">/.test(lastHtml)) onlyIns = false;
if (/<span class="ins">/.test(lastHtml)) onlyDel = false;
}
//console.log(onlyIns, onlyDel);
if (onlyIns || onlyDel) return htmlDiff(texts[edits[0]], text);
return text + `<div><a href="javascript:" onclick="this.parentNode.childNodes[1].style.display = (this.parentNode.childNodes[1].style.display ? '' : 'none');">Show ${edits.length-1} edits</a><div style="display: none;">${html}</div></div>`;
};
// comment html
var commentHtml = function (id, comment, childInnerHTML) {
return `
<div class="midcol unvoted">
<div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0"></div>
<div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0"></div>
</div>
<div class="entry unvoted">
<p class="tagline">
<a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a>
<a href="https://www.reddit.com/user/${comment.author}" class="author may-blank id-t2_ydhqk">${comment.author}</a>
<span class="userattrs"></span>
<span class="score dislikes"></span>
<span class="score unvoted"></span>
<span class="score likes"></span>
${timeHtml(comment.created)}
<span class="del">DELETED</span>
<a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(- childs)</a>
</p>
<form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_${id}6t3"><input type="hidden" name="thing_id" value="t1_${id}">
<div class="usertext-body may-blank-within md-container ">
<div class="md">${editsHtml(comment.text)}</div>
</div>
</form>
<ul class="flat-list buttons">
<li class="first"><a href="${postCacheUrl+id}/" target="_blank" data-event-action="permalink" class="bylink" rel="nofollow">cachelink</a></li>
</ul>
<div class="reportform report-t1_${id}"></div>
</div>
<div class="child">${childInnerHTML ? childInnerHTML : ''}</div>
<div class="clearleft"></div>`;
};
// retrieve ids of visible comments
var els = document.getElementsByClassName('parent'), visibleComments = [];
for (var i = 0; i < els.length; i++) {
var el = els[i].firstChild;
if (el) visibleComments.push(el.getAttribute('name'));
}
// retrieve ids of orphan comments
var orphanComments = [];
var deletedComments = document.getElementsByClassName('deleted');
for (var i = 0; i < deletedComments.length; i++) {
orphanComments.push(deletedComments[i].getElementsByClassName('child')[0].childNodes[0].childNodes[0].id.replace(/^thing_t1_/, ''));
}
// the request to the cache site
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var result = JSON.parse(this.responseText);
console.log(result);
for (var id in result.edits) {
// add edits
var md = document.querySelector('#thing_'+id+' .md');
if (md) {
md.innerHTML = editsHtml(result.edits[id], md.innerHTML);
// add cachelink button
var buttons = document.querySelector('#thing_'+id+' .buttons');
var button = document.createElement('li');
button.innerHTML = '<a href="'+postCacheUrl+id.replace(/^t1_/, '')+'/" target="_blank">cachelink</a>'; // fixme: won't work for posts (t3)
buttons.insertBefore(button, buttons.childNodes[1]);
}
else console.log('could not add edits to ' + id);
}
// restore parent of orphan comments
var deletedComments = document.getElementsByClassName('deleted');
for (var i = 0; i < deletedComments.length; i++) {
var childsDiv = deletedComments[i].getElementsByClassName('child')[0];
var orphanId = childsDiv.childNodes[0].childNodes[0].id.replace(/^thing_t1_/, '');
var parent = result.parents && result.parents[orphanId];
if (parent) {
deletedComments[i].innerHTML = commentHtml(parent.id, parent, childsDiv.innerHTML);
deletedComments[i].className = deletedComments[i].className.replace(/deleted/i,'');
}
else console.log('parent not received for ' + orphanId);
}
// restore remaining deleted comments
var siteTable = document.getElementById('siteTable_t3_'+postId, true);
for (var id in result.dels) {
var comment = result.dels[id];
var parentEl = comment.parent_id === "" ? siteTable : document.querySelector('#thing_t1_'+comment.parent_id+' .child');
if (parentEl) {
var div = document.createElement('div');
div.className = 'thing comment noncollapsed';
div.innerHTML = commentHtml(id, comment);
parentEl.appendChild(div);
}
else console.log('could not add deleted comment' + id + ' to ' + comment.parent_id);
}
}
};
xhttp.open("POST", "https://www.reddit.dynu.net/?gm&p="+postId, true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("c="+visibleComments.join(',')+'&o='+orphanComments.join(','));
})();
// compute diff between two strings
this.diff = function(oldStr, newStr) {
var array_keys = function (a, s) {
var r = [];
if (!a) return r;
for (var i = 0; i < a.length; i++) {
if (a[i] == s) r.push(i);
}
return r;
};
var maxlen = 0, omax = 0, nmax = 0, matrix = [];
for (var oindex = 0; oindex < oldStr.length; oindex++) {
var ovalue = oldStr[oindex];
var nkeys = array_keys(newStr, ovalue);
for (var i = 0; i < nkeys.length; i++) {
var nindex = nkeys[i];
if (!matrix[oindex]) matrix[oindex] = [];
matrix[oindex][nindex] = (matrix[oindex - 1] && matrix[oindex - 1][nindex - 1]) ?
matrix[oindex - 1][nindex - 1] + 1 : 1;
if (matrix[oindex][nindex] > maxlen) {
maxlen = matrix[oindex][nindex];
omax = oindex + 1 - maxlen;
nmax = nindex + 1 - maxlen;
}
}
}
if (maxlen === 0) return [{d: oldStr, i: newStr}];
return diff(oldStr.slice(0, omax), newStr.slice(0, nmax)).concat(
newStr.slice(nmax, nmax + maxlen)).concat(
diff(oldStr.slice(omax + maxlen), newStr.slice(nmax + maxlen)));
};
// pretty print the diff
this.htmlDiff = function(oldStr, newStr) {
var explode = function (str) { return str.trim().match(/<.+?>|[a-záéíóúâêîôûãõç0-9]+[ \n]*|[^a-záéíóúâêîôûãõç0-9]/ig); };
var diffResult = diff(explode(oldStr), explode(newStr));
var ret = '';
for (var i = 0; i < diffResult.length; i++) {
var k = diffResult[i];
if (k instanceof Object) {
ret += (k.d.length ? '<span class="del">'+k.d.join('')+"</span>" : '') +
(k.i.length ? '<span class="ins">'+k.i.join('')+"</span>" : '');
}
else ret += k;
}
return ret;
};