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 (!(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) {
} 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;
if (this.isReply) {
return;
}
if (this.isClone) {
toggler = $('.watch-thread-link', this.nodes.post);
} else {
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.on(toggler, 'click', ThreadWatcher.cb.toggle);
return $.before($('input', this.OP.nodes.post), toggler);
$.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,26 +13741,85 @@
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) {
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,
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,
@ -13667,6 +13827,8 @@
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
@ -13675,8 +13837,7 @@
}
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,25 +13917,77 @@
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++) {
threadID = _ref2[_j];
thread = threads[threadID];
toggler = $('.watch-thread-link', thread.OP.nodes.post);
watched = ThreadWatcher.db.get({
boardID: thread.board.ID,
threadID: threadID
});
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
post = _ref2[_j];
toggler = $('.watch-thread-link', post.nodes.post);
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
refresher = _ref3[_k];
}
if (thread.catalogView) {
return $[helper[0]](thread.catalogView.nodes.root, 'watched');
}
});
_ref2 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
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
});
ThreadWatcher.refresh();
return;
}
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) {
var boardID, threadID;
@ -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;
}
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);
break;
default:
return;
}
if (Conf['Relative Post Dates']) {
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")');
}
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'
});
return $.on($('form'), 'submit', function(e) {
var response;
e.preventDefault();
response = field.value.trim();
if (!/\s|^\d+$/.test(response)) {
field.value = "" + response + " " + response;
$.replace(spoiler, span);
$.add(span, __slice.call(spoiler.childNodes));
}
return this.submit();
});
}
};
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 (!(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) {
} 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;
if (this.isReply) {
return;
}
if (this.isClone) {
toggler = $('.watch-thread-link', this.nodes.post);
} else {
toggler = $.el('img', {
className: 'watch-thread-link'
});
$.on(toggler, 'click', ThreadWatcher.cb.toggle);
return $.before($('input', this.OP.nodes.post), toggler);
$.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,26 +13763,85 @@
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) {
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,
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,
@ -13689,6 +13849,8 @@
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
@ -13697,8 +13859,7 @@
}
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,25 +13939,77 @@
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++) {
threadID = _ref2[_j];
thread = threads[threadID];
toggler = $('.watch-thread-link', thread.OP.nodes.post);
watched = ThreadWatcher.db.get({
boardID: thread.board.ID,
threadID: threadID
});
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
post = _ref2[_j];
toggler = $('.watch-thread-link', post.nodes.post);
$[helper[0]](toggler, 'watched');
toggler.title = "" + helper[1] + " Thread";
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
refresher = _ref3[_k];
}
if (thread.catalogView) {
return $[helper[0]](thread.catalogView.nodes.root, 'watched');
}
});
_ref2 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
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
});
ThreadWatcher.refresh();
return;
}
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) {
var boardID, threadID;
@ -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;
}
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);
break;
default:
return;
}
if (Conf['Relative Post Dates']) {
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")');
}
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'
});
return $.on($('form'), 'submit', function(e) {
var response;
e.preventDefault();
response = field.value.trim();
if (!/\s|^\d+$/.test(response)) {
field.value = "" + response + " " + response;
$.replace(spoiler, span);
$.add(span, __slice.call(spoiler.childNodes));
}
return this.submit();
});
}
};
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>

View File

@ -11,6 +11,7 @@ class Thread
@isPinned = false
@isSticky = false
@isClosed = false
@isArchived = false
@postLimit = false
@fileLimit = false
@ipCount = undefined

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']
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
else
return
if Conf['Relative Post Dates']
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']
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,9 +21,11 @@ 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
@ -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
@ -95,6 +92,7 @@ ThreadUpdater =
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']
@ -111,8 +109,6 @@ ThreadUpdater =
Rice.nodes ThreadUpdater.dialog
return
###
http://freesound.org/people/pierrecartoons1979/sounds/90112/
cc-by-nc-3.0
@ -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
@ -331,11 +331,13 @@ ThreadUpdater =
# continue if post.isDead
ID = +post.ID
# 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 !post.file.isDead and ID not in files
else if post.file and not (post.file.isDead or ID in files)
post.kill true
# Fetching your own posts after posting
@ -343,6 +345,14 @@ ThreadUpdater =
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: ->
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 @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()
,
type: 'head'
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
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']
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()
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