Finish Miscellaneous features (for now), almost finish Monitoring

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

View File

@ -115,7 +115,7 @@
'use strict'; 'use strict';
(function() { (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, __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; }, __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, __hasProp = {}.hasOwnProperty,
@ -3207,6 +3207,7 @@
this.isPinned = false; this.isPinned = false;
this.isSticky = false; this.isSticky = false;
this.isClosed = false; this.isClosed = false;
this.isArchived = false;
this.postLimit = false; this.postLimit = false;
this.fileLimit = false; this.fileLimit = false;
this.ipCount = void 0; this.ipCount = void 0;
@ -12747,6 +12748,19 @@
Favicon = { Favicon = {
init: function() { 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; var f, funreadDeadY, i, items, t;
items = { 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=='], 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 = { ThreadExcerpt = {
init: function() { 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;
} }
return Thread.callbacks.push({ return Thread.callbacks.push({
@ -12897,7 +12911,10 @@
}); });
Header.addShortcut(sc); Header.addShortcut(sc);
} else { } else {
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "<div class=move title='" + title + "'>" + html + "</div>"); this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
innerHTML: "<div class=\"move\" title=\"" + E(statsTitle) + "\">" + statsHTML.innerHTML + "</div>"
});
$.addClass(doc, 'float');
$.ready(function() { $.ready(function() {
return $.add(d.body, sc); return $.add(d.body, sc);
}); });
@ -12918,7 +12935,10 @@
this.posts.forEach(function(post) { this.posts.forEach(function(post) {
postCount++; postCount++;
if (post.file) { if (post.file) {
return fileCount++; fileCount++;
}
if (Conf["Page Count in Stats"]) {
return ThreadStats.lastPost = post.info.date;
} }
}); });
ThreadStats.thread = this; ThreadStats.thread = this;
@ -12977,6 +12997,7 @@
if (!Conf["Page Count in Stats"]) { if (!Conf["Page Count in Stats"]) {
return; return;
} }
clearTimeout(ThreadStats.timeout);
if (ThreadStats.thread.isDead) { if (ThreadStats.thread.isDead) {
ThreadStats.pageCountEl.textContent = 'Dead'; ThreadStats.pageCountEl.textContent = 'Dead';
$.addClass(ThreadStats.pageCountEl, 'warning'); $.addClass(ThreadStats.pageCountEl, 'warning');
@ -13005,6 +13026,7 @@
} }
ThreadStats.pageCountEl.textContent = page.page; ThreadStats.pageCountEl.textContent = page.page;
(page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning'); (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND);
return; return;
} }
} }
@ -13019,15 +13041,18 @@
} }
if (Conf['Updater and Stats in Header']) { if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', { this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
id: 'updater' id: 'updater'
}); });
$.extend(sc, {
innerHTML: "[<span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>]"
});
$.ready(function() { $.ready(function() {
return Header.addShortcut(sc); return Header.addShortcut(sc);
}); });
} else { } else {
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "<div class=move><span id=update-status></span><span id=update-timer title='Update now'></span></div>"); this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
$.addClass(doc, 'float'); innerHTML: "<div class=\"move\"></div><span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>"
});
$.ready(function() { $.ready(function() {
$.addClass(doc, 'float'); $.addClass(doc, 'float');
return $.add(d.body, sc); return $.add(d.body, sc);
@ -13044,24 +13069,21 @@
for (name in _ref) { for (name in _ref) {
conf = _ref[name]; conf = _ref[name];
checked = Conf[name] ? 'checked' : ''; checked = Conf[name] ? 'checked' : '';
el = $.el('label', { el = UI.checkbox(name, " " + name);
title: "" + conf[1],
innerHTML: "<input name='" + name + "' type=checkbox " + checked + "> " + name
});
input = el.firstElementChild; input = el.firstElementChild;
$.on(input, 'change', $.cb.checked); $.on(input, 'change', $.cb.checked);
if (input.name === 'Scroll BG') { if (input.name === 'Scroll BG') {
$.on(input, 'change', this.cb.scrollBG); $.on(input, 'change', this.cb.scrollBG);
this.cb.scrollBG(); this.cb.scrollBG();
} else if (input.name === 'Auto Update') { } else if (input.name === 'Auto Update') {
$.on(input, 'change', this.cb.update); $.on(input, 'change', this.cb.autoUpdate);
} }
subEntries.push({ subEntries.push({
el: el el: el
}); });
} }
this.settings = $.el('span', { this.settings = $.el('span', {
innerHTML: '<a href=javascript:;>Interval</a>' innerHTML: "<a href=\"javascript:;\">Interval</a>"
}); });
$.on(this.settings, 'click', this.intervalShortcut); $.on(this.settings, 'click', this.intervalShortcut);
subEntries.push({ subEntries.push({
@ -13122,6 +13144,7 @@
ThreadUpdater.thread = this; ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1]; ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', { ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'], value: Conf['Interval'],
name: 'Interval' name: 'Interval'
@ -13134,7 +13157,7 @@
} else { } else {
ThreadUpdater.cb.online(); 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', 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: { cb: {
online: function() { online: function() {
if (ThreadUpdater.thread.isDead) {
return;
}
if (ThreadUpdater.online = navigator.onLine) { if (ThreadUpdater.online = navigator.onLine) {
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', '', '');
return; return;
} }
ThreadUpdater.set('timer', null); ThreadUpdater.set('timer', '');
return ThreadUpdater.set('status', 'Offline', 'warning'); ThreadUpdater.set('status', 'Offline', 'warning');
return clearTimeout(ThreadUpdater.timeoutID);
}, },
post: function(e) { post: function(e) {
if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
@ -13164,14 +13191,14 @@
}, },
checkpost: function(e) { checkpost: function(e) {
if (!ThreadUpdater.checkPostCount) { if (!ThreadUpdater.checkPostCount) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) { if (e && e.detail.threadID !== ThreadUpdater.thread.ID) {
return; return;
} }
ThreadUpdater.seconds = 0; ThreadUpdater.seconds = 0;
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.set('timer', '...'); 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); return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND);
} }
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
@ -13195,6 +13222,9 @@
return !d.hidden; return !d.hidden;
}; };
}, },
autoUpdate: function(e) {
return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked);
},
interval: function(e) { interval: function(e) {
var val; var val;
val = parseInt(this.value, 10); val = parseInt(this.value, 10);
@ -13211,7 +13241,6 @@
req = ThreadUpdater.req; req = ThreadUpdater.req;
switch (req.status) { switch (req.status) {
case 200: case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts); ThreadUpdater.parse(req.response.posts);
if (ThreadUpdater.thread.isArchived) { if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning'); ThreadUpdater.set('status', 'Archived', 'warning');
@ -13316,9 +13345,10 @@
var n; var n;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
if (!(n = --ThreadUpdater.seconds)) { if (!(n = --ThreadUpdater.seconds)) {
ThreadUpdater.outdateCount++;
return ThreadUpdater.update(); return ThreadUpdater.update();
} else if (n <= -60) { } else if (n <= -60) {
ThreadUpdater.set('status', 'Retrying', null); ThreadUpdater.set('status', 'Retrying', '');
return ThreadUpdater.update(); return ThreadUpdater.update();
} else if (n > 0) { } else if (n > 0) {
return ThreadUpdater.set('timer', n); return ThreadUpdater.set('timer', n);
@ -13393,18 +13423,27 @@
ThreadUpdater.thread.posts.forEach(function(post) { ThreadUpdater.thread.posts.forEach(function(post) {
var ID; var ID;
ID = +post.ID; ID = +post.ID;
if (__indexOf.call(index, ID) < 0) { if (!(post.info.date > Date.now() - 60 * $.SECOND)) {
post.kill(); if (__indexOf.call(index, ID) < 0) {
} else if (post.isDead) { post.kill();
post.resurrect(); } else if (post.isDead) {
} else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) { post.resurrect();
post.kill(true); } else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) {
post.kill(true);
}
} }
if (ThreadUpdater.postID && ThreadUpdater.postID === ID) { if (ThreadUpdater.postID && ThreadUpdater.postID === ID) {
return ThreadUpdater.foundPost = true; return ThreadUpdater.foundPost = true;
} }
}); });
sendEvent = function() { 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', { return $.event('ThreadUpdate', {
404: false, 404: false,
threadID: ThreadUpdater.thread.fullID, threadID: ThreadUpdater.thread.fullID,
@ -13417,7 +13456,7 @@
}); });
}; };
if (!count) { if (!count) {
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', '', '');
ThreadUpdater.outdateCount++; ThreadUpdater.outdateCount++;
sendEvent(); sendEvent();
return; return;
@ -13464,13 +13503,17 @@
return; return;
} }
this.db = new DataBoard('watchedThreads', this.refresh, true); this.db = new DataBoard('watchedThreads', this.refresh, true);
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "<div><span class=\"move\">Thread Watcher <span id=\"watcher-status\"></span></span><a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\uf107</i></a></div><div id=\"watched-threads\"></div>"); this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
innerHTML: "<div>\r<span class=\"move\">\rThread Watcher \r<a class=\"refresh fa\" title=\"Check threads\" href=\"javascript:;\">\\uf021</a>\r<span id=\"watcher-status\"></span>\r</span>\r<a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\\uf107</i></a>\r</div>\r<div id=\"watched-threads\"></div>"
});
this.status = $('#watcher-status', this.dialog); this.status = $('#watcher-status', this.dialog);
this.list = this.dialog.lastElementChild; this.list = this.dialog.lastElementChild;
this.refreshButton = $('.refresh', this.dialog);
$.on(d, 'QRPostSuccessful', this.cb.post); $.on(d, 'QRPostSuccessful', this.cb.post);
if (g.VIEW === 'thread') { if (g.VIEW === 'thread') {
$.on(d, 'ThreadUpdate', this.cb.threadUpdate); $.on(d, 'ThreadUpdate', this.cb.threadUpdate);
} }
$.on(this.refreshButton, 'click', this.fetchAllStatus);
$.on(d, '4chanXInitFinished', this.ready); $.on(d, '4chanXInitFinished', this.ready);
switch (g.VIEW) { switch (g.VIEW) {
case 'index': case 'index':
@ -13485,18 +13528,75 @@
ThreadWatcher.fetchAllStatus(); ThreadWatcher.fetchAllStatus();
this.db.save(); 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', name: 'Thread Watcher',
cb: this.node 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() { node: function() {
var toggler; var toggler;
toggler = $.el('img', { if (this.isReply) {
className: 'watch-thread-link' return;
}); }
$.on(toggler, 'click', ThreadWatcher.cb.toggle); if (this.isClone) {
return $.before($('input', this.OP.nodes.post), toggler); 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() { ready: function() {
var el; var el;
@ -13541,12 +13641,6 @@
} }
return $.event('CloseMenu'); return $.event('CloseMenu');
}, },
checkThreads: function() {
if ($.hasClass(this, 'disabled')) {
return;
}
return ThreadWatcher.fetchAllStatus();
},
pruneDeads: function() { pruneDeads: function() {
var boardID, data, threadID, _i, _len, _ref, _ref1; var boardID, data, threadID, _i, _len, _ref, _ref1;
if ($.hasClass(this, 'disabled')) { if ($.hasClass(this, 'disabled')) {
@ -13568,7 +13662,10 @@
return $.event('CloseMenu'); return $.event('CloseMenu');
}, },
toggle: function() { 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() { rm: function() {
var boardID, threadID, _ref; var boardID, threadID, _ref;
@ -13587,9 +13684,11 @@
} }
}, },
onIndexRefresh: function() { onIndexRefresh: function() {
var boardID, data, threadID, _ref; var boardID, data, db, threadID, _ref;
db = ThreadWatcher.db;
boardID = g.BOARD.ID; boardID = g.BOARD.ID;
_ref = ThreadWatcher.db.data.boards[boardID]; db.forceSync();
_ref = db.data.boards[boardID];
for (threadID in _ref) { for (threadID in _ref) {
data = _ref[threadID]; data = _ref[threadID];
if (!data.isDead && !(threadID in g.BOARD.threads)) { if (!data.isDead && !(threadID in g.BOARD.threads)) {
@ -13628,10 +13727,12 @@
}, },
fetchAllStatus: function() { fetchAllStatus: function() {
var thread, threads, _i, _len; var thread, threads, _i, _len;
ThreadWatcher.db.forceSync();
ThreadWatcher.unreaddb.forceSync();
QR.db.forceSync();
if (!(threads = ThreadWatcher.getAll()).length) { if (!(threads = ThreadWatcher.getAll()).length) {
return; return;
} }
ThreadWatcher.status.textContent = '...';
for (_i = 0, _len = threads.length; _i < _len; _i++) { for (_i = 0, _len = threads.length; _i < _len; _i++) {
thread = threads[_i]; thread = threads[_i];
ThreadWatcher.fetchStatus(thread); ThreadWatcher.fetchStatus(thread);
@ -13640,43 +13741,103 @@
fetchStatus: function(_arg) { fetchStatus: function(_arg) {
var boardID, data, fetchCount, threadID; var boardID, data, fetchCount, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data; boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
if (data.isDead) { if (data.isDead && !Conf['Show Unread Count']) {
return; return;
} }
fetchCount = ThreadWatcher.fetchCount; fetchCount = ThreadWatcher.fetchCount;
if (fetchCount.fetching === 0) {
ThreadWatcher.status.textContent = '...';
$.addClass(ThreadWatcher.refreshButton, 'fa-spin');
}
fetchCount.fetching++; fetchCount.fetching++;
return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onloadend: function() { onloadend: function() {
var status; var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1;
fetchCount.fetched++; fetchCount.fetched++;
if (fetchCount.fetched === fetchCount.fetching) { if (fetchCount.fetched === fetchCount.fetching) {
fetchCount.fetched = 0; fetchCount.fetched = 0;
fetchCount.fetching = 0; fetchCount.fetching = 0;
status = ''; status = '';
$.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
} else { } else {
status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%"; status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
} }
ThreadWatcher.status.textContent = status; ThreadWatcher.status.textContent = status;
if (this.status !== 404) { if (this.status === 200 && this.response) {
return; isDead = !!this.response.posts[0].archived;
} if (isDead && Conf['Auto Prune']) {
if (Conf['Auto Prune']) { ThreadWatcher.db["delete"]({
ThreadWatcher.db["delete"]({ boardID: boardID,
boardID: boardID, threadID: threadID
threadID: threadID });
}); ThreadWatcher.refresh();
} else { return;
data.isDead = true; }
ThreadWatcher.db.set({ lastReadPost = ThreadWatcher.unreaddb.get({
boardID: boardID, boardID: boardID,
threadID: threadID, threadID: threadID,
val: data defaultValue: 0
}); });
unread = quotingYou = 0;
_ref = this.response.posts;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
postObj = _ref[_i];
if (!(postObj.no > lastReadPost)) {
continue;
}
if ((_ref1 = QR.db) != null ? _ref1.get({
boardID: boardID,
threadID: threadID,
postID: postObj.no
}) : void 0) {
continue;
}
unread++;
if (!(QR.db && postObj.com)) {
continue;
}
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g;
while (match = regexp.exec(postObj.com)) {
if (QR.db.get({
boardID: match[1] || boardID,
threadID: match[2] || threadID,
postID: match[3] || match[2] || threadID
})) {
quotingYou++;
continue;
}
}
}
if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) {
data.isDead = isDead;
data.unread = unread;
data.quotingYou = quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
}
} else if (this.status === 404) {
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
return ThreadWatcher.refresh();
} }
return ThreadWatcher.refresh();
} }
}, {
type: 'head'
}); });
}, },
getAll: function() { getAll: function() {
@ -13700,24 +13861,31 @@
return all; return all;
}, },
makeLine: function(boardID, threadID, data) { makeLine: function(boardID, threadID, data) {
var div, fullID, href, link, x; var count, div, fullID, link, title, x;
x = $.el('a', { x = $.el('a', {
className: 'fa', className: 'fa',
href: 'javascript:;', href: 'javascript:;',
textContent: '\uf00d' textContent: '\uf00d'
}); });
$.on(x, 'click', ThreadWatcher.cb.rm); $.on(x, 'click', ThreadWatcher.cb.rm);
if (data.isDead) {
href = Redirect.to('thread', {
boardID: boardID,
threadID: threadID
});
}
link = $.el('a', { link = $.el('a', {
href: href || ("/" + boardID + "/thread/" + threadID), href: "/" + boardID + "/thread/" + threadID,
textContent: data.excerpt, 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'); div = $.el('div');
fullID = "" + boardID + "." + threadID; fullID = "" + boardID + "." + threadID;
div.dataset.fullID = fullID; div.dataset.fullID = fullID;
@ -13727,11 +13895,19 @@
if (data.isDead) { if (data.isDead) {
$.addClass(div, 'dead-thread'); $.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]); $.add(div, [x, $.tn(' '), link]);
return div; return div;
}, },
refresh: function() { 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 = []; nodes = [];
_ref = ThreadWatcher.getAll(); _ref = ThreadWatcher.getAll();
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -13741,24 +13917,76 @@
list = ThreadWatcher.list; list = ThreadWatcher.list;
$.rmAll(list); $.rmAll(list);
$.add(list, nodes); $.add(list, nodes);
threads = g.BOARD.threads; g.threads.forEach(function(thread) {
_ref2 = threads.keys; 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++) { for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
threadID = _ref2[_j]; refresher = _ref2[_j];
thread = threads[threadID]; refresher();
toggler = $('.watch-thread-link', thread.OP.nodes.post); }
watched = ThreadWatcher.db.get({ if (Index.nodes && Conf['Pin Watched Threads']) {
boardID: thread.board.ID, 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 threadID: threadID
}); });
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; ThreadWatcher.refresh();
$[helper[0]](toggler, 'watched'); return;
toggler.title = "" + helper[1] + " Thread";
} }
_ref3 = ThreadWatcher.menu.refreshers; n = 0;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) { for (key in newData) {
refresher = _ref3[_k]; val = newData[key];
refresher(); 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) { toggle: function(thread) {
@ -13795,7 +14023,14 @@
threadID: threadID, threadID: threadID,
val: data 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) { rm: function(boardID, threadID) {
ThreadWatcher.db["delete"]({ ThreadWatcher.db["delete"]({
@ -13825,12 +14060,12 @@
if (!Conf['Thread Watcher']) { if (!Conf['Thread Watcher']) {
return; return;
} }
menu = new UI.Menu(); menu = this.menu = new UI.Menu('thread watcher');
$.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) { $.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) {
return menu.toggle(e, this, ThreadWatcher); return menu.toggle(e, this, ThreadWatcher);
}); });
this.addHeaderMenuEntry(); this.addHeaderMenuEntry();
return this.addMenuEntries(menu); return this.addMenuEntries;
}, },
addHeaderMenuEntry: function() { addHeaderMenuEntry: function() {
var entryEl; var entryEl;
@ -13855,7 +14090,7 @@
return entryEl.textContent = text; return entryEl.textContent = text;
}); });
}, },
addMenuEntries: function(menu) { addMenuEntries: function() {
var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1; var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1;
entries = []; entries = [];
entries.push({ entries.push({
@ -13869,22 +14104,11 @@
return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled'); 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({ entries.push({
cb: ThreadWatcher.cb.pruneDeads, cb: ThreadWatcher.cb.pruneDeads,
entry: { entry: {
el: $.el('a', { el: $.el('a', {
textContent: 'Prune 404\'d threads' textContent: 'Prune dead threads'
}) })
}, },
refresh: function() { refresh: function() {
@ -13916,22 +14140,18 @@
if (refresh) { if (refresh) {
this.refreshers.push(refresh.bind(entry)); this.refreshers.push(refresh.bind(entry));
} }
menu.addEntry(entry); this.menu.addEntry(entry);
} }
}, },
createSubEntry: function(name, desc) { createSubEntry: function(name, desc) {
var entry, input; var entry, input;
entry = { entry = {
type: 'thread watcher', type: 'thread watcher',
el: $.el('label', { el: UI.checkbox(name, " " + name)
innerHTML: "<input type=checkbox name='" + name + "'> " + name,
title: desc
})
}; };
input = entry.el.firstElementChild; input = entry.el.firstElementChild;
input.checked = Conf[name];
$.on(input, 'change', $.cb.checked); $.on(input, 'change', $.cb.checked);
if (name === 'Current Board') { if (name === 'Current Board' || name === 'Show Unread Count') {
$.on(input, 'change', ThreadWatcher.refresh); $.on(input, 'change', ThreadWatcher.refresh);
} }
return entry; return entry;
@ -15757,10 +15977,14 @@
} }
}, },
getThread: function() { getThread: function() {
var threadRoot, _i, _len, _ref; var thread, threadRoot, _i, _len, _ref;
_ref = $$('.thread'); _ref = $$('.thread');
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadRoot = _ref[_i]; threadRoot = _ref[_i];
thread = Get.threadFromRoot(threadRoot);
if (thread.isHidden && !thread.stub) {
continue;
}
if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) {
return threadRoot; return threadRoot;
} }
@ -15768,7 +15992,10 @@
return $('.board'); return $('.board');
}, },
scroll: function(delta) { 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(); thread = Nav.getThread();
axis = delta === +1 ? 'following' : 'preceding'; axis = delta === +1 ? 'following' : 'preceding';
if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) { if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) {
@ -15777,48 +16004,64 @@
thread = next; 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 = { RelativeDates = {
INTERVAL: $.MINUTE / 2, INTERVAL: $.MINUTE / 2,
init: function() { init: function() {
switch (g.VIEW) { var _ref;
case 'index': 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(); this.flush();
$.on(d, 'visibilitychange', this.flush); $.on(d, 'visibilitychange ThreadUpdate', this.flush);
if (!Conf['Relative Post Dates']) { }
return; if (Conf['Relative Post Dates']) {
} return Post.callbacks.push({
break; name: 'Relative Post Dates',
case 'thread': cb: this.node
if (!Conf['Relative Post Dates']) { });
return;
}
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
break;
default:
return;
} }
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
}, },
node: function() { node: function() {
var dateEl; 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) { if (this.isClone) {
return; return;
} }
dateEl = this.nodes.date;
dateEl.title = dateEl.textContent; dateEl.title = dateEl.textContent;
return RelativeDates.update(this); return RelativeDates.update(this);
}, },
relative: function(diff, now, date) { relative: function(diff, now, date) {
var days, months, number, rounded, unit, years; 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); rounded = Math.round(number);
if (rounded !== 1) { if (rounded !== 1) {
unit += 's'; unit += 's';
@ -15841,6 +16084,13 @@
clearTimeout(RelativeDates.timeout); clearTimeout(RelativeDates.timeout);
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); 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) { update: function(data, now) {
var date, diff, isPost, relative, singlePost, _i, _len, _ref; var date, diff, isPost, relative, singlePost, _i, _len, _ref;
isPost = data instanceof Post; isPost = data instanceof Post;
@ -15880,44 +16130,45 @@
if (Conf['Reveal Spoilers']) { if (Conf['Reveal Spoilers']) {
$.addClass(doc, 'reveal-spoilers'); $.addClass(doc, 'reveal-spoilers');
} }
if (Conf['Remove Spoilers']) { if (!Conf['Remove Spoilers']) {
return $.addClass(doc, 'remove-spoilers');
}
}
};
Report = {
init: function() {
if (!/report/.test(location.search)) {
return; return;
} }
return $.asap((function() { $.addClass(doc, 'remove-spoilers');
return $.id('recaptcha_response_field'); Post.callbacks.push({
}), Report.ready); 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() { node: function(post) {
var field; return RemoveSpoilers.unspoiler(this.nodes.comment);
field = $.id('recaptcha_response_field'); },
$.on(field, 'keydown', function(e) { unspoiler: function(el) {
if (e.keyCode === 8 && !field.value) { var span, spoiler, spoilers, _i, _len;
return $.globalEval('Recaptcha.reload("t")'); spoilers = $$('s', el);
} for (_i = 0, _len = spoilers.length; _i < _len; _i++) {
}); spoiler = spoilers[_i];
return $.on($('form'), 'submit', function(e) { span = $.el('span', {
var response; className: 'removed-spoiler'
e.preventDefault(); });
response = field.value.trim(); $.replace(spoiler, span);
if (!/\s|^\d+$/.test(response)) { $.add(span, __slice.call(spoiler.childNodes));
field.value = "" + response + " " + response; }
}
return this.submit();
});
} }
}; };
Time = { Time = {
init: function() { init: function() {
if (!Conf['Time Formatting']) { var _ref;
if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) {
return; return;
} }
return Post.callbacks.push({ return Post.callbacks.push({
@ -15932,7 +16183,7 @@
return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date); return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date);
}, },
format: function(formatString, 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) { if (c in Time.formatters) {
return Time.formatters[c].call(date); return Time.formatters[c].call(date);
} else { } else {
@ -16008,6 +16259,9 @@
}, },
Y: function() { Y: function() {
return this.getFullYear(); return this.getFullYear();
},
'%': function() {
return '%';
} }
} }
}; };

View File

@ -88,7 +88,7 @@
'use strict'; 'use strict';
(function() { (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, __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; }, __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, __hasProp = {}.hasOwnProperty,
@ -3233,6 +3233,7 @@
this.isPinned = false; this.isPinned = false;
this.isSticky = false; this.isSticky = false;
this.isClosed = false; this.isClosed = false;
this.isArchived = false;
this.postLimit = false; this.postLimit = false;
this.fileLimit = false; this.fileLimit = false;
this.ipCount = void 0; this.ipCount = void 0;
@ -12769,6 +12770,19 @@
Favicon = { Favicon = {
init: function() { 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; var f, funreadDeadY, i, items, t;
items = { 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=='], 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 = { ThreadExcerpt = {
init: function() { 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;
} }
return Thread.callbacks.push({ return Thread.callbacks.push({
@ -12919,7 +12933,10 @@
}); });
Header.addShortcut(sc); Header.addShortcut(sc);
} else { } else {
this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "<div class=move title='" + title + "'>" + html + "</div>"); this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', {
innerHTML: "<div class=\"move\" title=\"" + E(statsTitle) + "\">" + statsHTML.innerHTML + "</div>"
});
$.addClass(doc, 'float');
$.ready(function() { $.ready(function() {
return $.add(d.body, sc); return $.add(d.body, sc);
}); });
@ -12940,7 +12957,10 @@
this.posts.forEach(function(post) { this.posts.forEach(function(post) {
postCount++; postCount++;
if (post.file) { if (post.file) {
return fileCount++; fileCount++;
}
if (Conf["Page Count in Stats"]) {
return ThreadStats.lastPost = post.info.date;
} }
}); });
ThreadStats.thread = this; ThreadStats.thread = this;
@ -12999,6 +13019,7 @@
if (!Conf["Page Count in Stats"]) { if (!Conf["Page Count in Stats"]) {
return; return;
} }
clearTimeout(ThreadStats.timeout);
if (ThreadStats.thread.isDead) { if (ThreadStats.thread.isDead) {
ThreadStats.pageCountEl.textContent = 'Dead'; ThreadStats.pageCountEl.textContent = 'Dead';
$.addClass(ThreadStats.pageCountEl, 'warning'); $.addClass(ThreadStats.pageCountEl, 'warning');
@ -13027,6 +13048,7 @@
} }
ThreadStats.pageCountEl.textContent = page.page; ThreadStats.pageCountEl.textContent = page.page;
(page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning'); (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND);
return; return;
} }
} }
@ -13041,15 +13063,18 @@
} }
if (Conf['Updater and Stats in Header']) { if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', { this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
id: 'updater' id: 'updater'
}); });
$.extend(sc, {
innerHTML: "[<span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>]"
});
$.ready(function() { $.ready(function() {
return Header.addShortcut(sc); return Header.addShortcut(sc);
}); });
} else { } else {
this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "<div class=move><span id=update-status></span><span id=update-timer title='Update now'></span></div>"); this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', {
$.addClass(doc, 'float'); innerHTML: "<div class=\"move\"></div><span id=\"update-status\"></span><span id=\"update-timer\" title=\"Update now\"></span>"
});
$.ready(function() { $.ready(function() {
$.addClass(doc, 'float'); $.addClass(doc, 'float');
return $.add(d.body, sc); return $.add(d.body, sc);
@ -13066,24 +13091,21 @@
for (name in _ref) { for (name in _ref) {
conf = _ref[name]; conf = _ref[name];
checked = Conf[name] ? 'checked' : ''; checked = Conf[name] ? 'checked' : '';
el = $.el('label', { el = UI.checkbox(name, " " + name);
title: "" + conf[1],
innerHTML: "<input name='" + name + "' type=checkbox " + checked + "> " + name
});
input = el.firstElementChild; input = el.firstElementChild;
$.on(input, 'change', $.cb.checked); $.on(input, 'change', $.cb.checked);
if (input.name === 'Scroll BG') { if (input.name === 'Scroll BG') {
$.on(input, 'change', this.cb.scrollBG); $.on(input, 'change', this.cb.scrollBG);
this.cb.scrollBG(); this.cb.scrollBG();
} else if (input.name === 'Auto Update') { } else if (input.name === 'Auto Update') {
$.on(input, 'change', this.cb.update); $.on(input, 'change', this.cb.autoUpdate);
} }
subEntries.push({ subEntries.push({
el: el el: el
}); });
} }
this.settings = $.el('span', { this.settings = $.el('span', {
innerHTML: '<a href=javascript:;>Interval</a>' innerHTML: "<a href=\"javascript:;\">Interval</a>"
}); });
$.on(this.settings, 'click', this.intervalShortcut); $.on(this.settings, 'click', this.intervalShortcut);
subEntries.push({ subEntries.push({
@ -13144,6 +13166,7 @@
ThreadUpdater.thread = this; ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1]; ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', { ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'], value: Conf['Interval'],
name: 'Interval' name: 'Interval'
@ -13156,7 +13179,7 @@
} else { } else {
ThreadUpdater.cb.online(); 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', 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: { cb: {
online: function() { online: function() {
if (ThreadUpdater.thread.isDead) {
return;
}
if (ThreadUpdater.online = navigator.onLine) { if (ThreadUpdater.online = navigator.onLine) {
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', '', '');
return; return;
} }
ThreadUpdater.set('timer', null); ThreadUpdater.set('timer', '');
return ThreadUpdater.set('status', 'Offline', 'warning'); ThreadUpdater.set('status', 'Offline', 'warning');
return clearTimeout(ThreadUpdater.timeoutID);
}, },
post: function(e) { post: function(e) {
if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
@ -13186,14 +13213,14 @@
}, },
checkpost: function(e) { checkpost: function(e) {
if (!ThreadUpdater.checkPostCount) { if (!ThreadUpdater.checkPostCount) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) { if (e && e.detail.threadID !== ThreadUpdater.thread.ID) {
return; return;
} }
ThreadUpdater.seconds = 0; ThreadUpdater.seconds = 0;
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.set('timer', '...'); 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); return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND);
} }
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
@ -13217,6 +13244,9 @@
return !d.hidden; return !d.hidden;
}; };
}, },
autoUpdate: function(e) {
return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked);
},
interval: function(e) { interval: function(e) {
var val; var val;
val = parseInt(this.value, 10); val = parseInt(this.value, 10);
@ -13233,7 +13263,6 @@
req = ThreadUpdater.req; req = ThreadUpdater.req;
switch (req.status) { switch (req.status) {
case 200: case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts); ThreadUpdater.parse(req.response.posts);
if (ThreadUpdater.thread.isArchived) { if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning'); ThreadUpdater.set('status', 'Archived', 'warning');
@ -13338,9 +13367,10 @@
var n; var n;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
if (!(n = --ThreadUpdater.seconds)) { if (!(n = --ThreadUpdater.seconds)) {
ThreadUpdater.outdateCount++;
return ThreadUpdater.update(); return ThreadUpdater.update();
} else if (n <= -60) { } else if (n <= -60) {
ThreadUpdater.set('status', 'Retrying', null); ThreadUpdater.set('status', 'Retrying', '');
return ThreadUpdater.update(); return ThreadUpdater.update();
} else if (n > 0) { } else if (n > 0) {
return ThreadUpdater.set('timer', n); return ThreadUpdater.set('timer', n);
@ -13415,18 +13445,27 @@
ThreadUpdater.thread.posts.forEach(function(post) { ThreadUpdater.thread.posts.forEach(function(post) {
var ID; var ID;
ID = +post.ID; ID = +post.ID;
if (__indexOf.call(index, ID) < 0) { if (!(post.info.date > Date.now() - 60 * $.SECOND)) {
post.kill(); if (__indexOf.call(index, ID) < 0) {
} else if (post.isDead) { post.kill();
post.resurrect(); } else if (post.isDead) {
} else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) { post.resurrect();
post.kill(true); } else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) {
post.kill(true);
}
} }
if (ThreadUpdater.postID && ThreadUpdater.postID === ID) { if (ThreadUpdater.postID && ThreadUpdater.postID === ID) {
return ThreadUpdater.foundPost = true; return ThreadUpdater.foundPost = true;
} }
}); });
sendEvent = function() { 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', { return $.event('ThreadUpdate', {
404: false, 404: false,
threadID: ThreadUpdater.thread.fullID, threadID: ThreadUpdater.thread.fullID,
@ -13439,7 +13478,7 @@
}); });
}; };
if (!count) { if (!count) {
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', '', '');
ThreadUpdater.outdateCount++; ThreadUpdater.outdateCount++;
sendEvent(); sendEvent();
return; return;
@ -13486,13 +13525,17 @@
return; return;
} }
this.db = new DataBoard('watchedThreads', this.refresh, true); this.db = new DataBoard('watchedThreads', this.refresh, true);
this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "<div><span class=\"move\">Thread Watcher <span id=\"watcher-status\"></span></span><a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\uf107</i></a></div><div id=\"watched-threads\"></div>"); this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', {
innerHTML: "<div>\r<span class=\"move\">\rThread Watcher \r<a class=\"refresh fa\" title=\"Check threads\" href=\"javascript:;\">\\uf021</a>\r<span id=\"watcher-status\"></span>\r</span>\r<a class=\"menu-button\" href=\"javascript:;\"><i class=\"fa\">\\uf107</i></a>\r</div>\r<div id=\"watched-threads\"></div>"
});
this.status = $('#watcher-status', this.dialog); this.status = $('#watcher-status', this.dialog);
this.list = this.dialog.lastElementChild; this.list = this.dialog.lastElementChild;
this.refreshButton = $('.refresh', this.dialog);
$.on(d, 'QRPostSuccessful', this.cb.post); $.on(d, 'QRPostSuccessful', this.cb.post);
if (g.VIEW === 'thread') { if (g.VIEW === 'thread') {
$.on(d, 'ThreadUpdate', this.cb.threadUpdate); $.on(d, 'ThreadUpdate', this.cb.threadUpdate);
} }
$.on(this.refreshButton, 'click', this.fetchAllStatus);
$.on(d, '4chanXInitFinished', this.ready); $.on(d, '4chanXInitFinished', this.ready);
switch (g.VIEW) { switch (g.VIEW) {
case 'index': case 'index':
@ -13507,18 +13550,75 @@
ThreadWatcher.fetchAllStatus(); ThreadWatcher.fetchAllStatus();
this.db.save(); 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', name: 'Thread Watcher',
cb: this.node 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() { node: function() {
var toggler; var toggler;
toggler = $.el('img', { if (this.isReply) {
className: 'watch-thread-link' return;
}); }
$.on(toggler, 'click', ThreadWatcher.cb.toggle); if (this.isClone) {
return $.before($('input', this.OP.nodes.post), toggler); 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() { ready: function() {
var el; var el;
@ -13563,12 +13663,6 @@
} }
return $.event('CloseMenu'); return $.event('CloseMenu');
}, },
checkThreads: function() {
if ($.hasClass(this, 'disabled')) {
return;
}
return ThreadWatcher.fetchAllStatus();
},
pruneDeads: function() { pruneDeads: function() {
var boardID, data, threadID, _i, _len, _ref, _ref1; var boardID, data, threadID, _i, _len, _ref, _ref1;
if ($.hasClass(this, 'disabled')) { if ($.hasClass(this, 'disabled')) {
@ -13590,7 +13684,10 @@
return $.event('CloseMenu'); return $.event('CloseMenu');
}, },
toggle: function() { 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() { rm: function() {
var boardID, threadID, _ref; var boardID, threadID, _ref;
@ -13609,9 +13706,11 @@
} }
}, },
onIndexRefresh: function() { onIndexRefresh: function() {
var boardID, data, threadID, _ref; var boardID, data, db, threadID, _ref;
db = ThreadWatcher.db;
boardID = g.BOARD.ID; boardID = g.BOARD.ID;
_ref = ThreadWatcher.db.data.boards[boardID]; db.forceSync();
_ref = db.data.boards[boardID];
for (threadID in _ref) { for (threadID in _ref) {
data = _ref[threadID]; data = _ref[threadID];
if (!data.isDead && !(threadID in g.BOARD.threads)) { if (!data.isDead && !(threadID in g.BOARD.threads)) {
@ -13650,10 +13749,12 @@
}, },
fetchAllStatus: function() { fetchAllStatus: function() {
var thread, threads, _i, _len; var thread, threads, _i, _len;
ThreadWatcher.db.forceSync();
ThreadWatcher.unreaddb.forceSync();
QR.db.forceSync();
if (!(threads = ThreadWatcher.getAll()).length) { if (!(threads = ThreadWatcher.getAll()).length) {
return; return;
} }
ThreadWatcher.status.textContent = '...';
for (_i = 0, _len = threads.length; _i < _len; _i++) { for (_i = 0, _len = threads.length; _i < _len; _i++) {
thread = threads[_i]; thread = threads[_i];
ThreadWatcher.fetchStatus(thread); ThreadWatcher.fetchStatus(thread);
@ -13662,43 +13763,103 @@
fetchStatus: function(_arg) { fetchStatus: function(_arg) {
var boardID, data, fetchCount, threadID; var boardID, data, fetchCount, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data; boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
if (data.isDead) { if (data.isDead && !Conf['Show Unread Count']) {
return; return;
} }
fetchCount = ThreadWatcher.fetchCount; fetchCount = ThreadWatcher.fetchCount;
if (fetchCount.fetching === 0) {
ThreadWatcher.status.textContent = '...';
$.addClass(ThreadWatcher.refreshButton, 'fa-spin');
}
fetchCount.fetching++; fetchCount.fetching++;
return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onloadend: function() { onloadend: function() {
var status; var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1;
fetchCount.fetched++; fetchCount.fetched++;
if (fetchCount.fetched === fetchCount.fetching) { if (fetchCount.fetched === fetchCount.fetching) {
fetchCount.fetched = 0; fetchCount.fetched = 0;
fetchCount.fetching = 0; fetchCount.fetching = 0;
status = ''; status = '';
$.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
} else { } else {
status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%"; status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
} }
ThreadWatcher.status.textContent = status; ThreadWatcher.status.textContent = status;
if (this.status !== 404) { if (this.status === 200 && this.response) {
return; isDead = !!this.response.posts[0].archived;
} if (isDead && Conf['Auto Prune']) {
if (Conf['Auto Prune']) { ThreadWatcher.db["delete"]({
ThreadWatcher.db["delete"]({ boardID: boardID,
boardID: boardID, threadID: threadID
threadID: threadID });
}); ThreadWatcher.refresh();
} else { return;
data.isDead = true; }
ThreadWatcher.db.set({ lastReadPost = ThreadWatcher.unreaddb.get({
boardID: boardID, boardID: boardID,
threadID: threadID, threadID: threadID,
val: data defaultValue: 0
}); });
unread = quotingYou = 0;
_ref = this.response.posts;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
postObj = _ref[_i];
if (!(postObj.no > lastReadPost)) {
continue;
}
if ((_ref1 = QR.db) != null ? _ref1.get({
boardID: boardID,
threadID: threadID,
postID: postObj.no
}) : void 0) {
continue;
}
unread++;
if (!(QR.db && postObj.com)) {
continue;
}
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g;
while (match = regexp.exec(postObj.com)) {
if (QR.db.get({
boardID: match[1] || boardID,
threadID: match[2] || threadID,
postID: match[3] || match[2] || threadID
})) {
quotingYou++;
continue;
}
}
}
if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) {
data.isDead = isDead;
data.unread = unread;
data.quotingYou = quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
return ThreadWatcher.refresh();
}
} else if (this.status === 404) {
if (Conf['Auto Prune']) {
ThreadWatcher.db["delete"]({
boardID: boardID,
threadID: threadID
});
} else {
data.isDead = true;
delete data.unread;
delete data.quotingYou;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
return ThreadWatcher.refresh();
} }
return ThreadWatcher.refresh();
} }
}, {
type: 'head'
}); });
}, },
getAll: function() { getAll: function() {
@ -13722,24 +13883,31 @@
return all; return all;
}, },
makeLine: function(boardID, threadID, data) { makeLine: function(boardID, threadID, data) {
var div, fullID, href, link, x; var count, div, fullID, link, title, x;
x = $.el('a', { x = $.el('a', {
className: 'fa', className: 'fa',
href: 'javascript:;', href: 'javascript:;',
textContent: '\uf00d' textContent: '\uf00d'
}); });
$.on(x, 'click', ThreadWatcher.cb.rm); $.on(x, 'click', ThreadWatcher.cb.rm);
if (data.isDead) {
href = Redirect.to('thread', {
boardID: boardID,
threadID: threadID
});
}
link = $.el('a', { link = $.el('a', {
href: href || ("/" + boardID + "/thread/" + threadID), href: "/" + boardID + "/thread/" + threadID,
textContent: data.excerpt, 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'); div = $.el('div');
fullID = "" + boardID + "." + threadID; fullID = "" + boardID + "." + threadID;
div.dataset.fullID = fullID; div.dataset.fullID = fullID;
@ -13749,11 +13917,19 @@
if (data.isDead) { if (data.isDead) {
$.addClass(div, 'dead-thread'); $.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]); $.add(div, [x, $.tn(' '), link]);
return div; return div;
}, },
refresh: function() { 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 = []; nodes = [];
_ref = ThreadWatcher.getAll(); _ref = ThreadWatcher.getAll();
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -13763,24 +13939,76 @@
list = ThreadWatcher.list; list = ThreadWatcher.list;
$.rmAll(list); $.rmAll(list);
$.add(list, nodes); $.add(list, nodes);
threads = g.BOARD.threads; g.threads.forEach(function(thread) {
_ref2 = threads.keys; 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++) { for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
threadID = _ref2[_j]; refresher = _ref2[_j];
thread = threads[threadID]; refresher();
toggler = $('.watch-thread-link', thread.OP.nodes.post); }
watched = ThreadWatcher.db.get({ if (Index.nodes && Conf['Pin Watched Threads']) {
boardID: thread.board.ID, 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 threadID: threadID
}); });
helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; ThreadWatcher.refresh();
$[helper[0]](toggler, 'watched'); return;
toggler.title = "" + helper[1] + " Thread";
} }
_ref3 = ThreadWatcher.menu.refreshers; n = 0;
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) { for (key in newData) {
refresher = _ref3[_k]; val = newData[key];
refresher(); 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) { toggle: function(thread) {
@ -13817,7 +14045,14 @@
threadID: threadID, threadID: threadID,
val: data 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) { rm: function(boardID, threadID) {
ThreadWatcher.db["delete"]({ ThreadWatcher.db["delete"]({
@ -13847,12 +14082,12 @@
if (!Conf['Thread Watcher']) { if (!Conf['Thread Watcher']) {
return; return;
} }
menu = new UI.Menu(); menu = this.menu = new UI.Menu('thread watcher');
$.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) { $.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) {
return menu.toggle(e, this, ThreadWatcher); return menu.toggle(e, this, ThreadWatcher);
}); });
this.addHeaderMenuEntry(); this.addHeaderMenuEntry();
return this.addMenuEntries(menu); return this.addMenuEntries;
}, },
addHeaderMenuEntry: function() { addHeaderMenuEntry: function() {
var entryEl; var entryEl;
@ -13877,7 +14112,7 @@
return entryEl.textContent = text; return entryEl.textContent = text;
}); });
}, },
addMenuEntries: function(menu) { addMenuEntries: function() {
var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1; var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1;
entries = []; entries = [];
entries.push({ entries.push({
@ -13891,22 +14126,11 @@
return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled'); 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({ entries.push({
cb: ThreadWatcher.cb.pruneDeads, cb: ThreadWatcher.cb.pruneDeads,
entry: { entry: {
el: $.el('a', { el: $.el('a', {
textContent: 'Prune 404\'d threads' textContent: 'Prune dead threads'
}) })
}, },
refresh: function() { refresh: function() {
@ -13938,22 +14162,18 @@
if (refresh) { if (refresh) {
this.refreshers.push(refresh.bind(entry)); this.refreshers.push(refresh.bind(entry));
} }
menu.addEntry(entry); this.menu.addEntry(entry);
} }
}, },
createSubEntry: function(name, desc) { createSubEntry: function(name, desc) {
var entry, input; var entry, input;
entry = { entry = {
type: 'thread watcher', type: 'thread watcher',
el: $.el('label', { el: UI.checkbox(name, " " + name)
innerHTML: "<input type=checkbox name='" + name + "'> " + name,
title: desc
})
}; };
input = entry.el.firstElementChild; input = entry.el.firstElementChild;
input.checked = Conf[name];
$.on(input, 'change', $.cb.checked); $.on(input, 'change', $.cb.checked);
if (name === 'Current Board') { if (name === 'Current Board' || name === 'Show Unread Count') {
$.on(input, 'change', ThreadWatcher.refresh); $.on(input, 'change', ThreadWatcher.refresh);
} }
return entry; return entry;
@ -15778,10 +15998,14 @@
} }
}, },
getThread: function() { getThread: function() {
var threadRoot, _i, _len, _ref; var thread, threadRoot, _i, _len, _ref;
_ref = $$('.thread'); _ref = $$('.thread');
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadRoot = _ref[_i]; threadRoot = _ref[_i];
thread = Get.threadFromRoot(threadRoot);
if (thread.isHidden && !thread.stub) {
continue;
}
if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) {
return threadRoot; return threadRoot;
} }
@ -15789,7 +16013,10 @@
return $('.board'); return $('.board');
}, },
scroll: function(delta) { 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(); thread = Nav.getThread();
axis = delta === +1 ? 'following' : 'preceding'; axis = delta === +1 ? 'following' : 'preceding';
if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) { if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) {
@ -15798,48 +16025,64 @@
thread = next; 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 = { RelativeDates = {
INTERVAL: $.MINUTE / 2, INTERVAL: $.MINUTE / 2,
init: function() { init: function() {
switch (g.VIEW) { var _ref;
case 'index': 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(); this.flush();
$.on(d, 'visibilitychange', this.flush); $.on(d, 'visibilitychange ThreadUpdate', this.flush);
if (!Conf['Relative Post Dates']) { }
return; if (Conf['Relative Post Dates']) {
} return Post.callbacks.push({
break; name: 'Relative Post Dates',
case 'thread': cb: this.node
if (!Conf['Relative Post Dates']) { });
return;
}
this.flush();
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
break;
default:
return;
} }
return Post.callbacks.push({
name: 'Relative Post Dates',
cb: this.node
});
}, },
node: function() { node: function() {
var dateEl; 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) { if (this.isClone) {
return; return;
} }
dateEl = this.nodes.date;
dateEl.title = dateEl.textContent; dateEl.title = dateEl.textContent;
return RelativeDates.update(this); return RelativeDates.update(this);
}, },
relative: function(diff, now, date) { relative: function(diff, now, date) {
var days, months, number, rounded, unit, years; 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); rounded = Math.round(number);
if (rounded !== 1) { if (rounded !== 1) {
unit += 's'; unit += 's';
@ -15862,6 +16105,13 @@
clearTimeout(RelativeDates.timeout); clearTimeout(RelativeDates.timeout);
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); 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) { update: function(data, now) {
var date, diff, isPost, relative, singlePost, _i, _len, _ref; var date, diff, isPost, relative, singlePost, _i, _len, _ref;
isPost = data instanceof Post; isPost = data instanceof Post;
@ -15901,44 +16151,45 @@
if (Conf['Reveal Spoilers']) { if (Conf['Reveal Spoilers']) {
$.addClass(doc, 'reveal-spoilers'); $.addClass(doc, 'reveal-spoilers');
} }
if (Conf['Remove Spoilers']) { if (!Conf['Remove Spoilers']) {
return $.addClass(doc, 'remove-spoilers');
}
}
};
Report = {
init: function() {
if (!/report/.test(location.search)) {
return; return;
} }
return $.asap((function() { $.addClass(doc, 'remove-spoilers');
return $.id('recaptcha_response_field'); Post.callbacks.push({
}), Report.ready); 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() { node: function(post) {
var field; return RemoveSpoilers.unspoiler(this.nodes.comment);
field = $.id('recaptcha_response_field'); },
$.on(field, 'keydown', function(e) { unspoiler: function(el) {
if (e.keyCode === 8 && !field.value) { var span, spoiler, spoilers, _i, _len;
return $.globalEval('Recaptcha.reload("t")'); spoilers = $$('s', el);
} for (_i = 0, _len = spoilers.length; _i < _len; _i++) {
}); spoiler = spoilers[_i];
return $.on($('form'), 'submit', function(e) { span = $.el('span', {
var response; className: 'removed-spoiler'
e.preventDefault(); });
response = field.value.trim(); $.replace(spoiler, span);
if (!/\s|^\d+$/.test(response)) { $.add(span, __slice.call(spoiler.childNodes));
field.value = "" + response + " " + response; }
}
return this.submit();
});
} }
}; };
Time = { Time = {
init: function() { init: function() {
if (!Conf['Time Formatting']) { var _ref;
if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) {
return; return;
} }
return Post.callbacks.push({ return Post.callbacks.push({
@ -15953,7 +16204,7 @@
return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date); return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date);
}, },
format: function(formatString, 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) { if (c in Time.formatters) {
return Time.formatters[c].call(date); return Time.formatters[c].call(date);
} else { } else {
@ -16029,6 +16280,9 @@
}, },
Y: function() { Y: function() {
return this.getFullYear(); return this.getFullYear();
},
'%': function() {
return '%';
} }
} }
}; };

View File

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

View File

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

View File

@ -33,11 +33,14 @@ Nav =
getThread: -> getThread: ->
for threadRoot in $$ '.thread' 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 if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
return threadRoot return threadRoot
return $ '.board' return $ '.board'
scroll: (delta) -> scroll: (delta) ->
d.activeElement?.blur()
thread = Nav.getThread() thread = Nav.getThread()
axis = if delta is +1 axis = if delta is +1
'following' 'following'
@ -49,4 +52,21 @@ Nav =
# or we're above the first thread and don't want to skip it. # or we're above the first thread and don't want to skip it.
top = Header.getTopOf thread top = Header.getTopOf thread
thread = next if delta is +1 and top < 5 or delta is -1 and top > -5 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 Header.scrollTo thread
if extra > 0 and !Nav.haveExtra
Nav.haveExtra = true
$.on d, 'scroll', Nav.removeExtra
removeExtra: ->
extra = doc.clientHeight - d.body.getBoundingClientRect().bottom
if extra > 0
d.body.style.marginBottom = "#{extra}px"
else
d.body.style.marginBottom = null
delete Nav.haveExtra
$.off d, 'scroll', Nav.removeExtra

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,15 @@
Favicon = Favicon =
init: -> 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 = { items = {
ferongr: [ ferongr: [
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>' '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'

View File

@ -1,6 +1,6 @@
ThreadExcerpt = ThreadExcerpt =
init: -> 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 Thread.callbacks.push
name: 'Thread Excerpt' name: 'Thread Excerpt'

View File

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

View File

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

View File

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