diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js
index 1f82c94a4..54edd21ea 100644
--- a/builds/appchan-x.user.js
+++ b/builds/appchan-x.user.js
@@ -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;', "
" + html + "
");
+ this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
+ innerHTML: "" + statsHTML.innerHTML + "
"
+ });
+ $.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: "[]\u00A0",
id: 'updater'
});
+ $.extend(sc, {
+ innerHTML: "[]"
+ });
$.ready(function() {
return Header.addShortcut(sc);
});
} else {
- this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "
");
- $.addClass(doc, 'float');
+ this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
+ innerHTML: ""
+ });
$.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: " " + 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: 'Interval'
+ innerHTML: "Interval"
});
$.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;', "Thread Watcher
");
+ this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
+ innerHTML: "\r
\rThread Watcher \r\\uf021\r\r\r\r
\r"
+ });
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 = /]*\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: " " + 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 '%';
}
}
};
diff --git a/builds/crx/script.js b/builds/crx/script.js
index 078c7a668..52d9adbf1 100644
--- a/builds/crx/script.js
+++ b/builds/crx/script.js
@@ -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;', "" + html + "
");
+ this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
+ innerHTML: "" + statsHTML.innerHTML + "
"
+ });
+ $.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: "[]\u00A0",
id: 'updater'
});
+ $.extend(sc, {
+ innerHTML: "[]"
+ });
$.ready(function() {
return Header.addShortcut(sc);
});
} else {
- this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "
");
- $.addClass(doc, 'float');
+ this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
+ innerHTML: ""
+ });
$.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: " " + 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: 'Interval'
+ innerHTML: "Interval"
});
$.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;', "Thread Watcher
");
+ this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
+ innerHTML: "\r
\rThread Watcher \r\\uf021\r\r\r\r
\r"
+ });
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 = /]*\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: " " + 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 '%';
}
}
};
diff --git a/src/General/html/Monitoring/ThreadWatcher.html b/src/General/html/Monitoring/ThreadWatcher.html
index 7773abeb1..32741ab1c 100644
--- a/src/General/html/Monitoring/ThreadWatcher.html
+++ b/src/General/html/Monitoring/ThreadWatcher.html
@@ -1,5 +1,9 @@
-
Thread Watcher
+
+ Thread Watcher
+ \uf021
+
+
-
+
\ No newline at end of file
diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class
index 08adc1f94..2f795b66e 100755
--- a/src/General/lib/thread.class
+++ b/src/General/lib/thread.class
@@ -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
diff --git a/src/Miscellaneous/Nav.coffee b/src/Miscellaneous/Nav.coffee
index b69778689..9b9a0a978 100755
--- a/src/Miscellaneous/Nav.coffee
+++ b/src/Miscellaneous/Nav.coffee
@@ -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
diff --git a/src/Miscellaneous/RelativeDates.coffee b/src/Miscellaneous/RelativeDates.coffee
index 76a16d522..960a9ec75 100755
--- a/src/Miscellaneous/RelativeDates.coffee
+++ b/src/Miscellaneous/RelativeDates.coffee
@@ -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) ->
diff --git a/src/Miscellaneous/RemoveSpoilers.coffee b/src/Miscellaneous/RemoveSpoilers.coffee
index f83f44e94..339fc621e 100755
--- a/src/Miscellaneous/RemoveSpoilers.coffee
+++ b/src/Miscellaneous/RemoveSpoilers.coffee
@@ -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
\ No newline at end of file
diff --git a/src/Miscellaneous/Report.coffee b/src/Miscellaneous/Report.coffee
deleted file mode 100755
index 4bf7020f1..000000000
--- a/src/Miscellaneous/Report.coffee
+++ /dev/null
@@ -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()
diff --git a/src/Miscellaneous/Time.coffee b/src/Miscellaneous/Time.coffee
index 3d2d3f2f6..f31ce0d7a 100755
--- a/src/Miscellaneous/Time.coffee
+++ b/src/Miscellaneous/Time.coffee
@@ -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()
+ '%': -> '%'
diff --git a/src/Monitoring/Favicon.coffee b/src/Monitoring/Favicon.coffee
index ff25eab64..28db26bc1 100755
--- a/src/Monitoring/Favicon.coffee
+++ b/src/Monitoring/Favicon.coffee
@@ -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"}) %>'
diff --git a/src/Monitoring/ThreadExcerpt.coffee b/src/Monitoring/ThreadExcerpt.coffee
index 265ea202c..53bb549eb 100755
--- a/src/Monitoring/ThreadExcerpt.coffee
+++ b/src/Monitoring/ThreadExcerpt.coffee
@@ -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'
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index 0ec6a8c29..e0f48f15d 100755
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -21,16 +21,18 @@ ThreadStats =
id: 'thread-stats'
title: title
Header.addShortcut sc
+
else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
- "#{html}
"
+ <%= html('&{statsHTML}
') %>
+ $.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
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index 5c2924c0a..03911d010 100755
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -4,14 +4,13 @@ ThreadUpdater =
if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span',
- innerHTML: "[]\u00A0"
id: 'updater'
+ $.extend sc, <%= html('[]') %>
$.ready ->
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
- "
"
- $.addClass doc, 'float'
+ <%= html('') %>
$.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: " #{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: 'Interval'
+ <%= html('Interval') %>
$.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
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index b0c534675..da3e10425 100755
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -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+<').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 = /]*\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: " #{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