Finish Miscellaneous features (for now), almost finish Monitoring

This commit is contained in:
Zixaphir 2015-01-09 22:21:30 -07:00
parent 108129ca78
commit 3d3fc0a868
14 changed files with 1155 additions and 466 deletions

View File

@ -115,7 +115,7 @@
'use strict';
(function() {
var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation,
var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation,
__slice = [].slice,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__hasProp = {}.hasOwnProperty,
@ -3207,6 +3207,7 @@
this.isPinned = false;
this.isSticky = false;
this.isClosed = false;
this.isArchived = false;
this.postLimit = false;
this.fileLimit = false;
this.ipCount = void 0;
@ -12747,6 +12748,19 @@
Favicon = {
init: function() {
return $.asap((function() {
return d.head && (Favicon.el = $('link[rel="shortcut icon"]', d.head));
}), Favicon.initAsap);
},
initAsap: function() {
var href;
Favicon.el.type = 'image/x-icon';
href = Favicon.el.href;
Favicon.SFW = /ws\.ico$/.test(href);
Favicon["default"] = href;
return Favicon["switch"]();
},
"switch": function() {
var f, funreadDeadY, i, items, t;
items = {
ferongr: ['iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxUlEQVR42q1TOwrCQBB9s0FRtJI0WoqFtSLYegoP4gVSeJsUHsHSI3iFeIqRXXgwrhlXwYHHhLwPTB7B36abBCV+0pA4DUBQUNZYQptGtW3jtoKyxgoe0yrBCoyZfL/5ioQ3URZOXW9I341l3oo+NXEZiW4CEuIzvPECopED4OaZ3RNmeAm4u+a8Jr5f17VyVoL8fr8qcltzwlyyj2iqcgPOQ9ExkHAITgD75bYBe0A5S4H/P9htuWMF3QXoQpwaKeT+lnsC6JE5I6aq6fEAAAAASUVORK5CYII=', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8AcH4AtswA2PJ55fKi6fIA1/FtpPADAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4y2NgoBq4/vE/HJOsBiRQUIfA2AzBqQYqUfn00/9FLz+BaQxDCKqBmX7jExijKEDSDJPHrnnbGQhGV4RmOFwdVkNwhQMheYwQxhaIi7b9Z9A3gWAQm2BUoQOgRhgA8o7j1ozLC4LCyAZcx6kZI5qg4kLKqggDFFWxJySsUQVzlb4pwgAJaTRvokcVNgOqOv8zcHBCsL07DgNg8YsczzA5MxtUL+DMD8g0slxI/H8GQ/P/DJKyeKIRpglXZsIiBwBhP5O+VbI/JgAAAABJRU5ErkJggg==', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8oeQBJ3ABV/wHM/7Lu/+ZU/gAqUP3dAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAx0lEQVQ4y2NgoBYI+cfwH4ZJVgMS0KhEYGyG4FQDkzjzf9P/d/+fgWl0QwiqgSkI/c8IxsgKkDXD5LFq9rwDweiK0A2HqcNqCK5wICSPEcLYAtH+AMN/IXMIBrEJRie6OEgjDAC5x3FqxuUFNiEUA67j1IweTTBxBQ1puAG86jgSEraogskJWSBcwCGF5k30qMJmgMFEhv/MXBAs5oLDAFj8IsczTE7UEeECbhU8+QGZRpaTi2b4L2zF8J9TGk80wjThykzY5AAW/2O1C2mIbgAAAABJRU5ErkJggg=='],
@ -12862,7 +12876,7 @@
ThreadExcerpt = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
if ((g.BOARD.ID !== 'f' && g.BOARD.ID !== 'pol') || g.VIEW !== 'thread' || !Conf['Thread Excerpt'] || Conf['Remove Thread Excerpt']) {
return;
}
return Thread.callbacks.push({
@ -12897,7 +12911,10 @@
});
Header.addShortcut(sc);
} else {
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "<div class=move title='" + title + "'>" + html + "</div>");
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
innerHTML: "<div class=\"move\" title=\"" + E(statsTitle) + "\">" + statsHTML.innerHTML + "</div>"
});
$.addClass(doc, 'float');
$.ready(function() {
return $.add(d.body, sc);
});
@ -12918,7 +12935,10 @@
this.posts.forEach(function(post) {
postCount++;
if (post.file) {
return fileCount++;
fileCount++;
}
if (Conf["Page Count in Stats"]) {
return ThreadStats.lastPost = post.info.date;
}
});
ThreadStats.thread = this;
@ -12977,6 +12997,7 @@
if (!Conf["Page Count in Stats"]) {
return;
}
clearTimeout(ThreadStats.timeout);
if (ThreadStats.thread.isDead) {
ThreadStats.pageCountEl.textContent = 'Dead';
$.addClass(ThreadStats.pageCountEl, 'warning');
@ -13005,6 +13026,7 @@
}
ThreadStats.pageCountEl.textContent = page.page;
(page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND);
return;
}
}
@ -13019,15 +13041,18 @@
}
if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
id: 'updater'
});
$.extend(sc, {
innerHTML: "[<span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>]"
});
$.ready(function() {
return Header.addShortcut(sc);
});
} else {
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "<div class=move><span id=update-status></span><span id=update-timer title='Update now'></span></div>");
$.addClass(doc, 'float');
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
innerHTML: "<div class=\"move\"></div><span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>"
});
$.ready(function() {
$.addClass(doc, 'float');
return $.add(d.body, sc);
@ -13044,24 +13069,21 @@
for (name in _ref) {
conf = _ref[name];
checked = Conf[name] ? 'checked' : '';
el = $.el('label', {
title: "" + conf[1],
innerHTML: "<input name='" + name + "' type=checkbox " + checked + "> " + name
});
el = UI.checkbox(name, " " + name);
input = el.firstElementChild;
$.on(input, 'change', $.cb.checked);
if (input.name === 'Scroll BG') {
$.on(input, 'change', this.cb.scrollBG);
this.cb.scrollBG();
} else if (input.name === 'Auto Update') {
$.on(input, 'change', this.cb.update);
$.on(input, 'change', this.cb.autoUpdate);
}
subEntries.push({
el: el
});
}
this.settings = $.el('span', {
innerHTML: '<a href=javascript:;>Interval</a>'
innerHTML: "<a href=\"javascript:;\">Interval</a>"
});
$.on(this.settings, 'click', this.intervalShortcut);
subEntries.push({
@ -13122,6 +13144,7 @@
ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'],
name: 'Interval'
@ -13134,7 +13157,7 @@
} else {
ThreadUpdater.cb.online();
}
Rice.nodes(ThreadUpdater.dialog);
return Rice.nodes(ThreadUpdater.dialog);
},
/*
@ -13144,14 +13167,18 @@
beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA',
cb: {
online: function() {
if (ThreadUpdater.thread.isDead) {
return;
}
if (ThreadUpdater.online = navigator.onLine) {
ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval();
ThreadUpdater.set('status', null, null);
ThreadUpdater.set('status', '', '');
return;
}
ThreadUpdater.set('timer', null);
return ThreadUpdater.set('status', 'Offline', 'warning');
ThreadUpdater.set('timer', '');
ThreadUpdater.set('status', 'Offline', 'warning');
return clearTimeout(ThreadUpdater.timeoutID);
},
post: function(e) {
if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
@ -13164,14 +13191,14 @@
},
checkpost: function(e) {
if (!ThreadUpdater.checkPostCount) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) {
if (e && e.detail.threadID !== ThreadUpdater.thread.ID) {
return;
}
ThreadUpdater.seconds = 0;
ThreadUpdater.outdateCount = 0;
ThreadUpdater.set('timer', '...');
}
if (!(g.DEAD || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) {
if (!(ThreadUpdater.thread.isDead || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) {
return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND);
}
ThreadUpdater.setInterval();
@ -13195,6 +13222,9 @@
return !d.hidden;
};
},
autoUpdate: function(e) {
return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked);
},
interval: function(e) {
var val;
val = parseInt(this.value, 10);
@ -13211,7 +13241,6 @@
req = ThreadUpdater.req;
switch (req.status) {
case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts);
if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning');
@ -13316,9 +13345,10 @@
var n;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
if (!(n = --ThreadUpdater.seconds)) {
ThreadUpdater.outdateCount++;
return ThreadUpdater.update();
} else if (n <= -60) {
ThreadUpdater.set('status', 'Retrying', null);
ThreadUpdater.set('status', 'Retrying', '');
return ThreadUpdater.update();
} else if (n > 0) {
return ThreadUpdater.set('timer', n);
@ -13393,18 +13423,27 @@
ThreadUpdater.thread.posts.forEach(function(post) {
var ID;
ID = +post.ID;
if (__indexOf.call(index, ID) < 0) {
post.kill();
} else if (post.isDead) {
post.resurrect();
} else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) {
post.kill(true);
if (!(post.info.date > Date.now() - 60 * $.SECOND)) {
if (__indexOf.call(index, ID) < 0) {
post.kill();
} else if (post.isDead) {
post.resurrect();
} else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) {
post.kill(true);
}
}
if (ThreadUpdater.postID && ThreadUpdater.postID === ID) {
return ThreadUpdater.foundPost = true;
}
});
sendEvent = function() {
var ipCountEl;
if ((OP.unique_ips != null) && (ipCountEl = $.id('unique-ips'))) {
ipCountEl.textContent = OP.unique_ips;
ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are');
ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters');
}
ThreadUpdater.postIDs = index;
return $.event('ThreadUpdate', {
404: false,
threadID: ThreadUpdater.thread.fullID,
@ -13417,7 +13456,7 @@
});
};
if (!count) {
ThreadUpdater.set('status', null, null);
ThreadUpdater.set('status', '', '');
ThreadUpdater.outdateCount++;
sendEvent();
return;
@ -13464,13 +13503,17 @@
return;
}
this.db = new DataBoard('watchedThreads', this.refresh, true);
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "<div><span class=\"move\">Thread Watcher <span id=\"watcher-status\"></span></span><a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\uf107</i></a></div><div id=\"watched-threads\"></div>");
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
innerHTML: "<div>\r<span class=\"move\">\rThread Watcher \r<a class=\"refresh fa\" title=\"Check threads\" href=\"javascript:;\">\\uf021</a>\r<span id=\"watcher-status\"></span>\r</span>\r<a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\\uf107</i></a>\r</div>\r<div id=\"watched-threads\"></div>"
});
this.status = $('#watcher-status', this.dialog);
this.list = this.dialog.lastElementChild;
this.refreshButton = $('.refresh', this.dialog);
$.on(d, 'QRPostSuccessful', this.cb.post);
if (g.VIEW === 'thread') {
$.on(d, 'ThreadUpdate', this.cb.threadUpdate);
}
$.on(this.refreshButton, 'click', this.fetchAllStatus);
$.on(d, '4chanXInitFinished', this.ready);
switch (g.VIEW) {
case 'index':
@ -13485,18 +13528,75 @@
ThreadWatcher.fetchAllStatus();
this.db.save();
}
return Thread.callbacks.push({
if (Conf['JSON Navigation'] && Conf['Menu'] && g.BOARD.ID !== 'f') {
Menu.menu.addEntry({
el: $.el('a', {
href: 'javascript:;'
}),
order: 6,
open: function(_arg) {
var thread;
thread = _arg.thread;
if (!(Conf['Index Mode'] === 'catalog' && g.VIEW === 'index')) {
return false;
}
this.el.textContent = ThreadWatcher.isWatched(thread) ? 'Unwatch thread' : 'Watch thread';
if (this.cb) {
$.off(this.el, 'click', this.cb);
}
this.cb = function() {
$.event('CloseMenu');
return ThreadWatcher.toggle(thread);
};
$.on(this.el, 'click', this.cb);
return true;
}
});
}
Post.callbacks.push({
name: 'Thread Watcher',
cb: this.node
});
return CatalogThread.callbacks.push({
name: 'Thread Watcher',
cb: this.catalogNode
});
},
isWatched: function(thread) {
var _ref;
return (_ref = ThreadWatcher.db) != null ? _ref.get({
boardID: thread.board.ID,
threadID: thread.ID
}) : void 0;
},
node: function() {
var toggler;
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.on(toggler, 'click', ThreadWatcher.cb.toggle);
return $.before($('input', this.OP.nodes.post), toggler);
if (this.isReply) {
return;
}
if (this.isClone) {
toggler = $('.watch-thread-link', this.nodes.post);
} else {
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.before($('input', this.nodes.post), toggler);
}
return $.on(toggler, 'click', ThreadWatcher.cb.toggle);
},
catalogNode: function() {
if (ThreadWatcher.isWatched(this.thread)) {
$.addClass(this.nodes.root, 'watched');
}
return $.on(this.nodes.thumb.parentNode, 'click', (function(_this) {
return function(e) {
if (!(e.button === 0 && e.altKey)) {
return;
}
ThreadWatcher.toggle(_this.thread);
return e.preventDefault();
};
})(this));
},
ready: function() {
var el;
@ -13541,12 +13641,6 @@
}
return $.event('CloseMenu');
},
checkThreads: function() {
if ($.hasClass(this, 'disabled')) {
return;
}
return ThreadWatcher.fetchAllStatus();
},
pruneDeads: function() {
var boardID, data, threadID, _i, _len, _ref, _ref1;
if ($.hasClass(this, 'disabled')) {
@ -13568,7 +13662,10 @@
return $.event('CloseMenu');
},
toggle: function() {
return ThreadWatcher.toggle(Get.threadFromNode(this));
ThreadWatcher.toggle(Get.threadFromNode(this));
Index.followedThreadID = thread.ID;
ThreadWatcher.toggle(thread);
return delete Index.followedThreadID;
},
rm: function() {
var boardID, threadID, _ref;
@ -13587,9 +13684,11 @@
}
},
onIndexRefresh: function() {
var boardID, data, threadID, _ref;
var boardID, data, db, threadID, _ref;
db = ThreadWatcher.db;
boardID = g.BOARD.ID;
_ref = ThreadWatcher.db.data.boards[boardID];
db.forceSync();
_ref = db.data.boards[boardID];
for (threadID in _ref) {
data = _ref[threadID];
if (!data.isDead && !(threadID in g.BOARD.threads)) {
@ -13628,10 +13727,12 @@
},
fetchAllStatus: function() {
var thread, threads, _i, _len;
ThreadWatcher.db.forceSync();
ThreadWatcher.unreaddb.forceSync();
QR.db.forceSync();
if (!(threads = ThreadWatcher.getAll()).length) {
return;
}
ThreadWatcher.status.textContent = '...';
for (_i = 0, _len = threads.length; _i < _len; _i++) {
thread = threads[_i];
ThreadWatcher.fetchStatus(thread);
@ -13640,43 +13741,103 @@
fetchStatus: function(_arg) {
var boardID, data, fetchCount, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
if (data.isDead) {
if (data.isDead && !Conf['Show Unread Count']) {
return;
}
fetchCount = ThreadWatcher.fetchCount;
if (fetchCount.fetching === 0) {
ThreadWatcher.status.textContent = '...';
$.addClass(ThreadWatcher.refreshButton, 'fa-spin');
}
fetchCount.fetching++;
return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onloadend: function() {
var status;
var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1;
fetchCount.fetched++;
if (fetchCount.fetched === fetchCount.fetching) {
fetchCount.fetched = 0;
fetchCount.fetching = 0;
status = '';
$.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
} else {
status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
}
ThreadWatcher.status.textContent = status;
if (this.status !== 404) {
return;
}
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
ThreadWatcher.db.set({
if (this.status === 200 && this.response) {
isDead = !!this.response.posts[0].archived;
if (isDead && Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
ThreadWatcher.refresh();
return;
}
lastReadPost = ThreadWatcher.unreaddb.get({
boardID: boardID,
threadID: threadID,
val: data
defaultValue: 0
});
unread = quotingYou = 0;
_ref = this.response.posts;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
postObj = _ref[_i];
if (!(postObj.no > lastReadPost)) {
continue;
}
if ((_ref1 = QR.db) != null ? _ref1.get({
boardID: boardID,
threadID: threadID,
postID: postObj.no
}) : void 0) {
continue;
}
unread++;
if (!(QR.db && postObj.com)) {
continue;
}
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g;
while (match = regexp.exec(postObj.com)) {
if (QR.db.get({
boardID: match[1] || boardID,
threadID: match[2] || threadID,
postID: match[3] || match[2] || threadID
})) {
quotingYou++;
continue;
}
}
}
if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) {
data.isDead = isDead;
data.unread = unread;
data.quotingYou = quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
}
} else if (this.status === 404) {
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
return ThreadWatcher.refresh();
}
return ThreadWatcher.refresh();
}
}, {
type: 'head'
});
},
getAll: function() {
@ -13700,24 +13861,31 @@
return all;
},
makeLine: function(boardID, threadID, data) {
var div, fullID, href, link, x;
var count, div, fullID, link, title, x;
x = $.el('a', {
className: 'fa',
href: 'javascript:;',
textContent: '\uf00d'
});
$.on(x, 'click', ThreadWatcher.cb.rm);
if (data.isDead) {
href = Redirect.to('thread', {
boardID: boardID,
threadID: threadID
});
}
link = $.el('a', {
href: href || ("/" + boardID + "/thread/" + threadID),
href: "/" + boardID + "/thread/" + threadID,
textContent: data.excerpt,
title: data.excerpt
title: data.excerpt,
className: 'watcher-link'
});
if (Conf['Show Unread Count'] && (data.unread != null)) {
count = $.el('span', {
textContent: "(" + data.unread + ")",
className: 'watcher-unread'
});
$.add(link, count);
}
title = $.el('span', {
textContent: data.excerpt,
className: 'watcher-title'
});
$.add(link, title);
div = $.el('div');
fullID = "" + boardID + "." + threadID;
div.dataset.fullID = fullID;
@ -13727,11 +13895,19 @@
if (data.isDead) {
$.addClass(div, 'dead-thread');
}
if (Conf['Show Unread Count']) {
if (data.unread) {
$.addClass(div, 'replies-unread');
}
if (data.quotingYou) {
$.addClass(div, 'replies-quoting-you');
}
}
$.add(div, [x, $.tn(' '), link]);
return div;
},
refresh: function() {
var boardID, data, helper, list, nodes, refresher, thread, threadID, threads, toggler, watched, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3;
var boardID, data, list, nodes, refresher, threadID, _i, _j, _len, _len1, _ref, _ref1, _ref2;
nodes = [];
_ref = ThreadWatcher.getAll();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -13741,24 +13917,76 @@
list = ThreadWatcher.list;
$.rmAll(list);
$.add(list, nodes);
threads = g.BOARD.threads;
_ref2 = threads.keys;
g.threads.forEach(function(thread) {
var helper, post, toggler, _j, _len1, _ref2;
helper = ThreadWatcher.isWatched(thread) ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
if (thread.OP) {
_ref2 = [thread.OP].concat(__slice.call(thread.OP.clones));
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
post = _ref2[_j];
toggler = $('.watch-thread-link', post.nodes.post);
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
}
}
if (thread.catalogView) {
return $[helper[0]](thread.catalogView.nodes.root, 'watched');
}
});
_ref2 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
threadID = _ref2[_j];
thread = threads[threadID];
toggler = $('.watch-thread-link', thread.OP.nodes.post);
watched = ThreadWatcher.db.get({
boardID: thread.board.ID,
refresher = _ref2[_j];
refresher();
}
if (Index.nodes && Conf['Pin Watched Threads']) {
Index.sort();
return Index.buildIndex();
}
},
update: function(boardID, threadID, newData) {
var data, key, line, n, newLine, val, _ref;
if (!(data = (_ref = ThreadWatcher.db) != null ? _ref.get({
boardID: boardID,
threadID: threadID
}) : void 0)) {
return;
}
if (newData.isDead && Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
ThreadWatcher.refresh();
return;
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
refresher = _ref3[_k];
refresher();
n = 0;
for (key in newData) {
val = newData[key];
if (data[key] !== val) {
n++;
}
}
if (!n) {
return;
}
ThreadWatcher.db.forceSync();
if (!(data = ThreadWatcher.db.get({
boardID: boardID,
threadID: threadID
}))) {
return;
}
$.extend(data, newData);
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
if (line = $("#watched-threads > [data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) {
newLine = ThreadWatcher.makeLine(boardID, threadID, data);
return $.replace(line, newLine);
} else {
return ThreadWatcher.refresh();
}
},
toggle: function(thread) {
@ -13795,7 +14023,14 @@
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
ThreadWatcher.refresh();
if (Conf['Show Unread Count']) {
return ThreadWatcher.fetchStatus({
boardID: boardID,
threadID: threadID,
data: data
});
}
},
rm: function(boardID, threadID) {
ThreadWatcher.db["delete"]({
@ -13825,12 +14060,12 @@
if (!Conf['Thread Watcher']) {
return;
}
menu = new UI.Menu();
menu = this.menu = new UI.Menu('thread watcher');
$.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) {
return menu.toggle(e, this, ThreadWatcher);
});
this.addHeaderMenuEntry();
return this.addMenuEntries(menu);
return this.addMenuEntries;
},
addHeaderMenuEntry: function() {
var entryEl;
@ -13855,7 +14090,7 @@
return entryEl.textContent = text;
});
},
addMenuEntries: function(menu) {
addMenuEntries: function() {
var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1;
entries = [];
entries.push({
@ -13869,22 +14104,11 @@
return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
}
});
entries.push({
cb: ThreadWatcher.cb.checkThreads,
entry: {
el: $.el('a', {
textContent: 'Check 404\'d threads'
})
},
refresh: function() {
return ($('div:not(.dead-thread)', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
}
});
entries.push({
cb: ThreadWatcher.cb.pruneDeads,
entry: {
el: $.el('a', {
textContent: 'Prune 404\'d threads'
textContent: 'Prune dead threads'
})
},
refresh: function() {
@ -13916,22 +14140,18 @@
if (refresh) {
this.refreshers.push(refresh.bind(entry));
}
menu.addEntry(entry);
this.menu.addEntry(entry);
}
},
createSubEntry: function(name, desc) {
var entry, input;
entry = {
type: 'thread watcher',
el: $.el('label', {
innerHTML: "<input type=checkbox name='" + name + "'> " + name,
title: desc
})
el: UI.checkbox(name, " " + name)
};
input = entry.el.firstElementChild;
input.checked = Conf[name];
$.on(input, 'change', $.cb.checked);
if (name === 'Current Board') {
if (name === 'Current Board' || name === 'Show Unread Count') {
$.on(input, 'change', ThreadWatcher.refresh);
}
return entry;
@ -15757,10 +15977,14 @@
}
},
getThread: function() {
var threadRoot, _i, _len, _ref;
var thread, threadRoot, _i, _len, _ref;
_ref = $$('.thread');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadRoot = _ref[_i];
thread = Get.threadFromRoot(threadRoot);
if (thread.isHidden && !thread.stub) {
continue;
}
if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) {
return threadRoot;
}
@ -15768,7 +15992,10 @@
return $('.board');
},
scroll: function(delta) {
var axis, next, thread, top;
var axis, extra, next, thread, top, _ref;
if ((_ref = d.activeElement) != null) {
_ref.blur();
}
thread = Nav.getThread();
axis = delta === +1 ? 'following' : 'preceding';
if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) {
@ -15777,48 +16004,64 @@
thread = next;
}
}
return Header.scrollTo(thread);
extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom;
if (extra > 0) {
d.body.style.marginBottom = "" + extra + "px";
}
Header.scrollTo(thread);
if (extra > 0 && !Nav.haveExtra) {
Nav.haveExtra = true;
return $.on(d, 'scroll', Nav.removeExtra);
}
},
removeExtra: function() {
var extra;
extra = doc.clientHeight - d.body.getBoundingClientRect().bottom;
if (extra > 0) {
return d.body.style.marginBottom = "" + extra + "px";
} else {
d.body.style.marginBottom = null;
delete Nav.haveExtra;
return $.off(d, 'scroll', Nav.removeExtra);
}
}
};
RelativeDates = {
INTERVAL: $.MINUTE / 2,
init: function() {
switch (g.VIEW) {
case 'index':
this.flush();
$.on(d, 'visibilitychange', this.flush);
if (!Conf['Relative Post Dates']) {
return;
}
break;
case 'thread':
if (!Conf['Relative Post Dates']) {
return;
}
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
break;
default:
return;
var _ref;
if (((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Relative Post Dates'] && !Conf['Relative Date Title'] || g.VIEW === 'index' && Conf['JSON Navigation'] && g.BOARD.ID !== 'f') {
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
}
if (Conf['Relative Post Dates']) {
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
}
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
},
node: function() {
var dateEl;
dateEl = this.nodes.date;
if (Conf['Relative Date Title']) {
$.on(dateEl, 'mouseover', (function(_this) {
return function() {
return RelativeDates.hover(_this);
};
})(this));
return;
}
if (this.isClone) {
return;
}
dateEl = this.nodes.date;
dateEl.title = dateEl.textContent;
return RelativeDates.update(this);
},
relative: function(diff, now, date) {
var days, months, number, rounded, unit, years;
unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = (months + 12) % 12) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second');
unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = months + 12 * years) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second');
rounded = Math.round(number);
if (rounded !== 1) {
unit += 's';
@ -15841,6 +16084,13 @@
clearTimeout(RelativeDates.timeout);
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);
},
hover: function(post) {
var date, diff, now;
date = post.info.date;
now = new Date();
diff = now - date;
return post.nodes.date.title = RelativeDates.relative(diff, now, date);
},
update: function(data, now) {
var date, diff, isPost, relative, singlePost, _i, _len, _ref;
isPost = data instanceof Post;
@ -15880,44 +16130,45 @@
if (Conf['Reveal Spoilers']) {
$.addClass(doc, 'reveal-spoilers');
}
if (Conf['Remove Spoilers']) {
return $.addClass(doc, 'remove-spoilers');
}
}
};
Report = {
init: function() {
if (!/report/.test(location.search)) {
if (!Conf['Remove Spoilers']) {
return;
}
return $.asap((function() {
return $.id('recaptcha_response_field');
}), Report.ready);
$.addClass(doc, 'remove-spoilers');
Post.callbacks.push({
name: 'Reveal Spoilers',
cb: this.node
});
CatalogThread.callbacks.push({
name: 'Reveal Spoilers',
cb: this.node
});
if (g.VIEW === 'archive') {
return $.ready(function() {
return RemoveSpoilers.unspoiler($.id('arc-list'));
});
}
},
ready: function() {
var field;
field = $.id('recaptcha_response_field');
$.on(field, 'keydown', function(e) {
if (e.keyCode === 8 && !field.value) {
return $.globalEval('Recaptcha.reload("t")');
}
});
return $.on($('form'), 'submit', function(e) {
var response;
e.preventDefault();
response = field.value.trim();
if (!/\s|^\d+$/.test(response)) {
field.value = "" + response + " " + response;
}
return this.submit();
});
node: function(post) {
return RemoveSpoilers.unspoiler(this.nodes.comment);
},
unspoiler: function(el) {
var span, spoiler, spoilers, _i, _len;
spoilers = $$('s', el);
for (_i = 0, _len = spoilers.length; _i < _len; _i++) {
spoiler = spoilers[_i];
span = $.el('span', {
className: 'removed-spoiler'
});
$.replace(spoiler, span);
$.add(span, __slice.call(spoiler.childNodes));
}
}
};
Time = {
init: function() {
if (!Conf['Time Formatting']) {
var _ref;
if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) {
return;
}
return Post.callbacks.push({
@ -15932,7 +16183,7 @@
return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date);
},
format: function(formatString, date) {
return formatString.replace(/%([A-Za-z])/g, function(s, c) {
return formatString.replace(/%(.)/g, function(s, c) {
if (c in Time.formatters) {
return Time.formatters[c].call(date);
} else {
@ -16008,6 +16259,9 @@
},
Y: function() {
return this.getFullYear();
},
'%': function() {
return '%';
}
}
};

View File

@ -88,7 +88,7 @@
'use strict';
(function() {
var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation,
var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation,
__slice = [].slice,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__hasProp = {}.hasOwnProperty,
@ -3233,6 +3233,7 @@
this.isPinned = false;
this.isSticky = false;
this.isClosed = false;
this.isArchived = false;
this.postLimit = false;
this.fileLimit = false;
this.ipCount = void 0;
@ -12769,6 +12770,19 @@
Favicon = {
init: function() {
return $.asap((function() {
return d.head && (Favicon.el = $('link[rel="shortcut icon"]', d.head));
}), Favicon.initAsap);
},
initAsap: function() {
var href;
Favicon.el.type = 'image/x-icon';
href = Favicon.el.href;
Favicon.SFW = /ws\.ico$/.test(href);
Favicon["default"] = href;
return Favicon["switch"]();
},
"switch": function() {
var f, funreadDeadY, i, items, t;
items = {
ferongr: ['iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxUlEQVR42q1TOwrCQBB9s0FRtJI0WoqFtSLYegoP4gVSeJsUHsHSI3iFeIqRXXgwrhlXwYHHhLwPTB7B36abBCV+0pA4DUBQUNZYQptGtW3jtoKyxgoe0yrBCoyZfL/5ioQ3URZOXW9I341l3oo+NXEZiW4CEuIzvPECopED4OaZ3RNmeAm4u+a8Jr5f17VyVoL8fr8qcltzwlyyj2iqcgPOQ9ExkHAITgD75bYBe0A5S4H/P9htuWMF3QXoQpwaKeT+lnsC6JE5I6aq6fEAAAAASUVORK5CYII=', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8AcH4AtswA2PJ55fKi6fIA1/FtpPADAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4y2NgoBq4/vE/HJOsBiRQUIfA2AzBqQYqUfn00/9FLz+BaQxDCKqBmX7jExijKEDSDJPHrnnbGQhGV4RmOFwdVkNwhQMheYwQxhaIi7b9Z9A3gWAQm2BUoQOgRhgA8o7j1ozLC4LCyAZcx6kZI5qg4kLKqggDFFWxJySsUQVzlb4pwgAJaTRvokcVNgOqOv8zcHBCsL07DgNg8YsczzA5MxtUL+DMD8g0slxI/H8GQ/P/DJKyeKIRpglXZsIiBwBhP5O+VbI/JgAAAABJRU5ErkJggg==', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8oeQBJ3ABV/wHM/7Lu/+ZU/gAqUP3dAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAx0lEQVQ4y2NgoBYI+cfwH4ZJVgMS0KhEYGyG4FQDkzjzf9P/d/+fgWl0QwiqgSkI/c8IxsgKkDXD5LFq9rwDweiK0A2HqcNqCK5wICSPEcLYAtH+AMN/IXMIBrEJRie6OEgjDAC5x3FqxuUFNiEUA67j1IweTTBxBQ1puAG86jgSEraogskJWSBcwCGF5k30qMJmgMFEhv/MXBAs5oLDAFj8IsczTE7UEeECbhU8+QGZRpaTi2b4L2zF8J9TGk80wjThykzY5AAW/2O1C2mIbgAAAABJRU5ErkJggg=='],
@ -12884,7 +12898,7 @@
ThreadExcerpt = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
if ((g.BOARD.ID !== 'f' && g.BOARD.ID !== 'pol') || g.VIEW !== 'thread' || !Conf['Thread Excerpt'] || Conf['Remove Thread Excerpt']) {
return;
}
return Thread.callbacks.push({
@ -12919,7 +12933,10 @@
});
Header.addShortcut(sc);
} else {
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "<div class=move title='" + title + "'>" + html + "</div>");
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
innerHTML: "<div class=\"move\" title=\"" + E(statsTitle) + "\">" + statsHTML.innerHTML + "</div>"
});
$.addClass(doc, 'float');
$.ready(function() {
return $.add(d.body, sc);
});
@ -12940,7 +12957,10 @@
this.posts.forEach(function(post) {
postCount++;
if (post.file) {
return fileCount++;
fileCount++;
}
if (Conf["Page Count in Stats"]) {
return ThreadStats.lastPost = post.info.date;
}
});
ThreadStats.thread = this;
@ -12999,6 +13019,7 @@
if (!Conf["Page Count in Stats"]) {
return;
}
clearTimeout(ThreadStats.timeout);
if (ThreadStats.thread.isDead) {
ThreadStats.pageCountEl.textContent = 'Dead';
$.addClass(ThreadStats.pageCountEl, 'warning');
@ -13027,6 +13048,7 @@
}
ThreadStats.pageCountEl.textContent = page.page;
(page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND);
return;
}
}
@ -13041,15 +13063,18 @@
}
if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
id: 'updater'
});
$.extend(sc, {
innerHTML: "[<span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>]"
});
$.ready(function() {
return Header.addShortcut(sc);
});
} else {
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "<div class=move><span id=update-status></span><span id=update-timer title='Update now'></span></div>");
$.addClass(doc, 'float');
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
innerHTML: "<div class=\"move\"></div><span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>"
});
$.ready(function() {
$.addClass(doc, 'float');
return $.add(d.body, sc);
@ -13066,24 +13091,21 @@
for (name in _ref) {
conf = _ref[name];
checked = Conf[name] ? 'checked' : '';
el = $.el('label', {
title: "" + conf[1],
innerHTML: "<input name='" + name + "' type=checkbox " + checked + "> " + name
});
el = UI.checkbox(name, " " + name);
input = el.firstElementChild;
$.on(input, 'change', $.cb.checked);
if (input.name === 'Scroll BG') {
$.on(input, 'change', this.cb.scrollBG);
this.cb.scrollBG();
} else if (input.name === 'Auto Update') {
$.on(input, 'change', this.cb.update);
$.on(input, 'change', this.cb.autoUpdate);
}
subEntries.push({
el: el
});
}
this.settings = $.el('span', {
innerHTML: '<a href=javascript:;>Interval</a>'
innerHTML: "<a href=\"javascript:;\">Interval</a>"
});
$.on(this.settings, 'click', this.intervalShortcut);
subEntries.push({
@ -13144,6 +13166,7 @@
ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'],
name: 'Interval'
@ -13156,7 +13179,7 @@
} else {
ThreadUpdater.cb.online();
}
Rice.nodes(ThreadUpdater.dialog);
return Rice.nodes(ThreadUpdater.dialog);
},
/*
@ -13166,14 +13189,18 @@
beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA',
cb: {
online: function() {
if (ThreadUpdater.thread.isDead) {
return;
}
if (ThreadUpdater.online = navigator.onLine) {
ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval();
ThreadUpdater.set('status', null, null);
ThreadUpdater.set('status', '', '');
return;
}
ThreadUpdater.set('timer', null);
return ThreadUpdater.set('status', 'Offline', 'warning');
ThreadUpdater.set('timer', '');
ThreadUpdater.set('status', 'Offline', 'warning');
return clearTimeout(ThreadUpdater.timeoutID);
},
post: function(e) {
if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
@ -13186,14 +13213,14 @@
},
checkpost: function(e) {
if (!ThreadUpdater.checkPostCount) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) {
if (e && e.detail.threadID !== ThreadUpdater.thread.ID) {
return;
}
ThreadUpdater.seconds = 0;
ThreadUpdater.outdateCount = 0;
ThreadUpdater.set('timer', '...');
}
if (!(g.DEAD || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) {
if (!(ThreadUpdater.thread.isDead || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) {
return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND);
}
ThreadUpdater.setInterval();
@ -13217,6 +13244,9 @@
return !d.hidden;
};
},
autoUpdate: function(e) {
return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked);
},
interval: function(e) {
var val;
val = parseInt(this.value, 10);
@ -13233,7 +13263,6 @@
req = ThreadUpdater.req;
switch (req.status) {
case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts);
if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning');
@ -13338,9 +13367,10 @@
var n;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
if (!(n = --ThreadUpdater.seconds)) {
ThreadUpdater.outdateCount++;
return ThreadUpdater.update();
} else if (n <= -60) {
ThreadUpdater.set('status', 'Retrying', null);
ThreadUpdater.set('status', 'Retrying', '');
return ThreadUpdater.update();
} else if (n > 0) {
return ThreadUpdater.set('timer', n);
@ -13415,18 +13445,27 @@
ThreadUpdater.thread.posts.forEach(function(post) {
var ID;
ID = +post.ID;
if (__indexOf.call(index, ID) < 0) {
post.kill();
} else if (post.isDead) {
post.resurrect();
} else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) {
post.kill(true);
if (!(post.info.date > Date.now() - 60 * $.SECOND)) {
if (__indexOf.call(index, ID) < 0) {
post.kill();
} else if (post.isDead) {
post.resurrect();
} else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) {
post.kill(true);
}
}
if (ThreadUpdater.postID && ThreadUpdater.postID === ID) {
return ThreadUpdater.foundPost = true;
}
});
sendEvent = function() {
var ipCountEl;
if ((OP.unique_ips != null) && (ipCountEl = $.id('unique-ips'))) {
ipCountEl.textContent = OP.unique_ips;
ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are');
ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters');
}
ThreadUpdater.postIDs = index;
return $.event('ThreadUpdate', {
404: false,
threadID: ThreadUpdater.thread.fullID,
@ -13439,7 +13478,7 @@
});
};
if (!count) {
ThreadUpdater.set('status', null, null);
ThreadUpdater.set('status', '', '');
ThreadUpdater.outdateCount++;
sendEvent();
return;
@ -13486,13 +13525,17 @@
return;
}
this.db = new DataBoard('watchedThreads', this.refresh, true);
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "<div><span class=\"move\">Thread Watcher <span id=\"watcher-status\"></span></span><a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\uf107</i></a></div><div id=\"watched-threads\"></div>");
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
innerHTML: "<div>\r<span class=\"move\">\rThread Watcher \r<a class=\"refresh fa\" title=\"Check threads\" href=\"javascript:;\">\\uf021</a>\r<span id=\"watcher-status\"></span>\r</span>\r<a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\\uf107</i></a>\r</div>\r<div id=\"watched-threads\"></div>"
});
this.status = $('#watcher-status', this.dialog);
this.list = this.dialog.lastElementChild;
this.refreshButton = $('.refresh', this.dialog);
$.on(d, 'QRPostSuccessful', this.cb.post);
if (g.VIEW === 'thread') {
$.on(d, 'ThreadUpdate', this.cb.threadUpdate);
}
$.on(this.refreshButton, 'click', this.fetchAllStatus);
$.on(d, '4chanXInitFinished', this.ready);
switch (g.VIEW) {
case 'index':
@ -13507,18 +13550,75 @@
ThreadWatcher.fetchAllStatus();
this.db.save();
}
return Thread.callbacks.push({
if (Conf['JSON Navigation'] && Conf['Menu'] && g.BOARD.ID !== 'f') {
Menu.menu.addEntry({
el: $.el('a', {
href: 'javascript:;'
}),
order: 6,
open: function(_arg) {
var thread;
thread = _arg.thread;
if (!(Conf['Index Mode'] === 'catalog' && g.VIEW === 'index')) {
return false;
}
this.el.textContent = ThreadWatcher.isWatched(thread) ? 'Unwatch thread' : 'Watch thread';
if (this.cb) {
$.off(this.el, 'click', this.cb);
}
this.cb = function() {
$.event('CloseMenu');
return ThreadWatcher.toggle(thread);
};
$.on(this.el, 'click', this.cb);
return true;
}
});
}
Post.callbacks.push({
name: 'Thread Watcher',
cb: this.node
});
return CatalogThread.callbacks.push({
name: 'Thread Watcher',
cb: this.catalogNode
});
},
isWatched: function(thread) {
var _ref;
return (_ref = ThreadWatcher.db) != null ? _ref.get({
boardID: thread.board.ID,
threadID: thread.ID
}) : void 0;
},
node: function() {
var toggler;
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.on(toggler, 'click', ThreadWatcher.cb.toggle);
return $.before($('input', this.OP.nodes.post), toggler);
if (this.isReply) {
return;
}
if (this.isClone) {
toggler = $('.watch-thread-link', this.nodes.post);
} else {
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.before($('input', this.nodes.post), toggler);
}
return $.on(toggler, 'click', ThreadWatcher.cb.toggle);
},
catalogNode: function() {
if (ThreadWatcher.isWatched(this.thread)) {
$.addClass(this.nodes.root, 'watched');
}
return $.on(this.nodes.thumb.parentNode, 'click', (function(_this) {
return function(e) {
if (!(e.button === 0 && e.altKey)) {
return;
}
ThreadWatcher.toggle(_this.thread);
return e.preventDefault();
};
})(this));
},
ready: function() {
var el;
@ -13563,12 +13663,6 @@
}
return $.event('CloseMenu');
},
checkThreads: function() {
if ($.hasClass(this, 'disabled')) {
return;
}
return ThreadWatcher.fetchAllStatus();
},
pruneDeads: function() {
var boardID, data, threadID, _i, _len, _ref, _ref1;
if ($.hasClass(this, 'disabled')) {
@ -13590,7 +13684,10 @@
return $.event('CloseMenu');
},
toggle: function() {
return ThreadWatcher.toggle(Get.threadFromNode(this));
ThreadWatcher.toggle(Get.threadFromNode(this));
Index.followedThreadID = thread.ID;
ThreadWatcher.toggle(thread);
return delete Index.followedThreadID;
},
rm: function() {
var boardID, threadID, _ref;
@ -13609,9 +13706,11 @@
}
},
onIndexRefresh: function() {
var boardID, data, threadID, _ref;
var boardID, data, db, threadID, _ref;
db = ThreadWatcher.db;
boardID = g.BOARD.ID;
_ref = ThreadWatcher.db.data.boards[boardID];
db.forceSync();
_ref = db.data.boards[boardID];
for (threadID in _ref) {
data = _ref[threadID];
if (!data.isDead && !(threadID in g.BOARD.threads)) {
@ -13650,10 +13749,12 @@
},
fetchAllStatus: function() {
var thread, threads, _i, _len;
ThreadWatcher.db.forceSync();
ThreadWatcher.unreaddb.forceSync();
QR.db.forceSync();
if (!(threads = ThreadWatcher.getAll()).length) {
return;
}
ThreadWatcher.status.textContent = '...';
for (_i = 0, _len = threads.length; _i < _len; _i++) {
thread = threads[_i];
ThreadWatcher.fetchStatus(thread);
@ -13662,43 +13763,103 @@
fetchStatus: function(_arg) {
var boardID, data, fetchCount, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
if (data.isDead) {
if (data.isDead && !Conf['Show Unread Count']) {
return;
}
fetchCount = ThreadWatcher.fetchCount;
if (fetchCount.fetching === 0) {
ThreadWatcher.status.textContent = '...';
$.addClass(ThreadWatcher.refreshButton, 'fa-spin');
}
fetchCount.fetching++;
return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onloadend: function() {
var status;
var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1;
fetchCount.fetched++;
if (fetchCount.fetched === fetchCount.fetching) {
fetchCount.fetched = 0;
fetchCount.fetching = 0;
status = '';
$.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
} else {
status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
}
ThreadWatcher.status.textContent = status;
if (this.status !== 404) {
return;
}
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
ThreadWatcher.db.set({
if (this.status === 200 && this.response) {
isDead = !!this.response.posts[0].archived;
if (isDead && Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
ThreadWatcher.refresh();
return;
}
lastReadPost = ThreadWatcher.unreaddb.get({
boardID: boardID,
threadID: threadID,
val: data
defaultValue: 0
});
unread = quotingYou = 0;
_ref = this.response.posts;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
postObj = _ref[_i];
if (!(postObj.no > lastReadPost)) {
continue;
}
if ((_ref1 = QR.db) != null ? _ref1.get({
boardID: boardID,
threadID: threadID,
postID: postObj.no
}) : void 0) {
continue;
}
unread++;
if (!(QR.db && postObj.com)) {
continue;
}
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g;
while (match = regexp.exec(postObj.com)) {
if (QR.db.get({
boardID: match[1] || boardID,
threadID: match[2] || threadID,
postID: match[3] || match[2] || threadID
})) {
quotingYou++;
continue;
}
}
}
if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) {
data.isDead = isDead;
data.unread = unread;
data.quotingYou = quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
}
} else if (this.status === 404) {
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
return ThreadWatcher.refresh();
}
return ThreadWatcher.refresh();
}
}, {
type: 'head'
});
},
getAll: function() {
@ -13722,24 +13883,31 @@
return all;
},
makeLine: function(boardID, threadID, data) {
var div, fullID, href, link, x;
var count, div, fullID, link, title, x;
x = $.el('a', {
className: 'fa',
href: 'javascript:;',
textContent: '\uf00d'
});
$.on(x, 'click', ThreadWatcher.cb.rm);
if (data.isDead) {
href = Redirect.to('thread', {
boardID: boardID,
threadID: threadID
});
}
link = $.el('a', {
href: href || ("/" + boardID + "/thread/" + threadID),
href: "/" + boardID + "/thread/" + threadID,
textContent: data.excerpt,
title: data.excerpt
title: data.excerpt,
className: 'watcher-link'
});
if (Conf['Show Unread Count'] && (data.unread != null)) {
count = $.el('span', {
textContent: "(" + data.unread + ")",
className: 'watcher-unread'
});
$.add(link, count);
}
title = $.el('span', {
textContent: data.excerpt,
className: 'watcher-title'
});
$.add(link, title);
div = $.el('div');
fullID = "" + boardID + "." + threadID;
div.dataset.fullID = fullID;
@ -13749,11 +13917,19 @@
if (data.isDead) {
$.addClass(div, 'dead-thread');
}
if (Conf['Show Unread Count']) {
if (data.unread) {
$.addClass(div, 'replies-unread');
}
if (data.quotingYou) {
$.addClass(div, 'replies-quoting-you');
}
}
$.add(div, [x, $.tn(' '), link]);
return div;
},
refresh: function() {
var boardID, data, helper, list, nodes, refresher, thread, threadID, threads, toggler, watched, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3;
var boardID, data, list, nodes, refresher, threadID, _i, _j, _len, _len1, _ref, _ref1, _ref2;
nodes = [];
_ref = ThreadWatcher.getAll();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -13763,24 +13939,76 @@
list = ThreadWatcher.list;
$.rmAll(list);
$.add(list, nodes);
threads = g.BOARD.threads;
_ref2 = threads.keys;
g.threads.forEach(function(thread) {
var helper, post, toggler, _j, _len1, _ref2;
helper = ThreadWatcher.isWatched(thread) ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
if (thread.OP) {
_ref2 = [thread.OP].concat(__slice.call(thread.OP.clones));
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
post = _ref2[_j];
toggler = $('.watch-thread-link', post.nodes.post);
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
}
}
if (thread.catalogView) {
return $[helper[0]](thread.catalogView.nodes.root, 'watched');
}
});
_ref2 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
threadID = _ref2[_j];
thread = threads[threadID];
toggler = $('.watch-thread-link', thread.OP.nodes.post);
watched = ThreadWatcher.db.get({
boardID: thread.board.ID,
refresher = _ref2[_j];
refresher();
}
if (Index.nodes && Conf['Pin Watched Threads']) {
Index.sort();
return Index.buildIndex();
}
},
update: function(boardID, threadID, newData) {
var data, key, line, n, newLine, val, _ref;
if (!(data = (_ref = ThreadWatcher.db) != null ? _ref.get({
boardID: boardID,
threadID: threadID
}) : void 0)) {
return;
}
if (newData.isDead && Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
ThreadWatcher.refresh();
return;
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
refresher = _ref3[_k];
refresher();
n = 0;
for (key in newData) {
val = newData[key];
if (data[key] !== val) {
n++;
}
}
if (!n) {
return;
}
ThreadWatcher.db.forceSync();
if (!(data = ThreadWatcher.db.get({
boardID: boardID,
threadID: threadID
}))) {
return;
}
$.extend(data, newData);
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
if (line = $("#watched-threads > [data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) {
newLine = ThreadWatcher.makeLine(boardID, threadID, data);
return $.replace(line, newLine);
} else {
return ThreadWatcher.refresh();
}
},
toggle: function(thread) {
@ -13817,7 +14045,14 @@
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
ThreadWatcher.refresh();
if (Conf['Show Unread Count']) {
return ThreadWatcher.fetchStatus({
boardID: boardID,
threadID: threadID,
data: data
});
}
},
rm: function(boardID, threadID) {
ThreadWatcher.db["delete"]({
@ -13847,12 +14082,12 @@
if (!Conf['Thread Watcher']) {
return;
}
menu = new UI.Menu();
menu = this.menu = new UI.Menu('thread watcher');
$.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) {
return menu.toggle(e, this, ThreadWatcher);
});
this.addHeaderMenuEntry();
return this.addMenuEntries(menu);
return this.addMenuEntries;
},
addHeaderMenuEntry: function() {
var entryEl;
@ -13877,7 +14112,7 @@
return entryEl.textContent = text;
});
},
addMenuEntries: function(menu) {
addMenuEntries: function() {
var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1;
entries = [];
entries.push({
@ -13891,22 +14126,11 @@
return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
}
});
entries.push({
cb: ThreadWatcher.cb.checkThreads,
entry: {
el: $.el('a', {
textContent: 'Check 404\'d threads'
})
},
refresh: function() {
return ($('div:not(.dead-thread)', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
}
});
entries.push({
cb: ThreadWatcher.cb.pruneDeads,
entry: {
el: $.el('a', {
textContent: 'Prune 404\'d threads'
textContent: 'Prune dead threads'
})
},
refresh: function() {
@ -13938,22 +14162,18 @@
if (refresh) {
this.refreshers.push(refresh.bind(entry));
}
menu.addEntry(entry);
this.menu.addEntry(entry);
}
},
createSubEntry: function(name, desc) {
var entry, input;
entry = {
type: 'thread watcher',
el: $.el('label', {
innerHTML: "<input type=checkbox name='" + name + "'> " + name,
title: desc
})
el: UI.checkbox(name, " " + name)
};
input = entry.el.firstElementChild;
input.checked = Conf[name];
$.on(input, 'change', $.cb.checked);
if (name === 'Current Board') {
if (name === 'Current Board' || name === 'Show Unread Count') {
$.on(input, 'change', ThreadWatcher.refresh);
}
return entry;
@ -15778,10 +15998,14 @@
}
},
getThread: function() {
var threadRoot, _i, _len, _ref;
var thread, threadRoot, _i, _len, _ref;
_ref = $$('.thread');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadRoot = _ref[_i];
thread = Get.threadFromRoot(threadRoot);
if (thread.isHidden && !thread.stub) {
continue;
}
if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) {
return threadRoot;
}
@ -15789,7 +16013,10 @@
return $('.board');
},
scroll: function(delta) {
var axis, next, thread, top;
var axis, extra, next, thread, top, _ref;
if ((_ref = d.activeElement) != null) {
_ref.blur();
}
thread = Nav.getThread();
axis = delta === +1 ? 'following' : 'preceding';
if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) {
@ -15798,48 +16025,64 @@
thread = next;
}
}
return Header.scrollTo(thread);
extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom;
if (extra > 0) {
d.body.style.marginBottom = "" + extra + "px";
}
Header.scrollTo(thread);
if (extra > 0 && !Nav.haveExtra) {
Nav.haveExtra = true;
return $.on(d, 'scroll', Nav.removeExtra);
}
},
removeExtra: function() {
var extra;
extra = doc.clientHeight - d.body.getBoundingClientRect().bottom;
if (extra > 0) {
return d.body.style.marginBottom = "" + extra + "px";
} else {
d.body.style.marginBottom = null;
delete Nav.haveExtra;
return $.off(d, 'scroll', Nav.removeExtra);
}
}
};
RelativeDates = {
INTERVAL: $.MINUTE / 2,
init: function() {
switch (g.VIEW) {
case 'index':
this.flush();
$.on(d, 'visibilitychange', this.flush);
if (!Conf['Relative Post Dates']) {
return;
}
break;
case 'thread':
if (!Conf['Relative Post Dates']) {
return;
}
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
break;
default:
return;
var _ref;
if (((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Relative Post Dates'] && !Conf['Relative Date Title'] || g.VIEW === 'index' && Conf['JSON Navigation'] && g.BOARD.ID !== 'f') {
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
}
if (Conf['Relative Post Dates']) {
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
}
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
},
node: function() {
var dateEl;
dateEl = this.nodes.date;
if (Conf['Relative Date Title']) {
$.on(dateEl, 'mouseover', (function(_this) {
return function() {
return RelativeDates.hover(_this);
};
})(this));
return;
}
if (this.isClone) {
return;
}
dateEl = this.nodes.date;
dateEl.title = dateEl.textContent;
return RelativeDates.update(this);
},
relative: function(diff, now, date) {
var days, months, number, rounded, unit, years;
unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = (months + 12) % 12) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second');
unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = months + 12 * years) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second');
rounded = Math.round(number);
if (rounded !== 1) {
unit += 's';
@ -15862,6 +16105,13 @@
clearTimeout(RelativeDates.timeout);
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);
},
hover: function(post) {
var date, diff, now;
date = post.info.date;
now = new Date();
diff = now - date;
return post.nodes.date.title = RelativeDates.relative(diff, now, date);
},
update: function(data, now) {
var date, diff, isPost, relative, singlePost, _i, _len, _ref;
isPost = data instanceof Post;
@ -15901,44 +16151,45 @@
if (Conf['Reveal Spoilers']) {
$.addClass(doc, 'reveal-spoilers');
}
if (Conf['Remove Spoilers']) {
return $.addClass(doc, 'remove-spoilers');
}
}
};
Report = {
init: function() {
if (!/report/.test(location.search)) {
if (!Conf['Remove Spoilers']) {
return;
}
return $.asap((function() {
return $.id('recaptcha_response_field');
}), Report.ready);
$.addClass(doc, 'remove-spoilers');
Post.callbacks.push({
name: 'Reveal Spoilers',
cb: this.node
});
CatalogThread.callbacks.push({
name: 'Reveal Spoilers',
cb: this.node
});
if (g.VIEW === 'archive') {
return $.ready(function() {
return RemoveSpoilers.unspoiler($.id('arc-list'));
});
}
},
ready: function() {
var field;
field = $.id('recaptcha_response_field');
$.on(field, 'keydown', function(e) {
if (e.keyCode === 8 && !field.value) {
return $.globalEval('Recaptcha.reload("t")');
}
});
return $.on($('form'), 'submit', function(e) {
var response;
e.preventDefault();
response = field.value.trim();
if (!/\s|^\d+$/.test(response)) {
field.value = "" + response + " " + response;
}
return this.submit();
});
node: function(post) {
return RemoveSpoilers.unspoiler(this.nodes.comment);
},
unspoiler: function(el) {
var span, spoiler, spoilers, _i, _len;
spoilers = $$('s', el);
for (_i = 0, _len = spoilers.length; _i < _len; _i++) {
spoiler = spoilers[_i];
span = $.el('span', {
className: 'removed-spoiler'
});
$.replace(spoiler, span);
$.add(span, __slice.call(spoiler.childNodes));
}
}
};
Time = {
init: function() {
if (!Conf['Time Formatting']) {
var _ref;
if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) {
return;
}
return Post.callbacks.push({
@ -15953,7 +16204,7 @@
return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date);
},
format: function(formatString, date) {
return formatString.replace(/%([A-Za-z])/g, function(s, c) {
return formatString.replace(/%(.)/g, function(s, c) {
if (c in Time.formatters) {
return Time.formatters[c].call(date);
} else {
@ -16029,6 +16280,9 @@
},
Y: function() {
return this.getFullYear();
},
'%': function() {
return '%';
}
}
};

View File

@ -1,5 +1,9 @@
<div>
<span class="move">Thread Watcher <span id="watcher-status"></span></span>
<span class="move">
Thread Watcher
<a class="refresh fa" title="Check threads" href="javascript:;">\uf021</a>
<span id="watcher-status"></span>
</span>
<a class="menu-button" href="javascript:;"><i class="fa">\uf107</i></a>
</div>
<div id="watched-threads"></div>
<div id="watched-threads"></div>

View File

@ -3,16 +3,17 @@ class Thread
toString: -> @ID
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = new SimpleDict
@isDead = false
@isHidden = false
@isOnTop = false
@isPinned = false
@isSticky = false
@isClosed = false
@postLimit = false
@fileLimit = false
@fullID = "#{@board}.#{@ID}"
@posts = new SimpleDict
@isDead = false
@isHidden = false
@isOnTop = false
@isPinned = false
@isSticky = false
@isClosed = false
@isArchived = false
@postLimit = false
@fileLimit = false
@ipCount = undefined
@OP = null

View File

@ -33,11 +33,14 @@ Nav =
getThread: ->
for threadRoot in $$ '.thread'
thread = Get.threadFromRoot threadRoot
continue if thread.isHidden and !thread.stub
if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
return threadRoot
return $ '.board'
scroll: (delta) ->
d.activeElement?.blur()
thread = Nav.getThread()
axis = if delta is +1
'following'
@ -49,4 +52,21 @@ Nav =
# or we're above the first thread and don't want to skip it.
top = Header.getTopOf thread
thread = next if delta is +1 and top < 5 or delta is -1 and top > -5
# Add extra space to the end of the page if necessary so that all threads can be selected by keybinds.
extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom
d.body.style.marginBottom = "#{extra}px" if extra > 0
Header.scrollTo thread
if extra > 0 and !Nav.haveExtra
Nav.haveExtra = true
$.on d, 'scroll', Nav.removeExtra
removeExtra: ->
extra = doc.clientHeight - d.body.getBoundingClientRect().bottom
if extra > 0
d.body.style.marginBottom = "#{extra}px"
else
d.body.style.marginBottom = null
delete Nav.haveExtra
$.off d, 'scroll', Nav.removeExtra

View File

@ -1,28 +1,28 @@
RelativeDates =
INTERVAL: $.MINUTE / 2
init: ->
switch g.VIEW
when 'index'
@flush()
$.on d, 'visibilitychange', @flush
return unless Conf['Relative Post Dates']
when 'thread'
return unless Conf['Relative Post Dates']
@flush()
$.on d, 'visibilitychange ThreadUpdate', @flush
else
return
if (
g.VIEW in ['index', 'thread'] and Conf['Relative Post Dates'] and !Conf['Relative Date Title'] or
g.VIEW is 'index' and Conf['JSON Navigation'] and g.BOARD.ID isnt 'f'
)
@flush()
$.on d, 'visibilitychange ThreadUpdate', @flush
if Conf['Relative Post Dates']
Post.callbacks.push
name: 'Relative Post Dates'
cb: @node
Post.callbacks.push
name: 'Relative Post Dates'
cb: @node
node: ->
dateEl = @nodes.date
if Conf['Relative Date Title']
$.on dateEl, 'mouseover', => RelativeDates.hover @
return
return if @isClone
# Show original absolute time as tooltip so users can still know exact times
# Since "Time Formatting" runs its `node` before us, the title tooltip will
# pick up the user-formatted time instead of 4chan time when enabled.
dateEl = @nodes.date
dateEl.title = dateEl.textContent
RelativeDates.update @
@ -39,7 +39,7 @@ RelativeDates =
else if years is 1 and (months > 0 or months is 0 and days >= 0)
number = years
'year'
else if (months = (months+12)%12 ) > 1
else if (months = months + 12*years) > 1
number = months - (days < 0)
'month'
else if months is 1 and days >= 0
@ -82,6 +82,12 @@ RelativeDates =
clearTimeout RelativeDates.timeout
RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL
hover: (post) ->
date = post.info.date
now = new Date()
diff = now - date
post.nodes.date.title = RelativeDates.relative diff, now, date
# `update()`, when called from `flush()`, updates the elements,
# and re-calls `setOwnTimeout()` to re-add `data` to the stale list later.
update: (data, now) ->

View File

@ -3,5 +3,28 @@ RemoveSpoilers =
if Conf['Reveal Spoilers']
$.addClass doc, 'reveal-spoilers'
if Conf['Remove Spoilers']
$.addClass doc, 'remove-spoilers'
return unless Conf['Remove Spoilers']
$.addClass doc, 'remove-spoilers'
Post.callbacks.push
name: 'Reveal Spoilers'
cb: @node
CatalogThread.callbacks.push
name: 'Reveal Spoilers'
cb: @node
if g.VIEW is 'archive'
$.ready -> RemoveSpoilers.unspoiler $.id 'arc-list'
node: (post) ->
RemoveSpoilers.unspoiler @nodes.comment
unspoiler: (el) ->
spoilers = $$ 's', el
for spoiler in spoilers
span = $.el 'span', className: 'removed-spoiler'
$.replace spoiler, span
$.add span, [spoiler.childNodes...]
return

View File

@ -1,13 +0,0 @@
Report =
init: ->
return unless /report/.test(location.search)
$.asap (-> $.id 'recaptcha_response_field'), Report.ready
ready: ->
field = $.id 'recaptcha_response_field'
$.on field, 'keydown', (e) ->
$.globalEval 'Recaptcha.reload("t")' if e.keyCode is 8 and not field.value
$.on $('form'), 'submit', (e) ->
e.preventDefault()
response = field.value.trim()
field.value = "#{response} #{response}" unless /\s|^\d+$/.test response
@submit()

View File

@ -1,19 +1,21 @@
Time =
init: ->
return if !Conf['Time Formatting']
return unless g.VIEW in ['index', 'thread'] and Conf['Time Formatting']
Post.callbacks.push
name: 'Time Formatting'
cb: @node
node: ->
return if @isClone
@nodes.date.textContent = Time.format Conf['time'], @info.date
format: (formatString, date) ->
formatString.replace /%([A-Za-z])/g, (s, c) ->
formatString.replace /%(.)/g, (s, c) ->
if c of Time.formatters
Time.formatters[c].call(date)
else
s
day: [
'Sunday'
'Monday'
@ -23,6 +25,7 @@ Time =
'Friday'
'Saturday'
]
month: [
'January'
'February'
@ -37,7 +40,9 @@ Time =
'November'
'December'
]
zeroPad: (n) -> if n < 10 then "0#{n}" else n
formatters:
a: -> Time.day[@getDay()][...3]
A: -> Time.day[@getDay()]
@ -56,3 +61,4 @@ Time =
S: -> Time.zeroPad @getSeconds()
y: -> @getFullYear().toString()[2..]
Y: -> @getFullYear()
'%': -> '%'

View File

@ -1,5 +1,15 @@
Favicon =
init: ->
$.asap (-> d.head and Favicon.el = $ 'link[rel="shortcut icon"]', d.head), Favicon.initAsap
initAsap: ->
Favicon.el.type = 'image/x-icon'
{href} = Favicon.el
Favicon.SFW = /ws\.ico$/.test href
Favicon.default = href
Favicon.switch()
switch: ->
items = {
ferongr: [
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'

View File

@ -1,6 +1,6 @@
ThreadExcerpt =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt']
return if (g.BOARD.ID isnt 'f' and g.BOARD.ID isnt 'pol') or g.VIEW isnt 'thread' or !Conf['Thread Excerpt'] or Conf['Remove Thread Excerpt']
Thread.callbacks.push
name: 'Thread Excerpt'

View File

@ -21,16 +21,18 @@ ThreadStats =
id: 'thread-stats'
title: title
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
"<div class=move title='#{title}'>#{html}</div>"
<%= html('<div class="move" title="${statsTitle}">&{statsHTML}</div>') %>
$.addClass doc, 'float'
$.ready ->
$.add d.body, sc
@postCountEl = $ '#post-count', sc
@ipCountEl = $ '#ip-count', sc
@fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc
@postCountEl = $ '#post-count', sc
@ipCountEl = $ '#ip-count', sc
@fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc
Thread.callbacks.push
name: 'Thread Stats'
@ -42,6 +44,7 @@ ThreadStats =
@posts.forEach (post) ->
postCount++
fileCount++ if post.file
ThreadStats.lastPost = post.info.date if Conf["Page Count in Stats"]
ThreadStats.thread = @
ThreadStats.fetchPage()
ThreadStats.update postCount, fileCount, @ipCount
@ -88,6 +91,7 @@ ThreadStats =
fetchPage: ->
return if !Conf["Page Count in Stats"]
clearTimeout ThreadStats.timeout
if ThreadStats.thread.isDead
ThreadStats.pageCountEl.textContent = 'Dead'
$.addClass ThreadStats.pageCountEl, 'warning'
@ -102,4 +106,6 @@ ThreadStats =
for thread in page.threads when thread.no is ThreadStats.thread.ID
ThreadStats.pageCountEl.textContent = page.page
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
# Thread data may be stale (modification date given < time of last post). If so, try again on next thread update.
ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND
return

View File

@ -4,14 +4,13 @@ ThreadUpdater =
if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span',
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0"
id: 'updater'
$.extend sc, <%= html('[<span id="update-status"></span><span id="update-timer" title="Update now"></span>]') %>
$.ready ->
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
"<div class=move><span id=update-status></span><span id=update-timer title='Update now'></span></div>"
$.addClass doc, 'float'
<%= html('<div class="move"></div><span id="update-status"></span><span id="update-timer" title="Update now"></span>') %>
$.ready ->
$.addClass doc, 'float'
$.add d.body, sc
@ -28,20 +27,18 @@ ThreadUpdater =
subEntries = []
for name, conf of Config.updater.checkbox
checked = if Conf[name] then 'checked' else ''
el = $.el 'label',
title: "#{conf[1]}"
innerHTML: "<input name='#{name}' type=checkbox #{checked}> #{name}"
el = UI.checkbox name, " #{name}"
input = el.firstElementChild
$.on input, 'change', $.cb.checked
if input.name is 'Scroll BG'
$.on input, 'change', @cb.scrollBG
@cb.scrollBG()
else if input.name is 'Auto Update'
$.on input, 'change', @cb.update
$.on input, 'change', @cb.autoUpdate
subEntries.push el: el
@settings = $.el 'span',
innerHTML: '<a href=javascript:;>Interval</a>'
<%= html('<a href="javascript:;">Interval</a>') %>
$.on @settings, 'click', @intervalShortcut
@ -92,9 +89,10 @@ ThreadUpdater =
Thread.callbacks.disconnect 'Thread Updater'
node: ->
ThreadUpdater.thread = @
ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +@posts.keys[@posts.keys.length - 1]
ThreadUpdater.thread = @
ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +@posts.keys[@posts.keys.length - 1]
ThreadUpdater.outdateCount = 0
ThreadUpdater.cb.interval.call $.el 'input',
value: Conf['Interval']
@ -110,8 +108,6 @@ ThreadUpdater =
ThreadUpdater.cb.online()
Rice.nodes ThreadUpdater.dialog
return
###
http://freesound.org/people/pierrecartoons1979/sounds/90112/
@ -121,24 +117,26 @@ ThreadUpdater =
cb:
online: ->
return if ThreadUpdater.thread.isDead
if ThreadUpdater.online = navigator.onLine
ThreadUpdater.outdateCount = 0
ThreadUpdater.setInterval()
ThreadUpdater.set 'status', null, null
ThreadUpdater.set 'status', '', ''
return
ThreadUpdater.set 'timer', null
ThreadUpdater.set 'timer', ''
ThreadUpdater.set 'status', 'Offline', 'warning'
clearTimeout ThreadUpdater.timeoutID
post: (e) ->
return unless ThreadUpdater.isUpdating and e.detail.threadID is ThreadUpdater.thread.ID
ThreadUpdater.outdateCount = 0
setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2
checkpost: (e) ->
unless ThreadUpdater.checkPostCount
return unless e.detail.threadID is ThreadUpdater.thread.ID
return if e and e.detail.threadID isnt ThreadUpdater.thread.ID
ThreadUpdater.seconds = 0
ThreadUpdater.outdateCount = 0
ThreadUpdater.set 'timer', '...'
unless g.DEAD or ThreadUpdater.foundPost or ThreadUpdater.checkPostCount >= 5
unless ThreadUpdater.thread.isDead or ThreadUpdater.foundPost or ThreadUpdater.checkPostCount >= 5
return setTimeout ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND
ThreadUpdater.setInterval()
ThreadUpdater.checkPostCount = 0
@ -155,6 +153,8 @@ ThreadUpdater =
-> true
else
-> not d.hidden
autoUpdate: (e) ->
ThreadUpdater.count ThreadUpdater.isUpdating = @checked
interval: (e) ->
val = parseInt @value, 10
if val < 1 then val = 1
@ -164,7 +164,6 @@ ThreadUpdater =
{req} = ThreadUpdater
switch req.status
when 200
g.DEAD = false
ThreadUpdater.parse req.response.posts
if ThreadUpdater.thread.isArchived
ThreadUpdater.set 'status', 'Archived', 'warning'
@ -257,9 +256,10 @@ ThreadUpdater =
timeout: ->
ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
unless n = --ThreadUpdater.seconds
ThreadUpdater.outdateCount++
ThreadUpdater.update()
else if n <= -60
ThreadUpdater.set 'status', 'Retrying', null
ThreadUpdater.set 'status', 'Retrying', ''
ThreadUpdater.update()
else if n > 0
ThreadUpdater.set 'timer', n
@ -307,7 +307,7 @@ ThreadUpdater =
ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
ThreadUpdater.thread.postLimit = !!OP.bumplimit
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
ThreadUpdater.thread.ipCount = OP.unique_ips if OP.unique_ips?
ThreadUpdater.thread.ipCount = OP.unique_ips if OP.unique_ips?
posts = [] # post objects
index = [] # existing posts
@ -331,18 +331,28 @@ ThreadUpdater =
# continue if post.isDead
ID = +post.ID
unless ID in index
post.kill()
else if post.isDead
post.resurrect()
else if post.file and !post.file.isDead and ID not in files
post.kill true
# Assume deleted posts less than 60 seconds old are false positives.
unless post.info.date > Date.now() - 60 * $.SECOND
unless ID in index
post.kill()
else if post.isDead
post.resurrect()
else if post.file and not (post.file.isDead or ID in files)
post.kill true
# Fetching your own posts after posting
if ThreadUpdater.postID and ThreadUpdater.postID is ID
ThreadUpdater.foundPost = true
sendEvent = ->
# Update IP count in original post form.
if OP.unique_ips? and ipCountEl = $.id('unique-ips')
ipCountEl.textContent = OP.unique_ips
ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, if OP.unique_ips is 1 then 'is' else 'are')
ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, if OP.unique_ips is 1 then 'poster' else 'posters')
ThreadUpdater.postIDs = index
$.event 'ThreadUpdate',
404: false
threadID: ThreadUpdater.thread.fullID
@ -352,7 +362,7 @@ ThreadUpdater =
ipCount: OP.unique_ips
unless count
ThreadUpdater.set 'status', null, null
ThreadUpdater.set 'status', '', ''
ThreadUpdater.outdateCount++
sendEvent()
return

View File

@ -3,17 +3,18 @@ ThreadWatcher =
return if !Conf['Thread Watcher']
@db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """<%=
grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim()
%>"""
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %>
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
@refreshButton = $ '.refresh', @dialog
$.on d, 'QRPostSuccessful', @cb.post
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on @refreshButton, 'click', @fetchAllStatus
$.on d, '4chanXInitFinished', @ready
switch g.VIEW
when 'index'
$.on d, 'IndexRefresh', @cb.onIndexRefresh
@ -26,15 +27,50 @@ ThreadWatcher =
ThreadWatcher.fetchAllStatus()
@db.save()
Thread.callbacks.push
if Conf['JSON Navigation'] and Conf['Menu'] and g.BOARD.ID isnt 'f'
Menu.menu.addEntry
el: $.el 'a', href: 'javascript:;'
order: 6
open: ({thread}) ->
return false unless Conf['Index Mode'] is 'catalog' and g.VIEW is 'index'
@el.textContent = if ThreadWatcher.isWatched thread
'Unwatch thread'
else
'Watch thread'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
ThreadWatcher.toggle thread
$.on @el, 'click', @cb
true
Post.callbacks.push
name: 'Thread Watcher'
cb: @node
CatalogThread.callbacks.push
name: 'Thread Watcher'
cb: @catalogNode
isWatched: (thread) ->
ThreadWatcher.db?.get {boardID: thread.board.ID, threadID: thread.ID}
node: ->
toggler = $.el 'img',
className: 'watch-thread-link'
return if @isReply
if @isClone
toggler = $ '.watch-thread-link', @nodes.post
else
toggler = $.el 'img',
className: 'watch-thread-link'
$.before $('input', @nodes.post), toggler
$.on toggler, 'click', ThreadWatcher.cb.toggle
$.before $('input', @OP.nodes.post), toggler
catalogNode: ->
$.addClass @nodes.root, 'watched' if ThreadWatcher.isWatched @thread
$.on @nodes.thumb.parentNode, 'click', (e) =>
return unless e.button is 0 and e.altKey
ThreadWatcher.toggle @thread
e.preventDefault()
ready: ->
$.off d, '4chanXInitFinished', ThreadWatcher.ready
@ -63,9 +99,6 @@ ThreadWatcher =
for a in $$ 'a[title]', ThreadWatcher.list
$.open a.href
$.event 'CloseMenu'
checkThreads: ->
return if $.hasClass @, 'disabled'
ThreadWatcher.fetchAllStatus()
pruneDeads: ->
return if $.hasClass @, 'disabled'
for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead
@ -76,6 +109,9 @@ ThreadWatcher =
$.event 'CloseMenu'
toggle: ->
ThreadWatcher.toggle Get.threadFromNode @
Index.followedThreadID = thread.ID
ThreadWatcher.toggle thread
delete Index.followedThreadID
rm: ->
[boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.rm boardID, +threadID
@ -87,8 +123,10 @@ ThreadWatcher =
else if Conf['Auto Watch Reply']
ThreadWatcher.add g.threads[boardID + '.' + threadID]
onIndexRefresh: ->
{db} = ThreadWatcher
boardID = g.BOARD.ID
for threadID, data of ThreadWatcher.db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
db.forceSync()
for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
if Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
else
@ -98,21 +136,26 @@ ThreadWatcher =
onThreadRefresh: (e) ->
thread = g.threads[e.detail.threadID]
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status.
# Update dead status.
ThreadWatcher.add thread
fetchCount:
fetched: 0
fetching: 0
fetchAllStatus: ->
ThreadWatcher.db.forceSync()
ThreadWatcher.unreaddb.forceSync()
QR.db.forceSync()
return unless (threads = ThreadWatcher.getAll()).length
ThreadWatcher.status.textContent = '...'
for thread in threads
ThreadWatcher.fetchStatus thread
return
fetchStatus: ({boardID, threadID, data}) ->
return if data.isDead
return if data.isDead and !Conf['Show Unread Count']
{fetchCount} = ThreadWatcher
if fetchCount.fetching is 0
ThreadWatcher.status.textContent = '...'
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
fetchCount.fetching++
$.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
onloadend: ->
@ -121,18 +164,56 @@ ThreadWatcher =
fetchCount.fetched = 0
fetchCount.fetching = 0
status = ''
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
else
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
ThreadWatcher.status.textContent = status
return if @status isnt 404
if Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
else
data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
,
type: 'head'
if @status is 200 and @response
isDead = !!@response.posts[0].archived
if isDead and Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
return
lastReadPost = ThreadWatcher.unreaddb.get
boardID: boardID
threadID: threadID
defaultValue: 0
unread = quotingYou = 0
for postObj in @response.posts
continue unless postObj.no > lastReadPost
continue if QR.db?.get {boardID, threadID, postID: postObj.no}
unread++
continue unless QR.db and postObj.com
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g
while match = regexp.exec postObj.com
if QR.db.get {
boardID: match[1] or boardID
threadID: match[2] or threadID
postID: match[3] or match[2] or threadID
}
quotingYou++
continue
if isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou
data.isDead = isDead
data.unread = unread
data.quotingYou = quotingYou
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
else if @status is 404
if Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
else
data.isDead = true
delete data.unread
delete data.quotingYou
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
getAll: ->
all = []
@ -150,18 +231,31 @@ ThreadWatcher =
textContent: '\uf00d'
$.on x, 'click', ThreadWatcher.cb.rm
if data.isDead
href = Redirect.to 'thread', {boardID, threadID}
link = $.el 'a',
href: href or "/#{boardID}/thread/#{threadID}"
href: "/#{boardID}/thread/#{threadID}"
textContent: data.excerpt
title: data.excerpt
className: 'watcher-link'
if Conf['Show Unread Count'] and data.unread?
count = $.el 'span',
textContent: "(#{data.unread})"
className: 'watcher-unread'
$.add link, count
title = $.el 'span',
textContent: data.excerpt
className: 'watcher-title'
$.add link, title
div = $.el 'div'
fullID = "#{boardID}.#{threadID}"
div.dataset.fullID = fullID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
if Conf['Show Unread Count']
$.addClass div, 'replies-unread' if data.unread
$.addClass div, 'replies-quoting-you' if data.quotingYou
$.add div, [x, $.tn(' '), link]
div
refresh: ->
@ -173,18 +267,40 @@ ThreadWatcher =
$.rmAll list
$.add list, nodes
{threads} = g.BOARD
for threadID in threads.keys
thread = threads[threadID]
toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']
$[helper[0]] toggler, 'watched'
toggler.title = "#{helper[1]} Thread"
g.threads.forEach (thread) ->
helper = if ThreadWatcher.isWatched thread then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']
if thread.OP
for post in [thread.OP, thread.OP.clones...]
toggler = $ '.watch-thread-link', post.nodes.post
$[helper[0]] toggler, 'watched'
toggler.title = "#{helper[1]} Thread"
$[helper[0]] thread.catalogView.nodes.root, 'watched' if thread.catalogView
for refresher in ThreadWatcher.menu.refreshers
refresher()
return
if Index.nodes and Conf['Pin Watched Threads']
Index.sort()
Index.buildIndex()
update: (boardID, threadID, newData) ->
return unless data = ThreadWatcher.db?.get {boardID, threadID}
if newData.isDead and Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
return
n = 0
n++ for key, val of newData when data[key] isnt val
return unless n
ThreadWatcher.db.forceSync()
return unless data = ThreadWatcher.db.get {boardID, threadID}
$.extend data, newData
ThreadWatcher.db.set {boardID, threadID, val: data}
if line = $ "#watched-threads > [data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog
newLine = ThreadWatcher.makeLine boardID, threadID, data
$.replace line, newLine
else
ThreadWatcher.refresh()
toggle: (thread) ->
boardID = thread.board.ID
@ -205,6 +321,8 @@ ThreadWatcher =
data.excerpt = Get.threadExcerpt thread
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
if Conf['Show Unread Count']
ThreadWatcher.fetchStatus {boardID, threadID, data}
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
@ -220,11 +338,11 @@ ThreadWatcher =
refreshers: []
init: ->
return if !Conf['Thread Watcher']
menu = new UI.Menu()
menu = @menu = new UI.Menu 'thread watcher'
$.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
menu.toggle e, @, ThreadWatcher
@addHeaderMenuEntry()
@addMenuEntries menu
@addMenuEntries
addHeaderMenuEntry: ->
return if g.VIEW isnt 'thread'
@ -243,7 +361,7 @@ ThreadWatcher =
$.rmClass entryEl, rmClass
entryEl.textContent = text
addMenuEntries: (menu) ->
addMenuEntries: ->
entries = []
# `Open all` entry
@ -254,20 +372,12 @@ ThreadWatcher =
textContent: 'Open all threads'
refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
# `Check 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.checkThreads
entry:
el: $.el 'a',
textContent: 'Check 404\'d threads'
refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Prune 404'd threads` entry
# `Prune dead threads` entry
entries.push
cb: ThreadWatcher.cb.pruneDeads
entry:
el: $.el 'a',
textContent: 'Prune 404\'d threads'
textContent: 'Prune dead threads'
refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Settings` entries:
@ -284,16 +394,14 @@ ThreadWatcher =
entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
$.on entry.el, 'click', cb if cb
@refreshers.push refresh.bind entry if refresh
menu.addEntry entry
@menu.addEntry entry
return
createSubEntry: (name, desc) ->
entry =
type: 'thread watcher'
el: $.el 'label',
innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
el: UI.checkbox name, " #{name}"
input = entry.el.firstElementChild
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
$.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
$.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count']
entry