';
}
if (rebuildButtons) {
ThreadWatcher.rebuildButtons();
}
ThreadWatcher.listNode.innerHTML = html;
};
ThreadWatcher.rebuildButtons = function() {
var i, buttons, key, btn;
buttons = $.cls('wbtn');
for (i = 0; btn = buttons[i]; ++i) {
key = btn.getAttribute('data-id') + '-' + Main.board;
if (ThreadWatcher.watched[key]) {
if (!btn.hasAttribute('data-active')) {
btn.src = Main.icons.watched;
btn.setAttribute('data-active', '1');
}
}
else {
if (btn.hasAttribute('data-active')) {
btn.src = Main.icons.notwatched;
btn.removeAttribute('data-active');
}
}
}
};
ThreadWatcher.initMobileButtons = function() {
var el, cnt, key, ref;
el = document.createElement('img');
key = Main.tid + '-' + Main.board;
if (ThreadWatcher.watched[key]) {
el.src = Main.icons.watched;
el.setAttribute('data-active', '1');
}
else {
el.src = Main.icons.notwatched;
}
el.className = 'extButton wbtn wbtn-' + key;
el.setAttribute('data-cmd', 'watch');
el.setAttribute('data-id', Main.tid);
el.alt = 'W';
cnt = document.createElement('span');
cnt.className = 'mobileib button';
cnt.appendChild(el);
if (ref = $.cls('navLinks')[0]) {
ref.appendChild(document.createTextNode(' '));
ref.appendChild(cnt);
}
if (ref = $.cls('navLinks')[3]) {
ref.appendChild(document.createTextNode(' '));
ref.appendChild(cnt.cloneNode(true));
}
};
ThreadWatcher.onClick = function(e) {
var t = e.target;
if (t.hasAttribute('data-id')) {
ThreadWatcher.toggle(
t.getAttribute('data-id'),
t.getAttribute('data-board')
);
}
else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) {
ThreadWatcher.refreshWithAutoWatch();
}
else if (t.id == 'twClose') {
ThreadWatcher.toggleList();
}
};
ThreadWatcher.generateLabel = function(sub, com, tid) {
var label;
if (label = sub) {
label = label.slice(0, this.charLimit);
}
else if (label = com) {
label = label.replace(/(?: )+/g, ' ')
.replace(/<[^>]*?>/g, '').slice(0, this.charLimit);
}
else {
label = 'No.' + tid;
}
return label;
};
ThreadWatcher.toggle = function(tid, board) {
var key;
key = tid + '-' + (board || Main.board);
if (this.watched[key]) {
this.blacklisted[key] = 1;
delete this.watched[key];
}
else {
this.add(tid, board, key);
}
this.save();
this.load();
this.build(true);
};
ThreadWatcher.add = function(tid, board) {
var key, label, sub, com, lastReply, thread;
key = tid + '-' + (board || Main.board);
sub = $.cls('subject', $.id('pi' + tid))[0].textContent;
com = $.id('m' + tid).innerHTML;
label = this.generateLabel(sub, com, tid);
if ((thread = $.id('t' + tid)).children[1]) {
lastReply = thread.lastElementChild.id.slice(2);
}
else {
lastReply = tid;
}
this.watched[key] = [ label, lastReply, 0 ];
};
ThreadWatcher.addRaw = function(post, board) {
var key, label;
key = post.no + '-' + (board || Main.board);
if (this.watched[key]) {
return;
}
label = ThreadWatcher.generateLabel(post.sub, post.com, post.no);
this.watched[key] = [ label, 0, 0 ];
};
ThreadWatcher.save = function() {
var i;
ThreadWatcher.sortByBoard();
localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched));
//StorageSync.sync('4chan-watch');
for (i in ThreadWatcher.blacklisted) {
localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted));
//StorageSync.sync('4chan-watch-bl');
break;
}
};
ThreadWatcher.sortByBoard = function() {
var i, self, key, sorted, keys;
self = ThreadWatcher;
sorted = {};
keys = [];
for (key in self.watched) {
keys.push(key);
}
keys.sort(function(a, b) {
a = a.split('-')[1];
b = b.split('-')[1];
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
for (i = 0; key = keys[i]; ++i) {
sorted[key] = self.watched[key];
}
self.watched = sorted;
};
ThreadWatcher.canAutoRefresh = function() {
var time;
if (time = localStorage.getItem('4chan-tw-timestamp')) {
return Date.now() - (+time) >= 60000;
}
return true;
};
ThreadWatcher.setRefreshTimestamp = function() {
localStorage.setItem('4chan-tw-timestamp', Date.now());
//StorageSync.sync('4chan-tw-timestamp');
};
ThreadWatcher.refreshWithAutoWatch = function() {
var i, f, count, board, boards, img;
if (!Config.filter) {
this.refresh();
return;
}
Filter.load();
boards = {};
count = 0;
for (i = 0; f = Filter.activeFilters[i]; ++i) {
if (!f.auto || !f.boards) {
continue;
}
for (board in f.boards) {
if (boards[board]) {
continue;
}
boards[board] = true;
++count;
}
}
if (!count) {
this.refresh();
return;
}
img = $.id('twPrune');
img.src = Main.icons.rotate;
this.isRefreshing = true;
this.fetchCatalogs(boards, count);
};
ThreadWatcher.fetchCatalogs = function(boards, count) {
var to, board, catalogs, meta;
catalogs = {};
meta = { count: count };
to = 0;
for (board in boards) {
setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta);
to += 200;
}
};
ThreadWatcher.fetchCatalog = function(board, catalogs, meta) {
var xhr;
xhr = new XMLHttpRequest();
xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json');
xhr.onload = function() {
meta.count--;
catalogs[board] = Parser.parseCatalogJSON(this.responseText);
if (!meta.count) {
ThreadWatcher.onCatalogsLoaded(catalogs);
}
};
xhr.onerror = function() {
meta.count--;
if (!meta.count) {
ThreadWatcher.onCatalogsLoaded(catalogs);
}
};
xhr.send(null);
};
ThreadWatcher.onCatalogsLoaded = function(catalogs) {
var i, j, board, page, pages, threads, thread, key, blacklisted;
$.id('twPrune').src = Main.icons.refresh;
this.isRefreshing = false;
blacklisted = {};
for (board in catalogs) {
pages = catalogs[board];
for (i = 0; page = pages[i]; ++i) {
threads = page.threads;
for (j = 0; thread = threads[j]; ++j) {
key = thread.no + '-' + board;
if (this.blacklisted[key]) {
blacklisted[key] = 1;
continue;
}
if (Filter.match(thread, board)) {
this.addRaw(thread, board);
}
}
}
}
this.blacklisted = blacklisted;
this.build(true);
this.refresh();
};
ThreadWatcher.refresh = function() {
var i, to, key, total, img;
if (total = $.id('watchList').children.length) {
i = to = 0;
img = $.id('twPrune');
img.src = Main.icons.rotate;
ThreadWatcher.isRefreshing = true;
ThreadWatcher.setRefreshTimestamp();
for (key in ThreadWatcher.watched) {
setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null);
to += 200;
}
}
};
ThreadWatcher.refreshCurrent = function(rebuild) {
var key, thread, lastReply;
key = Main.tid + '-' + Main.board;
if (this.watched[key]) {
if ((thread = $.id('t' + Main.tid)).children[1]) {
lastReply = thread.lastElementChild.id.slice(2);
}
else {
lastReply = Main.tid;
}
if (this.watched[key][1] < lastReply) {
this.watched[key][1] = lastReply;
}
this.watched[key][2] = 0;
this.watched[key][4] = 0;
this.save();
if (rebuild) {
this.build();
}
}
};
ThreadWatcher.setLastRead = function(pid, tid) {
var key = tid + '-' + Main.board;
if (this.watched[key]) {
this.watched[key][1] = pid;
this.watched[key][2] = 0;
this.watched[key][4] = 0;
this.save();
this.build();
}
};
ThreadWatcher.onRefreshEnd = function(img) {
img.src = Main.icons.refresh;
this.isRefreshing = false;
this.save();
this.load();
this.build();
};
ThreadWatcher.fetch = function(key, img) {
var tuid, xhr, li;
li = $.id('watch-' + key);
if (ThreadWatcher.watched[key][1] == -1) {
delete ThreadWatcher.watched[key];
li.parentNode.removeChild(li);
if (img) {
ThreadWatcher.onRefreshEnd(img);
}
return;
}
tuid = key.split('-'); // tid, board
xhr = new XMLHttpRequest();
xhr.onload = function() {
var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j;
if (this.status == 200) {
posts = Parser.parseThreadJSON(this.responseText);
lastReply = ThreadWatcher.watched[key][1];
newReplies = 0;
if (!ThreadWatcher.watched[key][4]) {
trackedReplies = Parser.getTrackedReplies(tuid[1], tuid[0]);
if (trackedReplies) {
dummy = document.createElement('div');
}
}
else {
trackedReplies = null;
}
for (i = posts.length - 1; i >= 1; i--) {
if (posts[i].no <= lastReply) {
break;
}
++newReplies;
if (trackedReplies) {
dummy.innerHTML = posts[i].com;
quotelinks = $.cls('quotelink', dummy);
if (!quotelinks[0]) {
continue;
}
for (j = 0; q = quotelinks[j]; ++j) {
if (trackedReplies[q.textContent]) {
ThreadWatcher.watched[key][4] = 1;
trackedReplies = null;
break;
}
}
}
}
if (newReplies > ThreadWatcher.watched[key][2]) {
ThreadWatcher.watched[key][2] = newReplies;
}
if (posts[0].archived) {
ThreadWatcher.watched[key][3] = 1;
}
}
else if (this.status == 404) {
ThreadWatcher.watched[key][1] = -1;
}
if (img) {
ThreadWatcher.onRefreshEnd(img);
}
};
if (img) {
xhr.onerror = xhr.onload;
}
xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json');
xhr.send(null);
};
/**
* Thread expansion
*/
var ThreadExpansion = {};
ThreadExpansion.init = function() {
this.enabled = UA.hasCORS;
this.fetchXhr = null;
};
ThreadExpansion.expandComment = function(link) {
var ids, tid, pid, abbr;
if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) {
return;
}
tid = ids[1];
pid = ids[2];
abbr = link.parentNode;
abbr.textContent = 'Loading...';
$.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json',
{
onload: function() {
var i, msg, post, posts;
if (this.status == 200) {
msg = $.id('m' + pid);
posts = Parser.parseThreadJSON(this.responseText);
if (tid == pid) {
post = posts[0];
}
else {
for (i = posts.length - 1; i > 0; i--) {
if (posts[i].no == pid) {
post = posts[i];
break;
}
}
}
if (post) {
post = Parser.buildHTMLFromJSON(post, Main.board);
msg.innerHTML = $.cls('postMessage', post)[0].innerHTML;
if (Parser.prettify) {
Parser.parseMarkup(msg);
}
if (window.math_tags) {
Parser.parseMathOne(msg);
}
}
else {
abbr.textContent = "This post doesn't exist anymore.";
}
}
else if (this.status == 404) {
abbr.textContent = "This thread doesn't exist anymore.";
}
else {
abbr.textContent = 'Connection Error';
console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText);
}
},
onerror: function() {
abbr.textContent = 'Connection Error';
console.log('ThreadExpansion: xhr failed');
}
}
);
};
ThreadExpansion.toggle = function(tid) {
var thread, msg, expmsg, summary, tmp;
thread = $.id('t' + tid);
summary = thread.children[1];
if (thread.hasAttribute('data-truncated')) {
msg = $.id('m' + tid);
expmsg = msg.nextSibling;
}
if ($.hasClass(thread, 'tExpanded')) {
thread.className = thread.className.replace(' tExpanded', ' tCollapsed');
summary.children[0].src = Main.icons.plus;
summary.children[1].style.display = 'inline';
summary.children[2].style.display = 'none';
if (msg) {
tmp = msg.innerHTML;
msg.innerHTML = expmsg.textContent;
expmsg.textContent = tmp;
}
}
else if ($.hasClass(thread, 'tCollapsed')) {
thread.className = thread.className.replace(' tCollapsed', ' tExpanded');
summary.children[0].src = Main.icons.minus;
summary.children[1].style.display = 'none';
summary.children[2].style.display = 'inline';
if (msg) {
tmp = msg.innerHTML;
msg.innerHTML = expmsg.textContent;
expmsg.textContent = tmp;
}
}
else {
summary.children[0].src = Main.icons.rotate;
if (!ThreadExpansion.fetchXhr) {
ThreadExpansion.fetch(tid);
}
}
};
ThreadExpansion.fetch = function(tid) {
ThreadExpansion.fetchXhr = $.get(
'//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json',
{
onload: function() {
var i, p, n, frag, thread, tail, posts, msg, metacap,
expmsg, summary, abbr;
ThreadExpansion.fetchXhr = null;
thread = $.id('t' + tid);
summary = thread.children[1];
if (this.status == 200) {
tail = $.cls('reply', thread);
posts = Parser.parseThreadJSON(this.responseText);
if (!Config.revealSpoilers && posts[0].custom_spoiler) {
Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler);
}
frag = document.createDocumentFragment();
if (tail[0]) {
tail = +tail[0].id.slice(1);
for (i = 1; p = posts[i]; ++i) {
if (p.no < tail) {
n = Parser.buildHTMLFromJSON(p, Main.board);
n.className += ' rExpanded';
frag.appendChild(n);
}
else {
break;
}
}
}
else {
for (i = 1; p = posts[i]; ++i) {
n = Parser.buildHTMLFromJSON(p, Main.board);
n.className += ' rExpanded';
frag.appendChild(n);
}
}
msg = $.id('m' + tid);
if ((abbr = $.cls('abbr', msg)[0])
&& /^Comment/.test(abbr.textContent)) {
thread.setAttribute('data-truncated', '1');
expmsg = document.createElement('div');
expmsg.style.display = 'none';
expmsg.textContent = msg.innerHTML;
msg.parentNode.insertBefore(expmsg, msg.nextSibling);
if (metacap = $.cls('capcodeReplies', msg)[0]) {
msg.innerHTML = posts[0].com + '
';
msg.appendChild(metacap);
}
else {
msg.innerHTML = posts[0].com;
}
if (Parser.prettify) {
Parser.parseMarkup(msg);
}
if (window.math_tags) {
Parser.parseMathOne(msg);
}
}
thread.insertBefore(frag, summary.nextSibling);
Parser.parseThread(tid, 1, i - 1);
thread.className += ' tExpanded';
summary.children[0].src = Main.icons.minus;
summary.children[1].style.display = 'none';
summary.children[2].style.display = 'inline';
}
else if (this.status == 404) {
summary.children[0].src = Main.icons.plus;
summary.children[0].display = 'none';
summary.children[1].textContent = "This thread doesn't exist anymore.";
}
else {
summary.children[0].src = Main.icons.plus;
console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText);
}
},
onerror: function() {
ThreadExpansion.fetchXhr = null;
$.id('t' + tid).children[1].children[0].src = Main.icons.plus;
console.log('ThreadExpansion: xhr failed');
}
}
);
};
/**
* Thread updater
*/
var ThreadUpdater = {};
ThreadUpdater.init = function() {
if (!UA.hasCORS || window.thread_archived) {
return;
}
this.enabled = true;
this.pageTitle = document.title;
this.unreadCount = 0;
this.auto = this.hadAuto = false;
this.delayId = 0;
this.delayIdHidden = 4;
this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ];
this.timeLeft = 0;
this.interval = null;
this.tailSize = window.tailSize || 0;
this.lastUpdated = Date.now();
this.lastModified = '0';
this.lastModifiedTail = '0';
this.lastReply = null;
this.currentIcon = null;
this.iconPath = '//s.4cdn.org/image/';
this.iconNode = $.qs('link[rel="shortcut icon"]', document.head);
this.iconNode.type = 'image/x-icon';
this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, '');
this.deletionQueue = {};
if (Config.updaterSound) {
this.audioEnabled = false;
this.audio = document.createElement('audio');
this.audio.src = '//s.4cdn.org/media/beep.ogg';
}
this.hidden = 'hidden';
this.visibilitychange = 'visibilitychange';
this.adsTTL = 900000;
this.adsReloadTs = 0;
if (typeof document.hidden === 'undefined') {
if ('mozHidden' in document) {
this.hidden = 'mozHidden';
this.visibilitychange = 'mozvisibilitychange';
}
else if ('webkitHidden' in document) {
this.hidden = 'webkitHidden';
this.visibilitychange = 'webkitvisibilitychange';
}
else if ('msHidden' in document) {
this.hidden = 'msHidden';
this.visibilitychange = 'msvisibilitychange';
}
}
this.initControls();
document.addEventListener('scroll', this.onScroll, false);
if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) {
this.start();
}
};
ThreadUpdater.apiUrlFilter = null;
ThreadUpdater.buildMobileControl = function(el, bottom) {
var wrap, cnt, ctrl, cb, label, oldBtn, btn;
bottom = (bottom ? 'Bot' : '');
wrap = document.createElement('div');
wrap.className = 'btn-row';
// Update button
oldBtn = el.parentNode;
btn = oldBtn.cloneNode(true);
btn.innerHTML = '';
wrap.appendChild(btn);
cnt = el.parentNode.parentNode;
ctrl = document.createElement('span');
ctrl.className = 'mobileib button';
// Auto checkbox
label = document.createElement('label');
cb = document.createElement('input');
cb.type = 'checkbox';
cb.setAttribute('data-cmd', 'auto');
this['autoNode' + bottom] = cb;
label.appendChild(cb);
label.appendChild(document.createTextNode('Auto'));
ctrl.appendChild(label);
wrap.appendChild(document.createTextNode(' '));
wrap.appendChild(ctrl);
// Status label
label = document.createElement('div');
label.className = 'mobile-tu-status';
wrap.appendChild(this['statusNode' + bottom] = label);
cnt.appendChild(wrap);
// Remove Update button
oldBtn.parentNode.removeChild(oldBtn);
if (cnt = $.id('mpostform')) {
cnt.parentNode.style.marginTop = '';
}
};
ThreadUpdater.buildDesktopControl = function(bottom) {
var frag, el, label, navlinks;
bottom = (bottom ? 'Bot' : '');
frag = document.createDocumentFragment();
// Update button
frag.appendChild(document.createTextNode(' ['));
el = document.createElement('a');
el.href = '';
el.textContent = 'Update';
el.setAttribute('data-cmd', 'update');
frag.appendChild(el);
frag.appendChild(document.createTextNode(']'));
// Auto checkbox
frag.appendChild(document.createTextNode(' ['));
label = document.createElement('label');
el = document.createElement('input');
el.type = 'checkbox';
el.title = 'Fetch new replies automatically';
el.setAttribute('data-cmd', 'auto');
this['autoNode' + bottom] = el;
label.appendChild(el);
label.appendChild(document.createTextNode('Auto'));
frag.appendChild(label);
frag.appendChild(document.createTextNode('] '));
if (Config.updaterSound) {
// Sound checkbox
frag.appendChild(document.createTextNode(' ['));
label = document.createElement('label');
el = document.createElement('input');
el.type = 'checkbox';
el.title = 'Play a sound on new replies to your posts';
el.setAttribute('data-cmd', 'sound');
this['soundNode' + bottom] = el;
label.appendChild(el);
label.appendChild(document.createTextNode('Sound'));
frag.appendChild(label);
frag.appendChild(document.createTextNode('] '));
}
// Status label
frag.appendChild(
this['statusNode' + bottom] = document.createElement('span')
);
if (bottom) {
navlinks = $.cls('navLinks' + bottom)[0];
}
else {
navlinks = $.cls('navLinks')[1];
}
if (navlinks) {
navlinks.appendChild(frag);
}
};
ThreadUpdater.initControls = function() {
// Mobile
if (Main.hasMobileLayout) {
this.buildMobileControl($.id('refresh_top'));
this.buildMobileControl($.id('refresh_bottom'), true);
}
// Desktop
else {
this.buildDesktopControl();
this.buildDesktopControl(true);
}
};
ThreadUpdater.start = function() {
if (this.dead) {
return;
}
this.auto = this.hadAuto = true;
this.autoNode.checked = this.autoNodeBot.checked = true;
this.force = this.updating = false;
if (this.hidden) {
document.addEventListener(this.visibilitychange,
this.onVisibilityChange, false);
}
this.delayId = 0;
this.timeLeft = this.delayRange[0];
this.pulse();
sessionStorage.setItem('4chan-auto-' + Main.tid, 1);
};
ThreadUpdater.stop = function(manual) {
clearTimeout(this.interval);
this.auto = this.updating = this.force = false;
this.autoNode.checked = this.autoNodeBot.checked = false;
if (this.hidden) {
document.removeEventListener(this.visibilitychange,
this.onVisibilityChange, false);
}
if (manual) {
this.setStatus('');
this.setIcon(null);
}
sessionStorage.removeItem('4chan-auto-' + Main.tid);
};
ThreadUpdater.pulse = function() {
var self = ThreadUpdater;
if (self.timeLeft === 0) {
self.update();
}
else {
self.setStatus(self.timeLeft--);
self.interval = setTimeout(self.pulse, 1000);
}
};
ThreadUpdater.adjustDelay = function(postCount)
{
if (postCount === 0) {
if (!this.force) {
if (this.delayId < this.delayRange.length - 1) {
++this.delayId;
}
}
}
else {
this.delayId = document[this.hidden] ? this.delayIdHidden : 0;
}
this.timeLeft = this.delayRange[this.delayId];
if (this.auto) {
this.pulse();
}
};
ThreadUpdater.onVisibilityChange = function() {
var self = ThreadUpdater;
if (document[self.hidden] && self.delayId < self.delayIdHidden) {
self.delayId = self.delayIdHidden;
}
else {
self.delayId = 0;
}
self.timeLeft = self.delayRange[0];
clearTimeout(self.interval);
self.pulse();
};
ThreadUpdater.onScroll = function() {
if (ThreadUpdater.hadAuto &&
(document.documentElement.scrollHeight
<= (Math.ceil(window.innerHeight + window.pageYOffset))
&& !document[ThreadUpdater.hidden])) {
ThreadUpdater.clearUnread();
}
};
ThreadUpdater.clearUnread = function() {
if (!this.dead) {
this.setIcon(null);
}
if (this.lastReply) {
this.unreadCount = 0;
document.title = this.pageTitle;
$.removeClass(this.lastReply, 'newPostsMarker');
this.lastReply = null;
}
};
ThreadUpdater.forceUpdate = function() {
ThreadUpdater.force = true;
ThreadUpdater.update();
};
ThreadUpdater.toggleAuto = function() {
if (this.updating) {
return;
}
this.auto ? this.stop(true) : this.start();
};
ThreadUpdater.toggleSound = function() {
this.soundNode.checked = this.soundNodeBot.checked =
this.audioEnabled = !this.audioEnabled;
};
ThreadUpdater.update = function(full) {
var self, isTail, url;
self = ThreadUpdater;
if (self.updating || self.dead) {
return;
}
clearTimeout(self.interval);
self.updating = true;
self.setStatus('Updating...');
isTail = !full && self.checkTailUpdate();
url = '//a.4cdn.org/' + Main.board + '/thread/' + Main.tid
+ (isTail ? '-tail' : '') + '.json';
if (self.apiUrlFilter) {
url = self.apiUrlFilter(url);
}
$.get(url,
{
onload: self.onload,
onerror: self.onerror,
istail: isTail
},
{
'If-Modified-Since': isTail ? self.lastModifiedTail : self.lastModified
}
);
};
ThreadUpdater.checkTailUpdate = function() {
var self, nodes, el, dtr, dtp;
self = ThreadUpdater;
if (!self.tailSize) {
return false;
}
nodes = $.cls('replyContainer');
el = nodes[nodes.length - self.tailSize];
if (!el) {
return true;
}
el = $.cls('dateTime', el)[0];
dtp = (0 | (self.lastUpdated / 1000)) - (+el.getAttribute('data-utc'));
dtr = 0 | ((Date.now() - self.lastUpdated) / 1000);
return dtp > dtr;
};
ThreadUpdater.checkTailValid = function(posts) {
var op = posts[0];
if (!op || !op.tail_id) {
ThreadUpdater.tailSize = 0;
return false;
}
return !!$.id('p' + op.tail_id);
};
ThreadUpdater.reloadAds = function() {
if (!window.Danbo || !window.reloadAdsDanbo) {
return;
}
let self = ThreadUpdater;
let now = Date.now();
if (!self.adsReloadTs) {
self.adsReloadTs = now;
return;
}
if (now - self.adsReloadTs < self.adsTTL) {
return;
}
self.adsReloadTs = now;
window.reloadAdsDanbo();
};
ThreadUpdater.onload = function() {
var i, state, self, nodes, thread, newposts, frag, lastrep, lastid,
op, doc, autoscroll, count, fromQR, lastRepPos;
self = ThreadUpdater;
nodes = [];
self.setStatus('');
if (this.status == 200) {
newposts = Parser.parseThreadJSON(this.responseText);
if (this.istail) {
if (!self.checkTailValid(newposts)) {
self.updating = false;
self.update(true);
return;
}
self.lastModifiedTail = this.getResponseHeader('Last-Modified');
}
else {
if (newposts[0].tail_size !== self.tailSize) {
self.tailSize = newposts[0].tail_size || 0;
}
self.lastModified = this.getResponseHeader('Last-Modified');
}
thread = $.id('t' + Main.tid);
lastrep = thread.children[thread.childElementCount - 1];
lastid = +lastrep.id.slice(2);
state = !!newposts[0].archived;
if (window.thread_archived !== undefined && state != window.thread_archived) {
QR.enabled && $.id('quickReply') && QR.lock();
Main.setThreadState('archived', state);
}
state = !!newposts[0].closed;
if (state != Main.threadClosed) {
if (newposts[0].archived) {
state = false;
}
else if (QR.enabled && $.id('quickReply')) {
if (state) {
QR.lock();
}
else {
QR.unlock();
}
}
Main.setThreadState('closed', state);
}
state = !!newposts[0].sticky;
if (state != Main.threadSticky) {
Main.setThreadState('sticky', state);
}
state = !!newposts[0].imagelimit;
if (QR.enabled && state != QR.fileDisabled) {
QR.fileDisabled = state;
}
if (!Config.revealSpoilers && newposts[0].custom_spoiler) {
Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler);
}
for (i = newposts.length - 1; i >= 0; i--) {
if (newposts[i].no <= lastid) {
break;
}
nodes.push(newposts[i]);
}
count = nodes.length;
if (count == 1 && QR.lastReplyId == nodes[0].no) {
fromQR = true;
QR.lastReplyId = null;
}
if (count) {
doc = document.documentElement;
autoscroll = (
Config.autoScroll
&& document[self.hidden]
&& doc.scrollHeight == Math.ceil(window.innerHeight + window.pageYOffset)
);
if (window.chrome && document.activeElement) {
if (document.activeElement.href || document.activeElement.type === 'checkbox') {
document.activeElement.blur();
}
}
frag = document.createDocumentFragment();
for (i = nodes.length - 1; i >= 0; i--) {
frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board));
}
thread.appendChild(frag);
lastRepPos = lastrep.offsetTop;
Parser.hasYouMarkers = false;
Parser.hasHighlightedPosts = false;
Parser.parseThread(thread.id.slice(1), -nodes.length);
if (lastRepPos != lastrep.offsetTop) {
window.scrollBy(0, lastrep.offsetTop - lastRepPos);
}
if (!fromQR) {
if (!self.force && doc.scrollHeight > window.innerHeight) {
if (!self.lastReply && lastid != Main.tid) {
(self.lastReply = lastrep.lastChild).className += ' newPostsMarker';
}
if (Parser.hasYouMarkers) {
self.setIcon('rep');
if (self.audioEnabled && document[self.hidden]) {
self.audio.play();
}
}
else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') {
self.setIcon('hl');
}
else if (self.unreadCount === 0) {
self.setIcon('new');
}
self.unreadCount += count;
document.title = '(' + self.unreadCount + ') ' + self.pageTitle;
}
else {
self.setStatus(count + ' new post' + (count > 1 ? 's' : ''));
}
}
if (autoscroll) {
window.scrollTo(0, document.documentElement.scrollHeight);
}
if (Config.threadWatcher) {
ThreadWatcher.refreshCurrent(true);
}
if (Config.threadStats) {
op = newposts[0];
ThreadStats.update(op.replies, op.images, op.unique_ips, op.bumplimit, op.imagelimit);
}
if (self.force) {
self.reloadAds();
}
UA.dispatchEvent('4chanThreadUpdated', { count: count });
}
else {
self.setStatus('No new posts');
}
if (newposts[0].archived) {
self.setError('This thread is archived');
if (!self.dead) {
self.setIcon('dead');
window.thread_archived = true;
self.dead = true;
self.stop();
}
}
}
else if (this.status === 304 || this.status === 0) {
self.setStatus('No new posts');
}
else if (this.status === 404) {
if (this.istail) {
self.updating = false;
self.update(true);
return;
}
self.setIcon('dead');
self.setError('This thread has been pruned or deleted');
self.dead = true;
self.stop();
return;
}
self.lastUpdated = Date.now();
self.adjustDelay(nodes.length);
self.updating = self.force = false;
};
ThreadUpdater.onerror = function() {
var self = ThreadUpdater;
if (UA.isOpera && !this.statusText && this.status === 0) {
self.setStatus('No new posts');
}
else {
self.setError('Connection Error');
}
self.lastUpdated = Date.now();
self.adjustDelay(0);
self.updating = self.force = false;
};
ThreadUpdater.setStatus = function(msg) {
this.statusNode.textContent = this.statusNodeBot.textContent = msg;
};
ThreadUpdater.setError = function(msg) {
this.statusNode.innerHTML
= this.statusNodeBot.innerHTML
= '' + msg + '';
};
ThreadUpdater.setIcon = function(type) {
var icon;
if (type === null) {
icon = this.defaultIcon;
}
else {
icon = this.icons[Main.type + type];
}
this.currentIcon = type;
this.iconNode.href = this.iconPath + icon;
document.head.appendChild(this.iconNode);
};
ThreadUpdater.icons = {
wsnew: 'favicon-ws-newposts.ico',
nwsnew: 'favicon-nws-newposts.ico',
wsrep: 'favicon-ws-newreplies.ico',
nwsrep: 'favicon-nws-newreplies.ico',
wsdead: 'favicon-ws-deadthread.ico',
nwsdead: 'favicon-nws-deadthread.ico',
wshl: 'favicon-ws-newfilters.ico',
nwshl: 'favicon-nws-newfilters.ico'
};
/**
* Thread stats
*/
var ThreadStats = {};
ThreadStats.init = function() {
var cnt;
this.nodeTop = document.createElement('div');
this.nodeTop.className = 'thread-stats';
if (!Main.hasMobileLayout) {
this.nodeBot = this.nodeTop.cloneNode(false);
cnt = $.cls('navLinks');
cnt[1] && cnt[1].appendChild(this.nodeTop);
cnt[2] && cnt[2].appendChild(this.nodeBot);
}
else {
this.nodeBot = {};
cnt = $.cls('navLinks');
if (cnt[0]) {
cnt = cnt[cnt.length - 1].nextElementSibling;
cnt.parentNode.insertBefore(this.nodeTop, cnt);
}
}
this.pageNumber = null;
this.update(null, null, null, window.bumplimit, window.imagelimit);
if (!window.thread_archived) {
this.updatePageNumber();
this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000);
}
};
ThreadStats.update = function(replies, images, ips, isBumpFull, isImageFull) {
var stats;
if (replies === null) {
replies = $.cls('replyContainer').length;
images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0);
}
stats = [];
if (Main.threadSticky) {
stats.push('Sticky');
}
if (window.thread_archived) {
stats.push('Archived');
}
else if (Main.threadClosed) {
stats.push('Closed');
}
if (isBumpFull) {
stats.push('' + replies + '');
}
else {
stats.push('' + replies + '');
}
if (isImageFull) {
stats.push('' + images + '');
}
else {
stats.push('' + images + '');
}
if (!window.thread_archived) {
if (window.unique_ips) {
stats.push('' + (ips || window.unique_ips) + '');
}
stats.push('' + (this.pageNumber || '?') + '');
}
this.nodeTop.innerHTML = this.nodeBot.innerHTML
= stats.join(' / ');
};
ThreadStats.updatePageNumber = function() {
$.get('//a.4cdn.org/' + Main.board + '/threads.json',
{
onload: ThreadStats.onCatalogLoad,
onerror: ThreadStats.onCatalogError
}
);
};
ThreadStats.onCatalogLoad = function() {
var self, i, j, k, n, page, post, threads, catalog, tid, nodes;
self = ThreadStats;
if (this.status == 200) {
tid = +Main.tid;
catalog = JSON.parse(this.responseText);
for (i = 0; page = catalog[i]; ++i) {
threads = page.threads;
for (j = 0; post = threads[j]; ++j) {
if (post.no == tid) {
nodes = $.cls('ts-page');
for (k = 0; n = nodes[k]; ++k) {
n.textContent = page.page;
}
self.pageNumber = page.page;
return;
}
}
}
clearInterval(self.pageInterval);
}
else {
ThreadStats.onCatalogError();
}
};
ThreadStats.onCatalogError = function() {
console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')');
};
/**
* Filter
*/
var Filter = {};
Filter.init = function() {
this.entities = document.createElement('div');
Filter.load();
};
Filter.onClick = function(e) {
var cmd;
if (cmd = e.target.getAttribute('data-cmd')) {
switch (cmd) {
case 'filters-add':
Filter.add();
break;
case 'filters-save':
Filter.save();
Filter.close();
break;
case 'filters-close':
Filter.close();
break;
case 'filters-palette':
Filter.openPalette(e.target);
break;
case 'filters-palette-close':
Filter.closePalette();
break;
case 'filters-palette-clear':
Filter.clearPalette();
break;
case 'filters-up':
Filter.moveUp(e.target.parentNode.parentNode);
break;
case 'filters-del':
Filter.remove(e.target.parentNode.parentNode);
break;
case 'filters-help-open':
Filter.openHelp();
break;
case 'filters-help-close':
Filter.closeHelp();
break;
}
}
};
Filter.onPaletteClick = function(e) {
var cmd;
if (cmd = e.target.getAttribute('data-cmd')) {
switch (cmd) {
case 'palette-pick':
Filter.pickColor(e.target);
break;
case 'palette-clear':
Filter.pickColor(e.target, true);
break;
case 'palette-close':
Filter.closePalette();
break;
}
}
};
Filter.match = function(post, board) {
var i, com, f, filters, hit;
hit = false;
filters = Filter.activeFilters;
for (i = 0; f = filters[i]; ++i) {
// boards
if (!f.boards[board]) {
continue;
}
// tripcode
if (f.type === 0) {
if (f.pattern === post.trip) {
hit = true;
break;
}
}
// name
else if (f.type === 1) {
if (f.pattern === post.name) {
hit = true;
break;
}
}
// comment
else if (f.type === 2 && post.com) {
if (com === undefined) {
this.entities.innerHTML
= post.com.replace(/ /g, '\n').replace(/[<[^>]+>/g, '');
com = this.entities.textContent;
}
if (f.pattern.test(com)) {
hit = true;
break;
}
}
// user id
else if (f.type === 4) {
if (f.pattern === post.id) {
hit = true;
break;
}
}
// subject
else if (f.type === 5) {
if (f.pattern.test(post.sub)) {
hit = true;
break;
}
}
// filename
else if (f.type === 6) {
if (f.pattern.test(post.filename)) {
hit = true;
break;
}
}
}
return hit;
};
Filter.exec = function(cnt, pi, msg, tid) {
var i, el, trip, name, com, uid, sub, fname, f, filters, hit, currentBoard;
if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) {
return false;
}
currentBoard = Main.board;
filters = Filter.activeFilters;
hit = false;
for (i = 0; f = filters[i]; ++i) {
// boards
if (f.boards && !f.boards[currentBoard]) {
continue;
}
// tripcode
if (f.type === 0) {
if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0])
) && f.pattern == trip.textContent) {
hit = true;
break;
}
}
// name
else if (f.type === 1) {
if ((name || (name = pi.getElementsByClassName('name')[0]))
&& f.pattern == name.textContent) {
hit = true;
break;
}
}
// comment
else if (f.type === 2) {
if (com === undefined) {
this.entities.innerHTML
= msg.innerHTML.replace(/ /g, '\n').replace(/[<[^>]+>/g, '');
com = this.entities.textContent;
}
if (f.pattern.test(com)) {
hit = true;
break;
}
}
// user id
else if (f.type === 4) {
if ((uid ||
((uid = pi.getElementsByClassName('posteruid')[0])
&& (uid = uid.firstElementChild.textContent)
)
) && f.pattern == uid) {
hit = true;
break;
}
}
// subject
else if (!Main.tid && f.type === 5) {
if ((sub ||
((sub = pi.getElementsByClassName('subject')[0])
&& (sub = sub.textContent)
)
) && f.pattern.test(sub)) {
hit = true;
break;
}
}
// filename
else if (f.type === 6) {
if (fname === undefined) {
if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) {
fname = fname.firstElementChild.textContent;
}
else {
fname = '';
}
}
if (f.pattern.test(fname)) {
hit = true;
break;
}
}
}
if (hit) {
if (f.hide) {
if (tid && Config.hideStubs && !$.cls('stickyIcon', cnt)[0]) {
cnt.style.display = cnt.nextElementSibling.style.display = 'none';
}
else {
cnt.className += ' post-hidden';
el = document.createElement('span');
if (!tid) {
el.textContent = '[View]';
el.setAttribute('data-filtered', '1');
el.setAttribute('data-cmd', 'unfilter');
}
else {
el.innerHTML = '[View]';
}
el.className = 'filter-preview';
pi.appendChild(el);
}
return true;
}
else {
cnt.className += ' filter-hl';
cnt.style.boxShadow = '-3px 0 ' + f.color;
Parser.hasHighlightedPosts = true;
}
}
return false;
};
Filter.unfilter = function(t) {
var cnt = t.parentNode.parentNode;
QuotePreview.remove();
$.removeClass(cnt, 'post-hidden');
t.parentNode.removeChild(t);
};
Filter.load = function() {
var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType,
wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard,
tmp, boards, pattern, match;
this.activeFilters = [];
if (!(rawFilters = localStorage.getItem('4chan-filters'))) {
return;
}
rawFilters = JSON.parse(rawFilters);
regexEscape = new RegExp('(\\'
+ ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\')
+ ')', 'g');
regexType = /^\/(.*)\/(i?)$/;
wordSepS = '(?=.*\\b';
wordSepE = '\\b)';
regexWildcard = /\\\*/g;
replaceWildcard = '[^\\s]*';
try {
for (fid = 0; f = rawFilters[fid]; ++fid) {
if (f.active && f.pattern !== '') {
// Boards
if (f.boards) {
tmp = f.boards.split(/[^a-z0-9]+/i);
boards = {};
for (i = 0; j = tmp[i]; ++i) {
boards[j] = true;
}
}
else {
boards = false;
}
rawPattern = f.pattern;
// Name, Tripcode or ID, string comparison
if (!f.type || f.type == 1 || f.type == 4) {
pattern = rawPattern;
}
// /RegExp/
else if (match = rawPattern.match(regexType)) {
pattern = new RegExp(match[1], match[2]);
}
// "Exact match"
else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') {
pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1'));
}
// Full words, AND operator
else {
words = rawPattern.split(' ');
pattern = '';
for (i = 0, j = words.length; i < j; ++i) {
inner = words[i]
.replace(regexEscape, '\\$1')
.replace(regexWildcard, replaceWildcard);
pattern += wordSepS + inner + wordSepE;
}
pattern = new RegExp('^' + pattern, 'im');
}
//console.log('Resulting pattern: ' + pattern);
this.activeFilters.push({
type: f.type,
pattern: pattern,
boards: boards,
color: f.color,
hide: f.hide,
auto: f.auto
});
}
}
}
catch (e) {
alert('There was an error processing one of the filters: '
+ e + ' in: ' + rawPattern);
}
};
Filter.addSelection = function() {
var text, type, node, sel = UA.getSelection(true);
if (Filter.open() === false) {
return;
}
if (typeof sel == 'string') {
text = sel.trim();
}
else {
node = sel.anchorNode.parentNode;
text = sel.toString().trim();
if ($.hasClass(node, 'name')) {
type = 1;
}
else if ($.hasClass(node, 'postertrip')) {
type = 0;
}
else if ($.hasClass(node, 'subject')) {
type = 5;
}
else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) {
type = 4;
}
else if ($.hasClass(node, 'fileText')) {
type = 6;
}
else {
type = 2;
}
}
Filter.add(text, type);
};
Filter.openHelp = function() {
var cnt;
if ($.id('filtersHelp')) {
return;
}
cnt = document.createElement('div');
cnt.id = 'filtersHelp';
cnt.className = 'UIPanel';
cnt.setAttribute('data-cmd', 'filters-help-close');
cnt.innerHTML = '\
Filters & Highlights Help\
\
Tripcode, Name and ID filters:
\
Those use simple string comparison.
\
Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.
\
Example: !Ep8pui8Vw2
\
Comment, Subject and E-mail filters:
\
Matching whole words:
\
feel — will match "feel" but not "feeling". This search is case-insensitive.
\
AND operator:
\
feel girlfriend — will match "feel" AND "girlfriend" in any order.
\
Exact match:
\
"that feel when" — place double quotes around the pattern to search for an exact string
\
Wildcards:
\
feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
\
idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
\
Regular expressions:
\
/feel when no (girl|boy)friend/i
\
/^(?!.*touhou).*$/i — NOT operator.
\
/^>/ — comments starting with a quote.
\
/^$/ — comments with no text.
\
Colors:
\
The color field can accept any valid CSS color:
\
red, #0f0, #00ff00, rgba( 34, 12, 64, 0.3), etc…
\
Boards:
\
A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
\
Auto-watching:
\
Enabling the "Auto" option will automatically add matched threads to the Thread Watcher when it is manually refreshed. This only works when the "Boards" field is not empty, and searches catalog JSON for the selected boards(s).
\
Shortcut:
\
If you have Keyboard shortcuts enabled, pressing F will add the selected text to your filters.
';
if (Main.hasMobileLayout) {
categories = {};
for (cat in SettingsMenu.options) {
mobileOpts = {};
opts = SettingsMenu.options[cat];
for (key in opts) {
if (opts[key][2]) {
mobileOpts[key] = opts[key];
}
}
for (i in mobileOpts) {
categories[cat] = mobileOpts;
break;
}
}
}
else {
categories = SettingsMenu.options;
}
for (cat in categories) {
opts = categories[cat];
html += '
'
+ ''
+ ''
+ cat + '
';
for (key in opts) {
// Mobile layout only?
if (opts[key][4] && !Main.hasMobileLayout) {
continue;
}
html += '