diff --git a/LICENSE b/LICENSE
index eeb4cc20d..a5ab91e88 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js
index 8eccab752..4fa2b8058 100644
--- a/builds/appchan-x.user.js
+++ b/builds/appchan-x.user.js
@@ -28,7 +28,7 @@
// ==/UserScript==
/*
-* appchan x - Version 2.9.38 - 2014-12-08
+* appchan x - Version 2.9.38 - 2014-12-09
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@@ -116,7 +116,7 @@
'use strict';
(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,
__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,
@@ -212,9 +212,11 @@
'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 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.'],
- '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': {
'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.'],
'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.'],
- '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 Backlinks': [true, 'Add quote backlinks.'],
@@ -3163,6 +3165,7 @@
this.isClosed = false;
this.postLimit = false;
this.fileLimit = false;
+ this.ipCount = void 0;
this.OP = null;
this.catalogView = null;
g.threads.push(this.fullID, board.threads.push(this, this));
@@ -3192,7 +3195,7 @@
};
Thread.prototype.setStatus = function(type, status) {
- var icon, name, root, typeLC;
+ var name, typeLC;
name = "is" + type;
if (this[name] === status) {
return;
@@ -3202,8 +3205,21 @@
return;
}
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) {
- $.rm($("." + typeLC + "Icon", this.OP.nodes.info));
+ $.rm(icon.previousSibling);
+ $.rm(icon);
if (this.catalogView) {
$.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons));
}
@@ -3211,10 +3227,11 @@
}
icon = $.el('img', {
src: "" + Build.staticPath + typeLC + Build.gifIcon,
+ alt: 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]);
if (!this.catalogView) {
return;
@@ -9707,29 +9724,23 @@
}
},
save: function(e) {
- var err, _base;
- try {
- if (this.needed()) {
- this.shouldFocus = true;
- this.reload();
- } else {
- this.nodes.counter.focus();
- if ((_base = this.timeouts).destroy == null) {
- _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
- }
+ var _base;
+ if (this.needed()) {
+ this.shouldFocus = true;
+ this.reload();
+ } else {
+ this.nodes.counter.focus();
+ if ((_base = this.timeouts).destroy == null) {
+ _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() {
var captcha, i, now, _i, _len, _ref;
@@ -11876,6 +11887,84 @@
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 = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
@@ -11905,9 +11994,9 @@
}
if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', {
- innerHTML: "[0 / 0" + (Conf["Page Count in Stats"] ? " / 0" : "") + "]",
+ innerHTML: "[0 / \n0\n" + (Conf['IP Count in Stats'] ? ' / ?' : "") + "\n" + (Conf['Page Count in Stats'] ? ' / 0' : "") + "]",
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() {
return Header.addShortcut(sc);
@@ -11921,6 +12010,7 @@
})(this));
}
this.postCountEl = $('#post-count', sc);
+ this.ipCountEl = $('#ip-count', sc);
this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc);
return Thread.callbacks.push({
@@ -11940,7 +12030,7 @@
});
ThreadStats.thread = this;
ThreadStats.fetchPage();
- ThreadStats.update(postCount, fileCount);
+ ThreadStats.update(postCount, fileCount, this.ipCount);
return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
disconnect: function() {
@@ -11963,18 +12053,30 @@
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
onUpdate: function(e) {
- var fileCount, postCount, _ref;
+ var fileCount, ipCount, newPosts, postCount, _ref, _ref1;
if (e.detail[404]) {
return;
}
- _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount;
- return ThreadStats.update(postCount, fileCount);
+ _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
+ 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) {
- var fileCountEl, postCountEl, thread;
- thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl;
+ update: function(postCount, fileCount, ipCount) {
+ var fileCountEl, ipCountEl, postCountEl, thread;
+ thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl, ipCountEl = ThreadStats.ipCountEl;
postCountEl.textContent = postCount;
fileCountEl.textContent = fileCount;
+ if ((ipCount != null) && Conf["IP Count in Stats"]) {
+ ipCountEl.textContent = ipCount;
+ }
(thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning');
return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning');
},
@@ -12134,8 +12236,12 @@
$.on(window, 'online offline', ThreadUpdater.cb.online);
$.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost);
$.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);
- ThreadUpdater.cb.online();
- return Rice.nodes(ThreadUpdater.dialog);
+ if (ThreadUpdater.thread.isArchived) {
+ ThreadUpdater.set('status', 'Archived', 'warning');
+ } else {
+ ThreadUpdater.cb.online();
+ }
+ Rice.nodes(ThreadUpdater.dialog);
},
/*
@@ -12208,36 +12314,72 @@
}
},
load: function(e) {
- var klass, req, text, _ref;
+ var req;
req = ThreadUpdater.req;
switch (req.status) {
case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts);
- ThreadUpdater.setInterval();
+ if (ThreadUpdater.thread.isArchived) {
+ ThreadUpdater.set('status', 'Archived', 'warning');
+ ThreadUpdater.kill();
+ } else {
+ ThreadUpdater.setInterval();
+ }
break;
case 404:
- g.DEAD = true;
- ThreadUpdater.set('timer', null);
- ThreadUpdater.set('status', '404', 'warning');
- clearTimeout(ThreadUpdater.timeoutID);
- ThreadUpdater.thread.kill();
- $.event('ThreadUpdate', {
- 404: true,
- threadID: ThreadUpdater.thread.fullID
+ $.ajax("//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
+ onloadend: function() {
+ var confirmed, page, thread, _i, _j, _len, _len1, _ref, _ref1;
+ if (this.status === 200) {
+ confirmed = true;
+ _ref = this.response;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ 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;
default:
- ThreadUpdater.outdateCount++;
- ThreadUpdater.setInterval();
- _ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
- ThreadUpdater.set('status', text, klass);
+ ThreadUpdater.error(req);
}
if (ThreadUpdater.postID) {
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() {
var cur, i, j, limit;
i = ThreadUpdater.interval + 1;
@@ -12319,6 +12461,9 @@
return;
}
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';
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;
OP = postObjects[0];
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler;
+ ThreadUpdater.thread.setStatus('Archived', !!+OP.archived);
ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky);
ThreadUpdater.updateThreadStatus('Closed', !!OP.closed);
ThreadUpdater.thread.postLimit = !!OP.bumplimit;
ThreadUpdater.thread.fileLimit = !!OP.imagelimit;
+ if (OP.unique_ips != null) {
+ ThreadUpdater.thread.ipCount = OP.unique_ips;
+ }
posts = [];
index = [];
files = [];
@@ -12370,7 +12519,8 @@
return post.fullID;
}),
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) {
@@ -17968,6 +18118,7 @@
init('Thread Updater', ThreadUpdater);
init('Thread Watcher', ThreadWatcher);
init('Thread Watcher (Menu)', ThreadWatcher.menu);
+ init('Mark New IPs', MarkNewIPs);
init('Index Navigation', Nav);
init('Keybinds', Keybinds);
init('Show Dice Roll', Dice);
diff --git a/builds/crx/script.js b/builds/crx/script.js
index 415ef2761..62baca509 100644
--- a/builds/crx/script.js
+++ b/builds/crx/script.js
@@ -1,6 +1,6 @@
// 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.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@@ -88,7 +88,7 @@
'use strict';
(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,
__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,
@@ -184,9 +184,11 @@
'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 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.'],
- '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': {
'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.'],
'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.'],
- '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 Backlinks': [true, 'Add quote backlinks.'],
@@ -3188,6 +3190,7 @@
this.isClosed = false;
this.postLimit = false;
this.fileLimit = false;
+ this.ipCount = void 0;
this.OP = null;
this.catalogView = null;
g.threads.push(this.fullID, board.threads.push(this, this));
@@ -3217,7 +3220,7 @@
};
Thread.prototype.setStatus = function(type, status) {
- var icon, name, root, typeLC;
+ var name, typeLC;
name = "is" + type;
if (this[name] === status) {
return;
@@ -3227,8 +3230,21 @@
return;
}
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) {
- $.rm($("." + typeLC + "Icon", this.OP.nodes.info));
+ $.rm(icon.previousSibling);
+ $.rm(icon);
if (this.catalogView) {
$.rm($("." + typeLC + "Icon", this.catalogView.nodes.icons));
}
@@ -3236,10 +3252,11 @@
}
icon = $.el('img', {
src: "" + Build.staticPath + typeLC + Build.gifIcon,
+ alt: 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]);
if (!this.catalogView) {
return;
@@ -9721,29 +9738,23 @@
}
},
save: function(e) {
- var err, _base;
- try {
- if (this.needed()) {
- this.shouldFocus = true;
- this.reload();
- } else {
- this.nodes.counter.focus();
- if ((_base = this.timeouts).destroy == null) {
- _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
- }
+ var _base;
+ if (this.needed()) {
+ this.shouldFocus = true;
+ this.reload();
+ } else {
+ this.nodes.counter.focus();
+ if ((_base = this.timeouts).destroy == null) {
+ _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() {
var captcha, i, now, _i, _len, _ref;
@@ -11859,6 +11870,84 @@
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 = {
init: function() {
if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) {
@@ -11888,9 +11977,9 @@
}
if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', {
- innerHTML: "[0 / 0" + (Conf["Page Count in Stats"] ? " / 0" : "") + "]",
+ innerHTML: "[0 / \n0\n" + (Conf['IP Count in Stats'] ? ' / ?' : "") + "\n" + (Conf['Page Count in Stats'] ? ' / 0' : "") + "]",
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() {
return Header.addShortcut(sc);
@@ -11904,6 +11993,7 @@
})(this));
}
this.postCountEl = $('#post-count', sc);
+ this.ipCountEl = $('#ip-count', sc);
this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc);
return Thread.callbacks.push({
@@ -11923,7 +12013,7 @@
});
ThreadStats.thread = this;
ThreadStats.fetchPage();
- ThreadStats.update(postCount, fileCount);
+ ThreadStats.update(postCount, fileCount, this.ipCount);
return $.on(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
disconnect: function() {
@@ -11946,18 +12036,30 @@
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
onUpdate: function(e) {
- var fileCount, postCount, _ref;
+ var fileCount, ipCount, newPosts, postCount, _ref, _ref1;
if (e.detail[404]) {
return;
}
- _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount;
- return ThreadStats.update(postCount, fileCount);
+ _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount, ipCount = _ref.ipCount, newPosts = _ref.newPosts;
+ 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) {
- var fileCountEl, postCountEl, thread;
- thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl;
+ update: function(postCount, fileCount, ipCount) {
+ var fileCountEl, ipCountEl, postCountEl, thread;
+ thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl, ipCountEl = ThreadStats.ipCountEl;
postCountEl.textContent = postCount;
fileCountEl.textContent = fileCount;
+ if ((ipCount != null) && Conf["IP Count in Stats"]) {
+ ipCountEl.textContent = ipCount;
+ }
(thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning');
return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning');
},
@@ -12117,8 +12219,12 @@
$.on(window, 'online offline', ThreadUpdater.cb.online);
$.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost);
$.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);
- ThreadUpdater.cb.online();
- return Rice.nodes(ThreadUpdater.dialog);
+ if (ThreadUpdater.thread.isArchived) {
+ ThreadUpdater.set('status', 'Archived', 'warning');
+ } else {
+ ThreadUpdater.cb.online();
+ }
+ Rice.nodes(ThreadUpdater.dialog);
},
/*
@@ -12191,36 +12297,72 @@
}
},
load: function(e) {
- var klass, req, text, _ref;
+ var req;
req = ThreadUpdater.req;
switch (req.status) {
case 200:
g.DEAD = false;
ThreadUpdater.parse(req.response.posts);
- ThreadUpdater.setInterval();
+ if (ThreadUpdater.thread.isArchived) {
+ ThreadUpdater.set('status', 'Archived', 'warning');
+ ThreadUpdater.kill();
+ } else {
+ ThreadUpdater.setInterval();
+ }
break;
case 404:
- g.DEAD = true;
- ThreadUpdater.set('timer', null);
- ThreadUpdater.set('status', '404', 'warning');
- clearTimeout(ThreadUpdater.timeoutID);
- ThreadUpdater.thread.kill();
- $.event('ThreadUpdate', {
- 404: true,
- threadID: ThreadUpdater.thread.fullID
+ $.ajax("//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
+ onloadend: function() {
+ var confirmed, page, thread, _i, _j, _len, _len1, _ref, _ref1;
+ if (this.status === 200) {
+ confirmed = true;
+ _ref = this.response;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ 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;
default:
- ThreadUpdater.outdateCount++;
- ThreadUpdater.setInterval();
- _ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
- ThreadUpdater.set('status', text, klass);
+ ThreadUpdater.error(req);
}
if (ThreadUpdater.postID) {
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() {
var cur, i, j, limit;
i = ThreadUpdater.interval + 1;
@@ -12302,6 +12444,9 @@
return;
}
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';
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;
OP = postObjects[0];
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler;
+ ThreadUpdater.thread.setStatus('Archived', !!+OP.archived);
ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky);
ThreadUpdater.updateThreadStatus('Closed', !!OP.closed);
ThreadUpdater.thread.postLimit = !!OP.bumplimit;
ThreadUpdater.thread.fileLimit = !!OP.imagelimit;
+ if (OP.unique_ips != null) {
+ ThreadUpdater.thread.ipCount = OP.unique_ips;
+ }
posts = [];
index = [];
files = [];
@@ -12353,7 +12502,8 @@
return post.fullID;
}),
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) {
@@ -17953,6 +18103,7 @@
init('Thread Updater', ThreadUpdater);
init('Thread Watcher', ThreadWatcher);
init('Thread Watcher (Menu)', ThreadWatcher.menu);
+ init('Mark New IPs', MarkNewIPs);
init('Index Navigation', Nav);
init('Keybinds', Keybinds);
init('Show Dice Roll', Dice);
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index d16ba38ae..f0b92bb3f 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -255,9 +255,13 @@ Config =
true
'Display reply and image count.'
]
+ 'IP Count in Stats': [
+ true
+ 'Display the unique IP count in the thread stats.'
+ ]
'Page Count in Stats': [
- false
- 'Display the page count in the thread stats as well.'
+ true
+ 'Display the page count in the thread stats.'
]
'Updater and Stats in Header': [
true,
@@ -267,6 +271,10 @@ Config =
true
'Bookmark threads.'
]
+ 'Mark New IPs': [
+ false
+ 'Label each post from a new IP with the thread\'s current IP count.'
+ ]
'Posting':
'Header Shortcut': [
@@ -315,7 +323,7 @@ Config =
]
'Auto-load captcha': [
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':
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 84dc44b01..dd97750aa 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -170,6 +170,7 @@ Main =
init 'Thread Updater', ThreadUpdater
init 'Thread Watcher', ThreadWatcher
init 'Thread Watcher (Menu)', ThreadWatcher.menu
+ init 'Mark New IPs', MarkNewIPs
init 'Index Navigation', Nav
init 'Keybinds', Keybinds
init 'Show Dice Roll', Dice
diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class
index 676ae8e4a..08adc1f94 100755
--- a/src/General/lib/thread.class
+++ b/src/General/lib/thread.class
@@ -13,6 +13,7 @@ class Thread
@isClosed = false
@postLimit = false
@fileLimit = false
+ @ipCount = undefined
@OP = null
@catalogView = null
@@ -35,21 +36,32 @@ class Thread
@[name] = status
return unless @OP
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
- $.rm $ ".#{typeLC}Icon", @OP.nodes.info
+ $.rm icon.previousSibling
+ $.rm icon
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return
-
icon = $.el 'img',
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
+ alt: type
title: type
- className: "#{typeLC}Icon"
- root = if type is 'Closed' and @isSticky
- $ '.stickyIcon', @OP.nodes.info
- else if g.VIEW is 'index'
- $ '.page-num', @OP.nodes.info
- else
- $ '[title="Reply to this post"]', @OP.nodes.info
+ className: "#{typeLC}Icon retina"
+
+ root =
+ if type isnt 'Sticky' and @isSticky
+ $ '.stickyIcon', @OP.nodes.info
+ else
+ $('.page-num', @OP.nodes.info) or $('[title="Reply to this post"]', @OP.nodes.info)
$.after root, [$.tn(' '), icon]
return unless @catalogView
diff --git a/src/Monitoring/MarkNewIPs.coffee b/src/Monitoring/MarkNewIPs.coffee
new file mode 100644
index 000000000..d146a2f5e
--- /dev/null
+++ b/src/Monitoring/MarkNewIPs.coffee
@@ -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'
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index 750f87146..9e8369d6a 100755
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -4,9 +4,17 @@ ThreadStats =
if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span',
- innerHTML: "[0 / 0#{if Conf["Page Count in Stats"] then " / 0" else ""}]"
+ innerHTML: """
+ [0 /
+ 0
+ #{if Conf['IP Count in Stats'] then ' / ?' else ""}
+ #{if Conf['Page Count in Stats'] then ' / 0' else ""}]
+ """
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 ->
Header.addShortcut sc
else
@@ -15,8 +23,9 @@ ThreadStats =
$.ready =>
$.add d.body, sc
- @postCountEl = $ '#post-count', sc
- @fileCountEl = $ '#file-count', sc
+ @postCountEl = $ '#post-count', sc
+ @ipCountEl = $ '#ip-count', sc
+ @fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc
Thread.callbacks.push
@@ -31,7 +40,7 @@ ThreadStats =
fileCount++ if post.file
ThreadStats.thread = @
ThreadStats.fetchPage()
- ThreadStats.update postCount, fileCount
+ ThreadStats.update postCount, fileCount, @ipCount
$.on d, 'ThreadUpdate', ThreadStats.onUpdate
disconnect: ->
@@ -56,13 +65,20 @@ ThreadStats =
onUpdate: (e) ->
return if e.detail[404]
- {postCount, fileCount} = e.detail
- ThreadStats.update postCount, fileCount
+ {postCount, fileCount, ipCount, newPosts} = e.detail
+ 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) ->
- {thread, postCountEl, fileCountEl} = ThreadStats
+ update: (postCount, fileCount, ipCount) ->
+ {thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats
postCountEl.textContent = postCount
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.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning'
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index e6e565db9..5c2924c0a 100755
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -104,8 +104,14 @@ ThreadUpdater =
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
$.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
+
+ return
###
http://freesound.org/people/pierrecartoons1979/sounds/90112/
@@ -160,28 +166,50 @@ ThreadUpdater =
when 200
g.DEAD = false
ThreadUpdater.parse req.response.posts
- ThreadUpdater.setInterval()
- when 404
- g.DEAD = true
- 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]
+ if ThreadUpdater.thread.isArchived
+ ThreadUpdater.set 'status', 'Archived', 'warning'
+ ThreadUpdater.kill()
else
- ["#{req.statusText} (#{req.status})", 'warning']
- ThreadUpdater.set 'status', text, klass
+ ThreadUpdater.setInterval()
+ 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
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: ->
i = ThreadUpdater.interval + 1
@@ -256,6 +284,7 @@ ThreadUpdater =
updateThreadStatus: (type, status) ->
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
ThreadUpdater.thread.setStatus type, status
+ return if type is 'Closed' and ThreadUpdater.thread.isArchived
change = if type is 'Sticky'
if status
'now a sticky'
@@ -272,10 +301,13 @@ ThreadUpdater =
OP = postObjects[0]
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 'Closed', !!OP.closed
ThreadUpdater.thread.postLimit = !!OP.bumplimit
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
+ ThreadUpdater.thread.ipCount = OP.unique_ips if OP.unique_ips?
posts = [] # post objects
index = [] # existing posts
@@ -317,6 +349,7 @@ ThreadUpdater =
newPosts: posts.map (post) -> post.fullID
postCount: OP.replies + 1
fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead)
+ ipCount: OP.unique_ips
unless count
ThreadUpdater.set 'status', null, null
diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee
index 0d83b9c1f..e098ecc74 100644
--- a/src/Posting/QR.captcha.coffee
+++ b/src/Posting/QR.captcha.coffee
@@ -98,22 +98,19 @@ QR.captcha =
else
null
- save: (e) -> try
+ save: (e) ->
if @needed()
@shouldFocus = true
@reload()
else
@nodes.counter.focus()
@timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND
- console.log e.detail
$.forceSync 'captchas'
@captchas.push
response: e.detail
timeout: Date.now() + 2 * $.MINUTE
@count()
$.set 'captchas', @captchas
- catch err
- console.log err
clear: ->
return unless @captchas.length