Merge ccd0's IP Count stuff

This commit is contained in:
Zixaphir 2014-12-09 00:28:38 -07:00
parent 0389bcf930
commit 020a68f73a
10 changed files with 570 additions and 156 deletions

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.9.38 - 2014-12-08 * appchan x - Version 2.9.38 - 2014-12-09
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -28,7 +28,7 @@
// ==/UserScript== // ==/UserScript==
/* /*
* appchan x - Version 2.9.38 - 2014-12-08 * appchan x - Version 2.9.38 - 2014-12-09
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -116,7 +116,7 @@
'use strict'; 'use strict';
(function() { (function() {
var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, 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, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, 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, TrashQueue, UI, Unread, Video, 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,
@ -212,9 +212,11 @@
'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'],
'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'],
'Thread Stats': [true, 'Display reply and image count.'], 'Thread Stats': [true, 'Display reply and image count.'],
'Page Count in Stats': [false, 'Display the page count in the thread stats as well.'], 'IP Count in Stats': [true, 'Display the unique IP count in the thread stats.'],
'Page Count in Stats': [true, 'Display the page count in the thread stats.'],
'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'], 'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'],
'Thread Watcher': [true, 'Bookmark threads.'] 'Thread Watcher': [true, 'Bookmark threads.'],
'Mark New IPs': [false, 'Label each post from a new IP with the thread\'s current IP count.']
}, },
'Posting': { 'Posting': {
'Header Shortcut': [true, 'Add a shortcut to the header to toggle the QR.'], 'Header Shortcut': [true, 'Add a shortcut to the header to toggle the QR.'],
@ -228,7 +230,7 @@
'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'], 'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'],
'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'], 'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'],
'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'], 'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'],
'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread, and reload it after you post.'] 'Auto-load captcha': [false, 'Automatically load the captcha in the QR even if your post is empty.']
}, },
'Quote Links': { 'Quote Links': {
'Quote Backlinks': [true, 'Add quote backlinks.'], 'Quote Backlinks': [true, 'Add quote backlinks.'],
@ -3163,6 +3165,7 @@
this.isClosed = false; this.isClosed = false;
this.postLimit = false; this.postLimit = false;
this.fileLimit = false; this.fileLimit = false;
this.ipCount = void 0;
this.OP = null; this.OP = null;
this.catalogView = null; this.catalogView = null;
g.threads.push(this.fullID, board.threads.push(this, this)); g.threads.push(this.fullID, board.threads.push(this, this));
@ -3192,7 +3195,7 @@
}; };
Thread.prototype.setStatus = function(type, status) { Thread.prototype.setStatus = function(type, status) {
var icon, name, root, typeLC; var name, typeLC;
name = "is" + type; name = "is" + type;
if (this[name] === status) { if (this[name] === status) {
return; return;
@ -3202,8 +3205,21 @@
return; return;
} }
typeLC = type.toLowerCase(); typeLC = type.toLowerCase();
this.setIcon('Sticky', this.isSticky);
this.setIcon('Closed', this.isClosed && !this.isArchived);
return this.setIcon('Archived', this.isArchived);
};
Thread.prototype.setIcon = function(type, status) {
var icon, root, typeLC;
typeLC = type.toLowerCase();
icon = $("." + typeLC + "Icon", this.OP.nodes.info);
if (!!icon === status) {
return;
}
if (!status) { if (!status) {
$.rm($("." + typeLC + "Icon", this.OP.nodes.info)); $.rm(icon.previousSibling);
$.rm(icon);
if (this.catalogView) { if (this.catalogView) {
$.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons)); $.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons));
} }
@ -3211,10 +3227,11 @@
} }
icon = $.el('img', { icon = $.el('img', {
src: "" + Build.staticPath + typeLC + Build.gifIcon, src: "" + Build.staticPath + typeLC + Build.gifIcon,
alt: type,
title: type, title: type,
className: "" + typeLC + "Icon" className: "" + typeLC + "Icon retina"
}); });
root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Reply to this post"]', this.OP.nodes.info); root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || $('[title="Reply to this post"]', this.OP.nodes.info);
$.after(root, [$.tn(' '), icon]); $.after(root, [$.tn(' '), icon]);
if (!this.catalogView) { if (!this.catalogView) {
return; return;
@ -9707,29 +9724,23 @@
} }
}, },
save: function(e) { save: function(e) {
var err, _base; var _base;
try { if (this.needed()) {
if (this.needed()) { this.shouldFocus = true;
this.shouldFocus = true; this.reload();
this.reload(); } else {
} else { this.nodes.counter.focus();
this.nodes.counter.focus(); if ((_base = this.timeouts).destroy == null) {
if ((_base = this.timeouts).destroy == null) { _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
_base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
}
} }
console.log(e.detail);
$.forceSync('captchas');
this.captchas.push({
response: e.detail,
timeout: Date.now() + 2 * $.MINUTE
});
this.count();
return $.set('captchas', this.captchas);
} catch (_error) {
err = _error;
return console.log(err);
} }
$.forceSync('captchas');
this.captchas.push({
response: e.detail,
timeout: Date.now() + 2 * $.MINUTE
});
this.count();
return $.set('captchas', this.captchas);
}, },
clear: function() { clear: function() {
var captcha, i, now, _i, _len, _ref; var captcha, i, now, _i, _len, _ref;
@ -11876,6 +11887,84 @@
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAAElBMVEX///8EZgR8ulSk0oT///8EAgQ1A88mAAAAAXRSTlMAQObYZgAAAJtJREFUeF7t17ENgDAMBdGswAqswAqskP1XgUhG37JQQv25a1I4eR1CbopoS3295wIASD1SBZnecwEApO9RRurjPdLcAQDQY9UnBSDEBQAIRA3gfGkA9cfiAwDUHqCnHqDl/AGA0v8AAL4FgO0uAxUcc2cAQEtFjwQoLSMuAMBqsVwtpp4AwNDngPIGAPI5mVsCAELSuZybAQBEF/bX4X+FReD/AAAAAElFTkSuQmCC' logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAAElBMVEX///8EZgR8ulSk0oT///8EAgQ1A88mAAAAAXRSTlMAQObYZgAAAJtJREFUeF7t17ENgDAMBdGswAqswAqskP1XgUhG37JQQv25a1I4eR1CbopoS3295wIASD1SBZnecwEApO9RRurjPdLcAQDQY9UnBSDEBQAIRA3gfGkA9cfiAwDUHqCnHqDl/AGA0v8AAL4FgO0uAxUcc2cAQEtFjwQoLSMuAMBqsVwtpp4AwNDngPIGAPI5mVsCAELSuZybAQBEF/bX4X+FReD/AAAAAElFTkSuQmCC'
}; };
MarkNewIPs = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Mark New IPs']) {
return;
}
return Thread.callbacks.push({
name: 'Mark New IPs',
cb: this.node
});
},
node: function() {
MarkNewIPs.ipCount = this.ipCount;
MarkNewIPs.postIDs = this.posts.keys.map(function(x) {
return +x;
});
return $.on(d, 'ThreadUpdate', MarkNewIPs.onUpdate);
},
onUpdate: function(e) {
var added, fullID, i, ipCount, newPosts, obj, postIDs, removed, x, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1;
_ref = e.detail, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
postIDs = ThreadUpdater.postIDs;
if (ipCount == null) {
return;
}
if (newPosts.length) {
obj = {};
_ref1 = MarkNewIPs.postIDs;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
x = _ref1[_i];
obj[x] = true;
}
added = 0;
for (_j = 0, _len1 = postIDs.length; _j < _len1; _j++) {
x = postIDs[_j];
if (!(x in obj)) {
added++;
}
}
removed = MarkNewIPs.postIDs.length + added - postIDs.length;
switch (ipCount - MarkNewIPs.ipCount) {
case added:
i = MarkNewIPs.ipCount;
for (_k = 0, _len2 = newPosts.length; _k < _len2; _k++) {
fullID = newPosts[_k];
MarkNewIPs.markNew(g.posts[fullID], ++i);
}
break;
case -removed:
for (_l = 0, _len3 = newPosts.length; _l < _len3; _l++) {
fullID = newPosts[_l];
MarkNewIPs.markOld(g.posts[fullID]);
}
}
}
MarkNewIPs.ipCount = ipCount;
return MarkNewIPs.postIDs = postIDs;
},
markNew: function(post, ipCount) {
var counter, suffix;
suffix = {
1: 'st',
2: 'nd',
3: 'rd'
}[ipCount % 10] || Math.floor('th' / fuck(switches));
counter = $.el('span', {
className: 'ip-counter',
textContent: "(" + ipCount + ")"
});
post.nodes.nameBlock.title = "This is the " + ipCount + suffix + " IP in the thread.";
$.add(post.nodes.nameBlock, [$.tn(' '), counter]);
return $.addClass(post.nodes.root, 'new-ip');
},
markOld: function(post) {
post.nodes.nameBlock.title = 'Not the first post from this IP.';
return $.addClass(post.nodes.root, 'old-ip');
}
};
ThreadExcerpt = { ThreadExcerpt = {
init: function() { init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) { if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
@ -11905,9 +11994,9 @@
} }
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=post-count>0</span> / <span id=file-count>0</span>" + (Conf["Page Count in Stats"] ? " / <span id=page-count>0</span>" : "") + "]", innerHTML: "[<span id=post-count>0</span> / \n<span id=file-count>0</span>\n" + (Conf['IP Count in Stats'] ? ' / <span id=ip-count>?</span>' : "") + "\n" + (Conf['Page Count in Stats'] ? ' / <span id=page-count>0</span>' : "") + "]",
id: 'thread-stats', id: 'thread-stats',
title: 'Post Count / File Count' + (Conf["Page Count in Stats"] ? " / Page Count" : "") title: 'Post Count / File Count' + (Conf['IP Count in Stats'] ? " / IPs" : "") + (Conf['Page Count in Stats'] ? " / Page Count" : "")
}); });
$.ready(function() { $.ready(function() {
return Header.addShortcut(sc); return Header.addShortcut(sc);
@ -11921,6 +12010,7 @@
})(this)); })(this));
} }
this.postCountEl = $('#post-count', sc); this.postCountEl = $('#post-count', sc);
this.ipCountEl = $('#ip-count', sc);
this.fileCountEl = $('#file-count', sc); this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc); this.pageCountEl = $('#page-count', sc);
return Thread.callbacks.push({ return Thread.callbacks.push({
@ -11940,7 +12030,7 @@
}); });
ThreadStats.thread = this; ThreadStats.thread = this;
ThreadStats.fetchPage(); ThreadStats.fetchPage();
ThreadStats.update(postCount, fileCount); ThreadStats.update(postCount, fileCount, this.ipCount);
return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate); return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate);
}, },
disconnect: function() { disconnect: function() {
@ -11963,18 +12053,30 @@
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate); return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
}, },
onUpdate: function(e) { onUpdate: function(e) {
var fileCount, postCount, _ref; var fileCount, ipCount, newPosts, postCount, _ref, _ref1;
if (e.detail[404]) { if (e.detail[404]) {
return; return;
} }
_ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount; _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
return ThreadStats.update(postCount, fileCount); ThreadStats.update(postCount, fileCount, ipCount);
if (!Conf["Page Count in Stats"]) {
return;
}
if (newPosts.length) {
ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date;
}
if (ThreadStats.lastPost > ThreadStats.lastPageUpdate && ((_ref1 = ThreadStats.pageCountEl) != null ? _ref1.textContent : void 0) !== '1') {
return ThreadStats.fetchPage();
}
}, },
update: function(postCount, fileCount) { update: function(postCount, fileCount, ipCount) {
var fileCountEl, postCountEl, thread; var fileCountEl, ipCountEl, postCountEl, thread;
thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl; thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl, ipCountEl = ThreadStats.ipCountEl;
postCountEl.textContent = postCount; postCountEl.textContent = postCount;
fileCountEl.textContent = fileCount; fileCountEl.textContent = fileCount;
if ((ipCount != null) && Conf["IP Count in Stats"]) {
ipCountEl.textContent = ipCount;
}
(thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning'); (thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning');
return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning'); return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning');
}, },
@ -12134,8 +12236,12 @@
$.on(window, 'online offline', ThreadUpdater.cb.online); $.on(window, 'online offline', ThreadUpdater.cb.online);
$.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost); $.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost);
$.on(d, 'visibilitychange', ThreadUpdater.cb.visibility); $.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);
ThreadUpdater.cb.online(); if (ThreadUpdater.thread.isArchived) {
return Rice.nodes(ThreadUpdater.dialog); ThreadUpdater.set('status', 'Archived', 'warning');
} else {
ThreadUpdater.cb.online();
}
Rice.nodes(ThreadUpdater.dialog);
}, },
/* /*
@ -12208,36 +12314,72 @@
} }
}, },
load: function(e) { load: function(e) {
var klass, req, text, _ref; var req;
req = ThreadUpdater.req; req = ThreadUpdater.req;
switch (req.status) { switch (req.status) {
case 200: case 200:
g.DEAD = false; g.DEAD = false;
ThreadUpdater.parse(req.response.posts); ThreadUpdater.parse(req.response.posts);
ThreadUpdater.setInterval(); if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning');
ThreadUpdater.kill();
} else {
ThreadUpdater.setInterval();
}
break; break;
case 404: case 404:
g.DEAD = true; $.ajax("//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
ThreadUpdater.set('timer', null); onloadend: function() {
ThreadUpdater.set('status', '404', 'warning'); var confirmed, page, thread, _i, _j, _len, _len1, _ref, _ref1;
clearTimeout(ThreadUpdater.timeoutID); if (this.status === 200) {
ThreadUpdater.thread.kill(); confirmed = true;
$.event('ThreadUpdate', { _ref = this.response;
404: true, for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadID: ThreadUpdater.thread.fullID page = _ref[_i];
_ref1 = page.threads;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
thread = _ref1[_j];
if (thread.no === ThreadUpdater.thread.ID) {
confirmed = false;
break;
}
}
}
} else {
confirmed = false;
}
if (confirmed) {
ThreadUpdater.set('status', '404', 'warning');
return ThreadUpdater.kill();
} else {
return ThreadUpdater.error(req);
}
}
}); });
break; break;
default: default:
ThreadUpdater.outdateCount++; ThreadUpdater.error(req);
ThreadUpdater.setInterval();
_ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
ThreadUpdater.set('status', text, klass);
} }
if (ThreadUpdater.postID) { if (ThreadUpdater.postID) {
return ThreadUpdater.cb.checkpost(); return ThreadUpdater.cb.checkpost();
} }
} }
}, },
kill: function() {
ThreadUpdater.set('timer', '');
clearTimeout(ThreadUpdater.timeoutID);
ThreadUpdater.thread.kill();
return $.event('ThreadUpdate', {
404: true,
threadID: ThreadUpdater.thread.fullID
});
},
error: function(req) {
var klass, text, _ref;
ThreadUpdater.setInterval();
_ref = req.status === 304 ? ['', ''] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
return ThreadUpdater.set('status', text, klass);
},
setInterval: function() { setInterval: function() {
var cur, i, j, limit; var cur, i, j, limit;
i = ThreadUpdater.interval + 1; i = ThreadUpdater.interval + 1;
@ -12319,6 +12461,9 @@
return; return;
} }
ThreadUpdater.thread.setStatus(type, status); ThreadUpdater.thread.setStatus(type, status);
if (type === 'Closed' && ThreadUpdater.thread.isArchived) {
return;
}
change = type === 'Sticky' ? status ? 'now a sticky' : 'not a sticky anymore' : status ? 'now closed' : 'not closed anymore'; change = type === 'Sticky' ? status ? 'now a sticky' : 'not a sticky anymore' : status ? 'now closed' : 'not closed anymore';
return new Notice('info', "The thread is " + change + ".", 30); return new Notice('info', "The thread is " + change + ".", 30);
}, },
@ -12326,10 +12471,14 @@
var OP, count, files, index, node, num, post, postObject, posts, root, scroll, sendEvent, _i, _j, _len, _len1; var OP, count, files, index, node, num, post, postObject, posts, root, scroll, sendEvent, _i, _j, _len, _len1;
OP = postObjects[0]; OP = postObjects[0];
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler;
ThreadUpdater.thread.setStatus('Archived', !!+OP.archived);
ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky); ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky);
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;
if (OP.unique_ips != null) {
ThreadUpdater.thread.ipCount = OP.unique_ips;
}
posts = []; posts = [];
index = []; index = [];
files = []; files = [];
@ -12370,7 +12519,8 @@
return post.fullID; return post.fullID;
}), }),
postCount: OP.replies + 1, postCount: OP.replies + 1,
fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead) fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead),
ipCount: OP.unique_ips
}); });
}; };
if (!count) { if (!count) {
@ -17968,6 +18118,7 @@
init('Thread Updater', ThreadUpdater); init('Thread Updater', ThreadUpdater);
init('Thread Watcher', ThreadWatcher); init('Thread Watcher', ThreadWatcher);
init('Thread Watcher (Menu)', ThreadWatcher.menu); init('Thread Watcher (Menu)', ThreadWatcher.menu);
init('Mark New IPs', MarkNewIPs);
init('Index Navigation', Nav); init('Index Navigation', Nav);
init('Keybinds', Keybinds); init('Keybinds', Keybinds);
init('Show Dice Roll', Dice); init('Show Dice Roll', Dice);

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript // Generated by CoffeeScript
/* /*
* appchan x - Version 2.9.38 - 2014-12-08 * appchan x - Version 2.9.38 - 2014-12-09
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -88,7 +88,7 @@
'use strict'; 'use strict';
(function() { (function() {
var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, 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, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, 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, TrashQueue, UI, Unread, Video, 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,
@ -184,9 +184,11 @@
'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'],
'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'],
'Thread Stats': [true, 'Display reply and image count.'], 'Thread Stats': [true, 'Display reply and image count.'],
'Page Count in Stats': [false, 'Display the page count in the thread stats as well.'], 'IP Count in Stats': [true, 'Display the unique IP count in the thread stats.'],
'Page Count in Stats': [true, 'Display the page count in the thread stats.'],
'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'], 'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'],
'Thread Watcher': [true, 'Bookmark threads.'] 'Thread Watcher': [true, 'Bookmark threads.'],
'Mark New IPs': [false, 'Label each post from a new IP with the thread\'s current IP count.']
}, },
'Posting': { 'Posting': {
'Header Shortcut': [true, 'Add a shortcut to the header to toggle the QR.'], 'Header Shortcut': [true, 'Add a shortcut to the header to toggle the QR.'],
@ -200,7 +202,7 @@
'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'], 'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'],
'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'], 'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'],
'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'], 'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'],
'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread, and reload it after you post.'] 'Auto-load captcha': [false, 'Automatically load the captcha in the QR even if your post is empty.']
}, },
'Quote Links': { 'Quote Links': {
'Quote Backlinks': [true, 'Add quote backlinks.'], 'Quote Backlinks': [true, 'Add quote backlinks.'],
@ -3188,6 +3190,7 @@
this.isClosed = false; this.isClosed = false;
this.postLimit = false; this.postLimit = false;
this.fileLimit = false; this.fileLimit = false;
this.ipCount = void 0;
this.OP = null; this.OP = null;
this.catalogView = null; this.catalogView = null;
g.threads.push(this.fullID, board.threads.push(this, this)); g.threads.push(this.fullID, board.threads.push(this, this));
@ -3217,7 +3220,7 @@
}; };
Thread.prototype.setStatus = function(type, status) { Thread.prototype.setStatus = function(type, status) {
var icon, name, root, typeLC; var name, typeLC;
name = "is" + type; name = "is" + type;
if (this[name] === status) { if (this[name] === status) {
return; return;
@ -3227,8 +3230,21 @@
return; return;
} }
typeLC = type.toLowerCase(); typeLC = type.toLowerCase();
this.setIcon('Sticky', this.isSticky);
this.setIcon('Closed', this.isClosed && !this.isArchived);
return this.setIcon('Archived', this.isArchived);
};
Thread.prototype.setIcon = function(type, status) {
var icon, root, typeLC;
typeLC = type.toLowerCase();
icon = $("." + typeLC + "Icon", this.OP.nodes.info);
if (!!icon === status) {
return;
}
if (!status) { if (!status) {
$.rm($("." + typeLC + "Icon", this.OP.nodes.info)); $.rm(icon.previousSibling);
$.rm(icon);
if (this.catalogView) { if (this.catalogView) {
$.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons)); $.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons));
} }
@ -3236,10 +3252,11 @@
} }
icon = $.el('img', { icon = $.el('img', {
src: "" + Build.staticPath + typeLC + Build.gifIcon, src: "" + Build.staticPath + typeLC + Build.gifIcon,
alt: type,
title: type, title: type,
className: "" + typeLC + "Icon" className: "" + typeLC + "Icon retina"
}); });
root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Reply to this post"]', this.OP.nodes.info); root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || $('[title="Reply to this post"]', this.OP.nodes.info);
$.after(root, [$.tn(' '), icon]); $.after(root, [$.tn(' '), icon]);
if (!this.catalogView) { if (!this.catalogView) {
return; return;
@ -9721,29 +9738,23 @@
} }
}, },
save: function(e) { save: function(e) {
var err, _base; var _base;
try { if (this.needed()) {
if (this.needed()) { this.shouldFocus = true;
this.shouldFocus = true; this.reload();
this.reload(); } else {
} else { this.nodes.counter.focus();
this.nodes.counter.focus(); if ((_base = this.timeouts).destroy == null) {
if ((_base = this.timeouts).destroy == null) { _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
_base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
}
} }
console.log(e.detail);
$.forceSync('captchas');
this.captchas.push({
response: e.detail,
timeout: Date.now() + 2 * $.MINUTE
});
this.count();
return $.set('captchas', this.captchas);
} catch (_error) {
err = _error;
return console.log(err);
} }
$.forceSync('captchas');
this.captchas.push({
response: e.detail,
timeout: Date.now() + 2 * $.MINUTE
});
this.count();
return $.set('captchas', this.captchas);
}, },
clear: function() { clear: function() {
var captcha, i, now, _i, _len, _ref; var captcha, i, now, _i, _len, _ref;
@ -11859,6 +11870,84 @@
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAAElBMVEX///8EZgR8ulSk0oT///8EAgQ1A88mAAAAAXRSTlMAQObYZgAAAJtJREFUeF7t17ENgDAMBdGswAqswAqskP1XgUhG37JQQv25a1I4eR1CbopoS3295wIASD1SBZnecwEApO9RRurjPdLcAQDQY9UnBSDEBQAIRA3gfGkA9cfiAwDUHqCnHqDl/AGA0v8AAL4FgO0uAxUcc2cAQEtFjwQoLSMuAMBqsVwtpp4AwNDngPIGAPI5mVsCAELSuZybAQBEF/bX4X+FReD/AAAAAElFTkSuQmCC' logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAAElBMVEX///8EZgR8ulSk0oT///8EAgQ1A88mAAAAAXRSTlMAQObYZgAAAJtJREFUeF7t17ENgDAMBdGswAqswAqskP1XgUhG37JQQv25a1I4eR1CbopoS3295wIASD1SBZnecwEApO9RRurjPdLcAQDQY9UnBSDEBQAIRA3gfGkA9cfiAwDUHqCnHqDl/AGA0v8AAL4FgO0uAxUcc2cAQEtFjwQoLSMuAMBqsVwtpp4AwNDngPIGAPI5mVsCAELSuZybAQBEF/bX4X+FReD/AAAAAElFTkSuQmCC'
}; };
MarkNewIPs = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Mark New IPs']) {
return;
}
return Thread.callbacks.push({
name: 'Mark New IPs',
cb: this.node
});
},
node: function() {
MarkNewIPs.ipCount = this.ipCount;
MarkNewIPs.postIDs = this.posts.keys.map(function(x) {
return +x;
});
return $.on(d, 'ThreadUpdate', MarkNewIPs.onUpdate);
},
onUpdate: function(e) {
var added, fullID, i, ipCount, newPosts, obj, postIDs, removed, x, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1;
_ref = e.detail, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
postIDs = ThreadUpdater.postIDs;
if (ipCount == null) {
return;
}
if (newPosts.length) {
obj = {};
_ref1 = MarkNewIPs.postIDs;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
x = _ref1[_i];
obj[x] = true;
}
added = 0;
for (_j = 0, _len1 = postIDs.length; _j < _len1; _j++) {
x = postIDs[_j];
if (!(x in obj)) {
added++;
}
}
removed = MarkNewIPs.postIDs.length + added - postIDs.length;
switch (ipCount - MarkNewIPs.ipCount) {
case added:
i = MarkNewIPs.ipCount;
for (_k = 0, _len2 = newPosts.length; _k < _len2; _k++) {
fullID = newPosts[_k];
MarkNewIPs.markNew(g.posts[fullID], ++i);
}
break;
case -removed:
for (_l = 0, _len3 = newPosts.length; _l < _len3; _l++) {
fullID = newPosts[_l];
MarkNewIPs.markOld(g.posts[fullID]);
}
}
}
MarkNewIPs.ipCount = ipCount;
return MarkNewIPs.postIDs = postIDs;
},
markNew: function(post, ipCount) {
var counter, suffix;
suffix = {
1: 'st',
2: 'nd',
3: 'rd'
}[ipCount % 10] || Math.floor('th' / fuck(switches));
counter = $.el('span', {
className: 'ip-counter',
textContent: "(" + ipCount + ")"
});
post.nodes.nameBlock.title = "This is the " + ipCount + suffix + " IP in the thread.";
$.add(post.nodes.nameBlock, [$.tn(' '), counter]);
return $.addClass(post.nodes.root, 'new-ip');
},
markOld: function(post) {
post.nodes.nameBlock.title = 'Not the first post from this IP.';
return $.addClass(post.nodes.root, 'old-ip');
}
};
ThreadExcerpt = { ThreadExcerpt = {
init: function() { init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) { if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
@ -11888,9 +11977,9 @@
} }
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=post-count>0</span> / <span id=file-count>0</span>" + (Conf["Page Count in Stats"] ? " / <span id=page-count>0</span>" : "") + "]", innerHTML: "[<span id=post-count>0</span> / \n<span id=file-count>0</span>\n" + (Conf['IP Count in Stats'] ? ' / <span id=ip-count>?</span>' : "") + "\n" + (Conf['Page Count in Stats'] ? ' / <span id=page-count>0</span>' : "") + "]",
id: 'thread-stats', id: 'thread-stats',
title: 'Post Count / File Count' + (Conf["Page Count in Stats"] ? " / Page Count" : "") title: 'Post Count / File Count' + (Conf['IP Count in Stats'] ? " / IPs" : "") + (Conf['Page Count in Stats'] ? " / Page Count" : "")
}); });
$.ready(function() { $.ready(function() {
return Header.addShortcut(sc); return Header.addShortcut(sc);
@ -11904,6 +11993,7 @@
})(this)); })(this));
} }
this.postCountEl = $('#post-count', sc); this.postCountEl = $('#post-count', sc);
this.ipCountEl = $('#ip-count', sc);
this.fileCountEl = $('#file-count', sc); this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc); this.pageCountEl = $('#page-count', sc);
return Thread.callbacks.push({ return Thread.callbacks.push({
@ -11923,7 +12013,7 @@
}); });
ThreadStats.thread = this; ThreadStats.thread = this;
ThreadStats.fetchPage(); ThreadStats.fetchPage();
ThreadStats.update(postCount, fileCount); ThreadStats.update(postCount, fileCount, this.ipCount);
return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate); return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate);
}, },
disconnect: function() { disconnect: function() {
@ -11946,18 +12036,30 @@
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate); return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
}, },
onUpdate: function(e) { onUpdate: function(e) {
var fileCount, postCount, _ref; var fileCount, ipCount, newPosts, postCount, _ref, _ref1;
if (e.detail[404]) { if (e.detail[404]) {
return; return;
} }
_ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount; _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
return ThreadStats.update(postCount, fileCount); ThreadStats.update(postCount, fileCount, ipCount);
if (!Conf["Page Count in Stats"]) {
return;
}
if (newPosts.length) {
ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date;
}
if (ThreadStats.lastPost > ThreadStats.lastPageUpdate && ((_ref1 = ThreadStats.pageCountEl) != null ? _ref1.textContent : void 0) !== '1') {
return ThreadStats.fetchPage();
}
}, },
update: function(postCount, fileCount) { update: function(postCount, fileCount, ipCount) {
var fileCountEl, postCountEl, thread; var fileCountEl, ipCountEl, postCountEl, thread;
thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl; thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl, ipCountEl = ThreadStats.ipCountEl;
postCountEl.textContent = postCount; postCountEl.textContent = postCount;
fileCountEl.textContent = fileCount; fileCountEl.textContent = fileCount;
if ((ipCount != null) && Conf["IP Count in Stats"]) {
ipCountEl.textContent = ipCount;
}
(thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning'); (thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning');
return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning'); return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning');
}, },
@ -12117,8 +12219,12 @@
$.on(window, 'online offline', ThreadUpdater.cb.online); $.on(window, 'online offline', ThreadUpdater.cb.online);
$.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost); $.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost);
$.on(d, 'visibilitychange', ThreadUpdater.cb.visibility); $.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);
ThreadUpdater.cb.online(); if (ThreadUpdater.thread.isArchived) {
return Rice.nodes(ThreadUpdater.dialog); ThreadUpdater.set('status', 'Archived', 'warning');
} else {
ThreadUpdater.cb.online();
}
Rice.nodes(ThreadUpdater.dialog);
}, },
/* /*
@ -12191,36 +12297,72 @@
} }
}, },
load: function(e) { load: function(e) {
var klass, req, text, _ref; var req;
req = ThreadUpdater.req; req = ThreadUpdater.req;
switch (req.status) { switch (req.status) {
case 200: case 200:
g.DEAD = false; g.DEAD = false;
ThreadUpdater.parse(req.response.posts); ThreadUpdater.parse(req.response.posts);
ThreadUpdater.setInterval(); if (ThreadUpdater.thread.isArchived) {
ThreadUpdater.set('status', 'Archived', 'warning');
ThreadUpdater.kill();
} else {
ThreadUpdater.setInterval();
}
break; break;
case 404: case 404:
g.DEAD = true; $.ajax("//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
ThreadUpdater.set('timer', null); onloadend: function() {
ThreadUpdater.set('status', '404', 'warning'); var confirmed, page, thread, _i, _j, _len, _len1, _ref, _ref1;
clearTimeout(ThreadUpdater.timeoutID); if (this.status === 200) {
ThreadUpdater.thread.kill(); confirmed = true;
$.event('ThreadUpdate', { _ref = this.response;
404: true, for (_i = 0, _len = _ref.length; _i < _len; _i++) {
threadID: ThreadUpdater.thread.fullID page = _ref[_i];
_ref1 = page.threads;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
thread = _ref1[_j];
if (thread.no === ThreadUpdater.thread.ID) {
confirmed = false;
break;
}
}
}
} else {
confirmed = false;
}
if (confirmed) {
ThreadUpdater.set('status', '404', 'warning');
return ThreadUpdater.kill();
} else {
return ThreadUpdater.error(req);
}
}
}); });
break; break;
default: default:
ThreadUpdater.outdateCount++; ThreadUpdater.error(req);
ThreadUpdater.setInterval();
_ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
ThreadUpdater.set('status', text, klass);
} }
if (ThreadUpdater.postID) { if (ThreadUpdater.postID) {
return ThreadUpdater.cb.checkpost(); return ThreadUpdater.cb.checkpost();
} }
} }
}, },
kill: function() {
ThreadUpdater.set('timer', '');
clearTimeout(ThreadUpdater.timeoutID);
ThreadUpdater.thread.kill();
return $.event('ThreadUpdate', {
404: true,
threadID: ThreadUpdater.thread.fullID
});
},
error: function(req) {
var klass, text, _ref;
ThreadUpdater.setInterval();
_ref = req.status === 304 ? ['', ''] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
return ThreadUpdater.set('status', text, klass);
},
setInterval: function() { setInterval: function() {
var cur, i, j, limit; var cur, i, j, limit;
i = ThreadUpdater.interval + 1; i = ThreadUpdater.interval + 1;
@ -12302,6 +12444,9 @@
return; return;
} }
ThreadUpdater.thread.setStatus(type, status); ThreadUpdater.thread.setStatus(type, status);
if (type === 'Closed' && ThreadUpdater.thread.isArchived) {
return;
}
change = type === 'Sticky' ? status ? 'now a sticky' : 'not a sticky anymore' : status ? 'now closed' : 'not closed anymore'; change = type === 'Sticky' ? status ? 'now a sticky' : 'not a sticky anymore' : status ? 'now closed' : 'not closed anymore';
return new Notice('info', "The thread is " + change + ".", 30); return new Notice('info', "The thread is " + change + ".", 30);
}, },
@ -12309,10 +12454,14 @@
var OP, count, files, index, node, num, post, postObject, posts, root, scroll, sendEvent, _i, _j, _len, _len1; var OP, count, files, index, node, num, post, postObject, posts, root, scroll, sendEvent, _i, _j, _len, _len1;
OP = postObjects[0]; OP = postObjects[0];
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler;
ThreadUpdater.thread.setStatus('Archived', !!+OP.archived);
ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky); ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky);
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;
if (OP.unique_ips != null) {
ThreadUpdater.thread.ipCount = OP.unique_ips;
}
posts = []; posts = [];
index = []; index = [];
files = []; files = [];
@ -12353,7 +12502,8 @@
return post.fullID; return post.fullID;
}), }),
postCount: OP.replies + 1, postCount: OP.replies + 1,
fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead) fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead),
ipCount: OP.unique_ips
}); });
}; };
if (!count) { if (!count) {
@ -17953,6 +18103,7 @@
init('Thread Updater', ThreadUpdater); init('Thread Updater', ThreadUpdater);
init('Thread Watcher', ThreadWatcher); init('Thread Watcher', ThreadWatcher);
init('Thread Watcher (Menu)', ThreadWatcher.menu); init('Thread Watcher (Menu)', ThreadWatcher.menu);
init('Mark New IPs', MarkNewIPs);
init('Index Navigation', Nav); init('Index Navigation', Nav);
init('Keybinds', Keybinds); init('Keybinds', Keybinds);
init('Show Dice Roll', Dice); init('Show Dice Roll', Dice);

View File

@ -255,9 +255,13 @@ Config =
true true
'Display reply and image count.' 'Display reply and image count.'
] ]
'IP Count in Stats': [
true
'Display the unique IP count in the thread stats.'
]
'Page Count in Stats': [ 'Page Count in Stats': [
false true
'Display the page count in the thread stats as well.' 'Display the page count in the thread stats.'
] ]
'Updater and Stats in Header': [ 'Updater and Stats in Header': [
true, true,
@ -267,6 +271,10 @@ Config =
true true
'Bookmark threads.' 'Bookmark threads.'
] ]
'Mark New IPs': [
false
'Label each post from a new IP with the thread\'s current IP count.'
]
'Posting': 'Posting':
'Header Shortcut': [ 'Header Shortcut': [
@ -315,7 +323,7 @@ Config =
] ]
'Auto-load captcha': [ 'Auto-load captcha': [
false false
'Automatically load the captcha when you open a thread, and reload it after you post.' 'Automatically load the captcha in the QR even if your post is empty.'
] ]
'Quote Links': 'Quote Links':

View File

@ -170,6 +170,7 @@ Main =
init 'Thread Updater', ThreadUpdater init 'Thread Updater', ThreadUpdater
init 'Thread Watcher', ThreadWatcher init 'Thread Watcher', ThreadWatcher
init 'Thread Watcher (Menu)', ThreadWatcher.menu init 'Thread Watcher (Menu)', ThreadWatcher.menu
init 'Mark New IPs', MarkNewIPs
init 'Index Navigation', Nav init 'Index Navigation', Nav
init 'Keybinds', Keybinds init 'Keybinds', Keybinds
init 'Show Dice Roll', Dice init 'Show Dice Roll', Dice

View File

@ -13,6 +13,7 @@ class Thread
@isClosed = false @isClosed = false
@postLimit = false @postLimit = false
@fileLimit = false @fileLimit = false
@ipCount = undefined
@OP = null @OP = null
@catalogView = null @catalogView = null
@ -35,21 +36,32 @@ class Thread
@[name] = status @[name] = status
return unless @OP return unless @OP
typeLC = type.toLowerCase() typeLC = type.toLowerCase()
@setIcon 'Sticky', @isSticky
@setIcon 'Closed', @isClosed and !@isArchived
@setIcon 'Archived', @isArchived
setIcon: (type, status) ->
typeLC = type.toLowerCase()
icon = $ ".#{typeLC}Icon", @OP.nodes.info
return if !!icon is status
unless status unless status
$.rm $ ".#{typeLC}Icon", @OP.nodes.info $.rm icon.previousSibling
$.rm icon
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView $.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return return
icon = $.el 'img', icon = $.el 'img',
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}" src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
alt: type
title: type title: type
className: "#{typeLC}Icon" className: "#{typeLC}Icon retina"
root = if type is 'Closed' and @isSticky
$ '.stickyIcon', @OP.nodes.info root =
else if g.VIEW is 'index' if type isnt 'Sticky' and @isSticky
$ '.page-num', @OP.nodes.info $ '.stickyIcon', @OP.nodes.info
else else
$ '[title="Reply to this post"]', @OP.nodes.info $('.page-num', @OP.nodes.info) or $('[title="Reply to this post"]', @OP.nodes.info)
$.after root, [$.tn(' '), icon] $.after root, [$.tn(' '), icon]
return unless @catalogView return unless @catalogView

View File

@ -0,0 +1,45 @@
MarkNewIPs =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Mark New IPs']
Thread.callbacks.push
name: 'Mark New IPs'
cb: @node
node: ->
MarkNewIPs.ipCount = @ipCount
MarkNewIPs.postIDs = @posts.keys.map (x) -> +x
$.on d, 'ThreadUpdate', MarkNewIPs.onUpdate
onUpdate: (e) ->
{ipCount, newPosts} = e.detail
{postIDs} = ThreadUpdater
return unless ipCount?
if newPosts.length
obj = {}
obj[x] = true for x in MarkNewIPs.postIDs
added = 0
added++ for x in postIDs when not (x of obj)
removed = MarkNewIPs.postIDs.length + added - postIDs.length
switch ipCount - MarkNewIPs.ipCount
when added
i = MarkNewIPs.ipCount
for fullID in newPosts
MarkNewIPs.markNew g.posts[fullID], ++i
when -removed
for fullID in newPosts
MarkNewIPs.markOld g.posts[fullID]
MarkNewIPs.ipCount = ipCount
MarkNewIPs.postIDs = postIDs
markNew: (post, ipCount) ->
suffix = {1: 'st', 2: 'nd', 3: 'rd'}[ipCount % 10] or 'th' // fuck switches
counter = $.el 'span',
className: 'ip-counter'
textContent: "(#{ipCount})"
post.nodes.nameBlock.title = "This is the #{ipCount}#{suffix} IP in the thread."
$.add post.nodes.nameBlock, [$.tn(' '), counter]
$.addClass post.nodes.root, 'new-ip'
markOld: (post) ->
post.nodes.nameBlock.title = 'Not the first post from this IP.'
$.addClass post.nodes.root, 'old-ip'

View File

@ -4,9 +4,17 @@ ThreadStats =
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=post-count>0</span> / <span id=file-count>0</span>#{if Conf["Page Count in Stats"] then " / <span id=page-count>0</span>" else ""}]" innerHTML: """
[<span id=post-count>0</span> /
<span id=file-count>0</span>
#{if Conf['IP Count in Stats'] then ' / <span id=ip-count>?</span>' else ""}
#{if Conf['Page Count in Stats'] then ' / <span id=page-count>0</span>' else ""}]
"""
id: 'thread-stats' id: 'thread-stats'
title: 'Post Count / File Count' + (if Conf["Page Count in Stats"] then " / Page Count" else "") title:
'Post Count / File Count' +
(if Conf['IP Count in Stats'] then " / IPs" else "") +
(if Conf['Page Count in Stats'] then " / Page Count" else "")
$.ready -> $.ready ->
Header.addShortcut sc Header.addShortcut sc
else else
@ -15,8 +23,9 @@ ThreadStats =
$.ready => $.ready =>
$.add d.body, sc $.add d.body, sc
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @ipCountEl = $ '#ip-count', sc
@fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc @pageCountEl = $ '#page-count', sc
Thread.callbacks.push Thread.callbacks.push
@ -31,7 +40,7 @@ ThreadStats =
fileCount++ if post.file fileCount++ if post.file
ThreadStats.thread = @ ThreadStats.thread = @
ThreadStats.fetchPage() ThreadStats.fetchPage()
ThreadStats.update postCount, fileCount ThreadStats.update postCount, fileCount, @ipCount
$.on d, 'ThreadUpdate', ThreadStats.onUpdate $.on d, 'ThreadUpdate', ThreadStats.onUpdate
disconnect: -> disconnect: ->
@ -56,13 +65,20 @@ ThreadStats =
onUpdate: (e) -> onUpdate: (e) ->
return if e.detail[404] return if e.detail[404]
{postCount, fileCount} = e.detail {postCount, fileCount, ipCount, newPosts} = e.detail
ThreadStats.update postCount, fileCount ThreadStats.update postCount, fileCount, ipCount
return unless Conf["Page Count in Stats"]
if newPosts.length
ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date
if ThreadStats.lastPost > ThreadStats.lastPageUpdate and ThreadStats.pageCountEl?.textContent isnt '1'
ThreadStats.fetchPage()
update: (postCount, fileCount) -> update: (postCount, fileCount, ipCount) ->
{thread, postCountEl, fileCountEl} = ThreadStats {thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats
postCountEl.textContent = postCount postCountEl.textContent = postCount
fileCountEl.textContent = fileCount fileCountEl.textContent = fileCount
if ipCount? and Conf["IP Count in Stats"]
ipCountEl.textContent = ipCount
(if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning' (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning'
(if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning' (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning'

View File

@ -104,8 +104,14 @@ ThreadUpdater =
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost $.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility $.on d, 'visibilitychange', ThreadUpdater.cb.visibility
ThreadUpdater.cb.online() if ThreadUpdater.thread.isArchived
ThreadUpdater.set 'status', 'Archived', 'warning'
else
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/
@ -160,28 +166,50 @@ ThreadUpdater =
when 200 when 200
g.DEAD = false g.DEAD = false
ThreadUpdater.parse req.response.posts ThreadUpdater.parse req.response.posts
ThreadUpdater.setInterval() if ThreadUpdater.thread.isArchived
when 404 ThreadUpdater.set 'status', 'Archived', 'warning'
g.DEAD = true ThreadUpdater.kill()
ThreadUpdater.set 'timer', null
ThreadUpdater.set 'status', '404', 'warning'
clearTimeout ThreadUpdater.timeoutID
ThreadUpdater.thread.kill()
$.event 'ThreadUpdate',
404: true
threadID: ThreadUpdater.thread.fullID
else
ThreadUpdater.outdateCount++
ThreadUpdater.setInterval()
[text, klass] = if req.status is 304
[null, null]
else else
["#{req.statusText} (#{req.status})", 'warning'] ThreadUpdater.setInterval()
ThreadUpdater.set 'status', text, klass when 404
# XXX workaround for 4chan sending false 404s
$.ajax "//a.4cdn.org/#{ThreadUpdater.thread.board}/catalog.json", onloadend: ->
if @status is 200
confirmed = true
for page in @response
for thread in page.threads
if thread.no is ThreadUpdater.thread.ID
confirmed = false
break
else
confirmed = false
if confirmed
ThreadUpdater.set 'status', '404', 'warning'
ThreadUpdater.kill()
else
ThreadUpdater.error req
else
ThreadUpdater.error req
if ThreadUpdater.postID if ThreadUpdater.postID
ThreadUpdater.cb.checkpost() ThreadUpdater.cb.checkpost()
kill: ->
ThreadUpdater.set 'timer', ''
clearTimeout ThreadUpdater.timeoutID
ThreadUpdater.thread.kill()
$.event 'ThreadUpdate',
404: true
threadID: ThreadUpdater.thread.fullID
error: (req) ->
ThreadUpdater.setInterval()
[text, klass] = if req.status is 304
['', '']
else
["#{req.statusText} (#{req.status})", 'warning']
ThreadUpdater.set 'status', text, klass
setInterval: -> setInterval: ->
i = ThreadUpdater.interval + 1 i = ThreadUpdater.interval + 1
@ -256,6 +284,7 @@ ThreadUpdater =
updateThreadStatus: (type, status) -> updateThreadStatus: (type, status) ->
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
ThreadUpdater.thread.setStatus type, status ThreadUpdater.thread.setStatus type, status
return if type is 'Closed' and ThreadUpdater.thread.isArchived
change = if type is 'Sticky' change = if type is 'Sticky'
if status if status
'now a sticky' 'now a sticky'
@ -272,10 +301,13 @@ ThreadUpdater =
OP = postObjects[0] OP = postObjects[0]
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
# XXX Some threads such as /g/'s sticky https://a.4cdn.org/g/thread/39894014.json still use a string as the archived property.
ThreadUpdater.thread.setStatus 'Archived', !!+OP.archived
ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
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?
posts = [] # post objects posts = [] # post objects
index = [] # existing posts index = [] # existing posts
@ -317,6 +349,7 @@ ThreadUpdater =
newPosts: posts.map (post) -> post.fullID newPosts: posts.map (post) -> post.fullID
postCount: OP.replies + 1 postCount: OP.replies + 1
fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead) fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead)
ipCount: OP.unique_ips
unless count unless count
ThreadUpdater.set 'status', null, null ThreadUpdater.set 'status', null, null

View File

@ -98,22 +98,19 @@ QR.captcha =
else else
null null
save: (e) -> try save: (e) ->
if @needed() if @needed()
@shouldFocus = true @shouldFocus = true
@reload() @reload()
else else
@nodes.counter.focus() @nodes.counter.focus()
@timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND @timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND
console.log e.detail
$.forceSync 'captchas' $.forceSync 'captchas'
@captchas.push @captchas.push
response: e.detail response: e.detail
timeout: Date.now() + 2 * $.MINUTE timeout: Date.now() + 2 * $.MINUTE
@count() @count()
$.set 'captchas', @captchas $.set 'captchas', @captchas
catch err
console.log err
clear: -> clear: ->
return unless @captchas.length return unless @captchas.length